@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.
- package/dist/jsenv_navi.js +5930 -5641
- package/dist/jsenv_navi.js.map +23 -16
- package/docs/AI_INSTRUCTIONS.md +8 -0
- package/docs/actions.md +3 -2
- package/docs/offline.md +119 -0
- package/docs/resource.md +2 -0
- package/package.json +1 -1
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -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 })`.
|
|
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).
|
package/docs/offline.md
ADDED
|
@@ -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
|