From 78aff362ff0c2c0740192d317e898367533cc061 Mon Sep 17 00:00:00 2001 From: slhx agent Date: Fri, 5 Jun 2026 10:03:39 +0200 Subject: [PATCH] docs(recipes): add pwa offline boundary Document optional PWA/offline support as an adapter boundary: cached shell, domain-command queues, server-canonical replay, conflict/fingerprint behavior, and sync-crate shape without adding a client state graph or service worker to core. req: canonical_authoring/008 req: runtime/003 req: runtime/004 req: auth/002 req: auth/004 req: failure/004 req: failure/005 req: sync/001 req: sync/007 --- README.md | 2 +- docs/recipes/pwa-offline.md | 132 ++++++++++++++++++++++++++++++++++++ docs/tutorial-saas.md | 8 ++- docs/v1-readiness.md | 10 ++- examples/saas/README.md | 2 +- 5 files changed, 143 insertions(+), 11 deletions(-) create mode 100644 docs/recipes/pwa-offline.md diff --git a/README.md b/README.md index 64f8765..6a9703a 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ and integrate at explicit boundaries. req: laws/002 req: auth/001 vendor providers or add framework-specific magic. See `docs/recipes/observability-flags.md` and `docs/recipes/deploy-versioning.md`. - **PWA/offline/sync:** optional adapters may reuse generated targets/effects, but core hemx must not gain a mandatory client state graph or local app - runtime. req: canonical_authoring/008 + runtime. See `docs/recipes/pwa-offline.md`. req: canonical_authoring/008 ## Escape hatches diff --git a/docs/recipes/pwa-offline.md b/docs/recipes/pwa-offline.md new file mode 100644 index 0000000..c783cb3 --- /dev/null +++ b/docs/recipes/pwa-offline.md @@ -0,0 +1,132 @@ +# Recipe: optional PWA/offline adapter boundary + +This recipe describes how a hemx app can add a cached shell or offline queue +without turning core hemx into a client app framework. Offline/PWA support is +opt-in adapter territory: reuse generated targets and server-canonical effects, +but keep service workers, queues, conflict policy, and local storage outside +`hemx`, `hemx-core`, `hemx-build`, `hemx-derive`, `hemx-axum`, and the tiny +runtime. req: canonical_authoring/008 req: runtime/003 req: runtime/004 + +Use this only after the normal server-first path works. A hemx app is allowed to +fail interactions while offline and recover with a full page once the network is +back. + +## Boundary rule + +Keep these concerns outside hemx core: + +- service worker registration and cache policy +- local persistence stores such as IndexedDB +- offline mutation queues +- background sync, retry, and conflict resolution +- CRDTs or collaborative sync engines +- analytics for offline queue health + +Keep these concerns in app/integration code: + +- deciding which pages/assets are safe to cache +- deciding which mutations may be queued +- serializing a domain command for later replay +- reconciling queued commands with server-canonical effect responses +- showing generated UI feedback such as "offline", "queued", "synced", or + "conflict" + +The normal path remains server-first typed handlers and generated effects. + +## Cached shell + +A PWA shell may cache page HTML, CSS, the matching `/hemx.js`, and explicit island +scripts for one release. It must obey the same release-unit policy as +`docs/recipes/deploy-versioning.md`: cached server HTML and cached runtime assets +must be compatible with the server that receives later interactions. req: abi/002 req: abi/004 + +Recommended behavior: + +- cache only content-addressed or release-scoped assets +- evict cached shells on release/fingerprint mismatch +- fall back to a full page GET when unsure +- do not patch cached DOM with selector retargeting + +The service worker is app code. hemx core should not register or own it. + +## Offline mutation queue + +If a mutation is safe to queue, store an app-domain command, not a raw DOM patch +or runtime opcode: + +```rust +#[derive(serde::Serialize, serde::Deserialize)] +pub enum OfflineCommand { + CreateProject { csrf: CsrfToken, name: ProjectName }, +} +``` + +When the browser is offline, the adapter can add the command to an IndexedDB +queue and show generated UI feedback from the app shell: + +```rust +pub fn queued_project_notice() -> impl IntoEffect { + ( + dashboard::flash.set("Project will be created when you are back online"), + dashboard::live_status.set("Offline: 1 change queued"), + ) +} +``` + +When the network returns, replay the command to the normal server endpoint. The +server still runs auth/session, CSRF, validation, persistence, and returns the +canonical generated effects. req: auth/002 req: auth/004 req: failure/004 + +Do not store `EffectBatch` as the source of truth for later replay. Effects are +UI outcomes for a server decision; queued commands are user intent that the +server must validate again. + +## Reconciliation + +The server is authoritative. A replay may succeed, fail validation, fail auth, +fail CSRF, or conflict with newer state. The adapter should apply the returned +server effects when compatible and otherwise navigate/reload to server-rendered +truth. + +Suggested outcomes: + +- **success:** apply generated append/replace/remove/summary effects from the + server response +- **validation failure:** apply generated form error/focus effects +- **auth or CSRF failure:** discard or pause the queue and navigate to sign-in or + refresh the page +- **conflict:** ask the server for the current page/partial and replace a + generated target, or show a generated conflict notice +- **fingerprint mismatch:** reload/navigate instead of applying queued effects + +This keeps conflict policy in the app and keeps core runtime selectorless. req: failure/005 + +## Optional sync crate shape + +A future `hemx-sync` or app-local adapter may provide helpers around this model, +but it should remain optional and explicit: + +```rust +pub trait OfflineQueue { + async fn push(&self, command: OfflineCommand) -> Result<(), QueueError>; + async fn drain(&self, session: CurrentSession) -> Result<(), QueueError>; +} +``` + +Such an adapter may reuse generated slots, forms, and keyed resources, but it +must not make every app value a client-side atom or introduce a mandatory local +state graph. req: sync/001 req: sync/007 + +## Tests + +Keep tests at the adapter boundary: + +- offline command is stored as a domain command, not a raw effect +- queued command replays through the same handler route as an online submit +- server validation and CSRF checks still run during replay +- fingerprint/runtime mismatch causes reload/navigation instead of partial apply +- conflict response uses generated UI feedback or full page refresh +- no selector targeting or client app store is required for normal forms/lists + +For the current v1 tutorial, `examples/saas` remains the server-first canonical +path. Offline/PWA is an optional recipe, not required app scaffolding. req: examples/001 req: test/001 diff --git a/docs/tutorial-saas.md b/docs/tutorial-saas.md index 0a87dde..55d1aca 100644 --- a/docs/tutorial-saas.md +++ b/docs/tutorial-saas.md @@ -216,10 +216,12 @@ To move from the local tutorial skeleton to production: `docs/recipes/auth-session-csrf.md`. 3. Wrap routes/handlers with app-owned metrics, flags, and killswitches from `docs/recipes/observability-flags.md`. -4. Deploy server, generated output, and `/hemx.js` as one release unit following +4. Add optional PWA/offline behavior only through the adapter boundary in + `docs/recipes/pwa-offline.md`. +5. Deploy server, generated output, and `/hemx.js` as one release unit following `docs/recipes/deploy-versioning.md`. -5. Follow `docs/versioning.md` for semver and upgrade notes. -6. Use `docs/diagnostics.md` when a template/build/derive/runtime mistake fails +6. Follow `docs/versioning.md` for semver and upgrade notes. +7. Use `docs/diagnostics.md` when a template/build/derive/runtime mistake fails the app. The handler and template model should stay recognizable throughout those swaps. diff --git a/docs/v1-readiness.md b/docs/v1-readiness.md index cb716ca..7d413e0 100644 --- a/docs/v1-readiness.md +++ b/docs/v1-readiness.md @@ -107,7 +107,7 @@ Remaining before GOAL_DONE: ### Production recipes -Status: mostly satisfied; optional PWA/offline remains unresolved. +Status: satisfied pending final recipe scan. Evidence: @@ -116,12 +116,11 @@ Evidence: - observability/metrics + feature flags/killswitches: `docs/recipes/observability-flags.md` - deploy/versioning: `docs/recipes/deploy-versioning.md` +- optional PWA/offline: `docs/recipes/pwa-offline.md` Remaining before GOAL_DONE: -- Add a bounded optional PWA/offline recipe, or explicitly scope it out of v1 if - optional adapters are not required for the release. -- Confirm whether SQLx/auth/observability/deploy recipes are sufficient as +- Confirm whether SQLx/auth/observability/deploy/PWA recipes are sufficient as recipe-only production guidance or whether one provider path must be exercised by the tutorial crate before v1. @@ -182,6 +181,5 @@ implementation path is a bounded, checked provider slice for `examples/saas` CI; the smallest docs path is an explicit release decision that the local adapter plus provider recipes is the v1 production boundary. -After that, either add the optional PWA/offline recipe or explicitly scope it out -of v1, then run the closure audit with the full gates from the active goal, +After that, run the closure audit with the full gates from the active goal, including `cargo run -p hemx-xtask -- test` and the full diagnostics scan. diff --git a/examples/saas/README.md b/examples/saas/README.md index fff6200..698b04f 100644 --- a/examples/saas/README.md +++ b/examples/saas/README.md @@ -12,7 +12,7 @@ What it proves today: - page shell with plain CSS and one explicit metrics island script - compile-time surface generation plus interaction tests -For provider-explicit boundaries, see `../../docs/recipes/sqlx-persistence.md`, `../../docs/recipes/auth-session-csrf.md`, `../../docs/recipes/observability-flags.md`, and `../../docs/recipes/deploy-versioning.md`. +For provider-explicit boundaries, see `../../docs/recipes/sqlx-persistence.md`, `../../docs/recipes/auth-session-csrf.md`, `../../docs/recipes/observability-flags.md`, `../../docs/recipes/deploy-versioning.md`, and `../../docs/recipes/pwa-offline.md`. What it deliberately does not claim yet: