@jsenv/navi 0.29.126 → 0.29.128

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -88,6 +88,14 @@ consistency across the app, not from any single call site.
88
88
  before displaying an error by hand, before writing an error boundary of your
89
89
  own, and before concluding that a dev overlay over a page that already shows
90
90
  its error is a crash.
91
+ - `docs/offline.md` — an app that keeps working without the network: what the
92
+ store and the actions already hold when a page is left and come back to,
93
+ the one declaration (`setNetworkPolicy`) that keeps requests from going out
94
+ — a GET answered from the store, a completed read kept as it is, a write
95
+ refused before the press with a message — and what stays the app's (deciding
96
+ the reason, persisting the store, replaying writes). Read it before caching
97
+ responses in the app, before wrapping a resource's callbacks to answer
98
+ offline, and before marking write buttons read-only one by one.
91
99
  - `docs/field_validation.md` — what a control refuses and who decides it: the
92
100
  split between what only a browser can answer (a blocked keystroke, where the
93
101
  callout lands, when the message appears) and « is this value acceptable »,
package/docs/actions.md CHANGED
@@ -13,8 +13,9 @@ const getUser = createAction(async ({ id }, { signal }) => {
13
13
  });
14
14
  ```
15
15
 
16
- The callback receives `(params, { reason, event, signal, isPrerun })`. `signal`
17
- is aborted when the run is called off — pass it to `fetch`.
16
+ The callback receives `(params, { reason, event, signal, isPrerun, action })`.
17
+ `signal` is aborted when the run is called off — pass it to `fetch`. `action` is
18
+ the instance being run.
18
19
 
19
20
  `resource()` creates one action per REST callback rather than having you write
20
21
  them by hand — see [resource.md](./resource.md).
@@ -0,0 +1,119 @@
1
+ # An app that works offline
2
+
3
+ What an app wants when the network is gone is one sentence: **answer from what
4
+ you already hold, ask nothing, and refuse writes politely.** Nothing in it is
5
+ about one app; navi does it once, and an app only has to say _when_.
6
+
7
+ ## What navi keeps without being asked
8
+
9
+ Leaving a page erases nothing. Four facts of the action layer add up to a
10
+ cache the app never has to write:
11
+
12
+ - **The data of a resource action is the store row.** A `GET` completes with an
13
+ id, and `action.data` is `store.select(id)`, live. Whatever was read once is
14
+ still there, updated by every later response that mentions it.
15
+ - **A route action left behind is aborted, not reset.** Its value survives; only
16
+ a run in flight is called off.
17
+ - **Running a completed action is a no-op.** Coming back to `/games/abc` from
18
+ the list sends nothing — `run()` on a `COMPLETED` action already has its data.
19
+ Only `rerun()` goes back to the network (see [actions.md](./actions.md)).
20
+ - **A failed rerun keeps the previous value.** Only `errorSignal` and the
21
+ running state move; what was on screen stays on screen.
22
+
23
+ So an app that caches responses in a `Map` beside navi is keeping a second copy
24
+ of what the store holds — and going stale on its own.
25
+
26
+ ## The one thing the app decides: the reason
27
+
28
+ Which moments count as "offline" is the app's. A device without a network
29
+ interface, a mode chosen in the settings, an outage the user agreed to stop
30
+ fighting — these are not said the same way to the user, and an app may have
31
+ none, one or all of them. Navi asks for a single value, the **reason**, and
32
+ reads it everywhere it matters:
33
+
34
+ ```js
35
+ import { setNetworkPolicy } from "@jsenv/navi";
36
+
37
+ // null → go to the network; any truthy value → do not, and carry it
38
+ setNetworkPolicy(offlineReasonSignal, {
39
+ readOnlyMessage: (reason) =>
40
+ reason === "device"
41
+ ? "No network: this cannot be sent."
42
+ : "Offline mode: this cannot be sent.",
43
+ });
44
+ ```
45
+
46
+ The source may be a signal (followed live), a function, or a plain value. Set
47
+ it once, at startup; there is nothing else to wire.
48
+
49
+ ## What the policy changes
50
+
51
+ Under a truthy reason, no resource callback is called. What happens instead
52
+ depends on what was asked:
53
+
54
+ - **A `GET` answers from the store.** If the row it designates is there, the
55
+ action completes with it and nothing is asked. The row is the one its params
56
+ name — by the resource's `idKey`, or by any of its `uniqueKeys` — or, when
57
+ the params name none (a `GET` without params, like `/me`), the row the
58
+ action last completed with. Going from game A to game B, both read
59
+ before, is a change of params that would normally rerun `GAME.GET`; under the
60
+ policy it completes from the store, and a screen that does not take
61
+ `error: true` keeps drawing what it holds instead of falling into its error
62
+ boundary. A route opening a user by id **or** by slug gives navi both keys to
63
+ look by: `resource("user", { uniqueKeys: ["slug"], … })`.
64
+ - **A completed read stays completed.** `GET_MANY` (and every other read) has
65
+ nowhere to answer from: the store holds items, not queries, and only the
66
+ action's own value knows which ids answered `/users?scope=shareable`. So a
67
+ rerun asked of a completed read under the policy is held — the action keeps
68
+ its state, its value and its data, exactly as a `run()` on a completed action
69
+ would. This is the piece an app cannot do on its own: by the time a callback
70
+ runs, its action has already been reset.
71
+ - **A `GET_RANGE` revalidation fails quietly.** A list that comes back to the
72
+ screen draws the composition it left and asks again for the window it draws;
73
+ under the policy that ask fails, and a failed revalidation keeps the rows it
74
+ had. Only a window never loaded shows the failure.
75
+ - **Everything else settles with an `OfflineError`.** A read with nothing in
76
+ the store, a relationship `GET` (it has no row of its own to answer with), a
77
+ write that got through anyway: the action ends `FAILED` with an error whose
78
+ `reason` is the policy's value. `isOfflineError(error)` tells it apart from a
79
+ request that left and never came back — that one is the app's `fetch`
80
+ rejecting, and the app names it.
81
+ - **A write is refused before the press.** A control bound to a `POST`, `PUT`,
82
+ `PATCH` or `DELETE` action — or any control inside a `<Form>` bound to one —
83
+ is read-only while the policy holds, and answers the press with
84
+ `readOnlyMessage`. Nothing to add per button.
85
+
86
+ Which actions the policy sees: those declaring a verb (`meta.verb`) — every
87
+ action a `resource()` makes. A plain `createAction` may not touch the network
88
+ at all, so it is left alone; give it `meta: { verb: "GET" }` to opt in.
89
+
90
+ ## What stays the app's
91
+
92
+ - **A button that only leads to a write** — one opening a dialog whose form
93
+ will send — is bound to no write action, so navi cannot know. The app marks
94
+ it, with the reason it reads live:
95
+
96
+ ```jsx
97
+ const reason = useNetworkPolicyReason();
98
+ <Button
99
+ command="--navi-open"
100
+ readOnly={reason !== null}
101
+ readOnlyMessage="…"
102
+ />;
103
+ ```
104
+
105
+ - **What a screen says on an `OfflineError`** — "no network" is a fact about
106
+ the device, "offline mode" a decision; `error.reason` is there so the screen
107
+ says the right one.
108
+ - **Persisting the store to disk**, so that a reload offline reopens a full
109
+ app rather than an empty one.
110
+ - **A queue of writes to replay** once the network is back. Deliberately not
111
+ navi's: what a replayed write means (a score entered twice? a seat taken
112
+ since?) is the app's business.
113
+
114
+ ## See also
115
+
116
+ - [resource.md](./resource.md) — the store, the callbacks and their contracts
117
+ - [actions.md](./actions.md) — `run` vs `rerun`, what a failing action does
118
+ - [error_handling.md](./error_handling.md) — where an error appears depending
119
+ on where it came from
package/docs/resource.md CHANGED
@@ -361,3 +361,5 @@ an action is callable: `GAME.DELETE({ id })` is
361
361
  - [list_refresh.md](./list_refresh.md) — what re-runs after a write, and what
362
362
  stays on screen while it does
363
363
  - [actions.md](./actions.md) — action lifecycle, `bindParams`, `useAsyncData`
364
+ - [offline.md](./offline.md) — what the store already keeps when the network
365
+ is gone, and the policy that keeps requests from going out
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.126",
3
+ "version": "0.29.128",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {