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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user