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
This commit is contained in:
slhx agent
2026-06-05 10:03:39 +02:00
parent 16cb15fd98
commit 78aff362ff
5 changed files with 143 additions and 11 deletions
+1 -1
View File
@@ -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`. 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, - **PWA/offline/sync:** optional adapters may reuse generated targets/effects,
but core hemx must not gain a mandatory client state graph or local app 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 ## Escape hatches
+132
View File
@@ -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
+5 -3
View File
@@ -216,10 +216,12 @@ To move from the local tutorial skeleton to production:
`docs/recipes/auth-session-csrf.md`. `docs/recipes/auth-session-csrf.md`.
3. Wrap routes/handlers with app-owned metrics, flags, and killswitches from 3. Wrap routes/handlers with app-owned metrics, flags, and killswitches from
`docs/recipes/observability-flags.md`. `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`. `docs/recipes/deploy-versioning.md`.
5. Follow `docs/versioning.md` for semver and upgrade notes. 6. Follow `docs/versioning.md` for semver and upgrade notes.
6. Use `docs/diagnostics.md` when a template/build/derive/runtime mistake fails 7. Use `docs/diagnostics.md` when a template/build/derive/runtime mistake fails
the app. the app.
The handler and template model should stay recognizable throughout those swaps. The handler and template model should stay recognizable throughout those swaps.
+4 -6
View File
@@ -107,7 +107,7 @@ Remaining before GOAL_DONE:
### Production recipes ### Production recipes
Status: mostly satisfied; optional PWA/offline remains unresolved. Status: satisfied pending final recipe scan.
Evidence: Evidence:
@@ -116,12 +116,11 @@ Evidence:
- observability/metrics + feature flags/killswitches: - observability/metrics + feature flags/killswitches:
`docs/recipes/observability-flags.md` `docs/recipes/observability-flags.md`
- deploy/versioning: `docs/recipes/deploy-versioning.md` - deploy/versioning: `docs/recipes/deploy-versioning.md`
- optional PWA/offline: `docs/recipes/pwa-offline.md`
Remaining before GOAL_DONE: Remaining before GOAL_DONE:
- Add a bounded optional PWA/offline recipe, or explicitly scope it out of v1 if - Confirm whether SQLx/auth/observability/deploy/PWA recipes are sufficient as
optional adapters are not required for the release.
- Confirm whether SQLx/auth/observability/deploy recipes are sufficient as
recipe-only production guidance or whether one provider path must be exercised recipe-only production guidance or whether one provider path must be exercised
by the tutorial crate before v1. 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 CI; the smallest docs path is an explicit release decision that the local adapter
plus provider recipes is the v1 production boundary. plus provider recipes is the v1 production boundary.
After that, either add the optional PWA/offline recipe or explicitly scope it out After that, run the closure audit with the full gates from the active goal,
of v1, then run the closure audit with the full gates from the active goal,
including `cargo run -p hemx-xtask -- test` and the full diagnostics scan. including `cargo run -p hemx-xtask -- test` and the full diagnostics scan.
+1 -1
View File
@@ -12,7 +12,7 @@ What it proves today:
- page shell with plain CSS and one explicit metrics island script - page shell with plain CSS and one explicit metrics island script
- compile-time surface generation plus interaction tests - 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: What it deliberately does not claim yet: