@jsenv/navi 0.29.392 → 0.29.394
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/dev/jsenv_navi.js +442 -65
- package/dist/dev/jsenv_navi.js.map +11 -9
- package/dist/jsenv_navi.js +393 -36
- package/dist/jsenv_navi.js.map +11 -9
- package/docs/AI_INSTRUCTIONS.md +4 -2
- package/docs/data_states.md +4 -1
- package/docs/navigation.md +41 -0
- package/docs/network_policy.md +5 -2
- package/docs/resource.md +95 -1
- package/package.json +1 -1
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -98,8 +98,10 @@ to start when unsure which export solves a problem.
|
|
|
98
98
|
action from a component, and before putting a delay on params.
|
|
99
99
|
- `resource.md` — REST state: `resource()`, the callback contract of each
|
|
100
100
|
relationship method (not guessable), `GET_RANGE`, a search as the same
|
|
101
|
-
`GET_MANY`, `withParams()` scopes and `dependencies
|
|
102
|
-
|
|
101
|
+
`GET_MANY`, `withParams()` scopes and `dependencies`, `persist` (the last
|
|
102
|
+
`GET` answer drawn again after a reload). Read before writing a resource, and
|
|
103
|
+
before caching a response in localStorage; never encode a sub-route as an
|
|
104
|
+
`op` discriminator.
|
|
103
105
|
- `data_states.md` — `data`, `loading` and `error` are three questions; the
|
|
104
106
|
four combinations of the first two; `loading: true` never suspends; a skeleton
|
|
105
107
|
is told whether it is loading. Read before drawing a skeleton on `!data` or
|
package/docs/data_states.md
CHANGED
|
@@ -82,7 +82,10 @@ whose params name a row the store already holds — read earlier by a `GET_MANY`
|
|
|
82
82
|
a relation, a list — starts on the second line, not the first: the row is drawn
|
|
83
83
|
and the request goes out behind it. The response replaces it, and a failure
|
|
84
84
|
lands beside it like any refresh failure. A `GET` of a row the store lacks
|
|
85
|
-
still starts on the first line, so what is drawn is never a different row.
|
|
85
|
+
still starts on the first line, so what is drawn is never a different row. A
|
|
86
|
+
row known from the previous page load counts too, when the resource asks for
|
|
87
|
+
it: [`persist`](./resource.md#persist-the-last-answer-drawn-again-after-a-reload)
|
|
88
|
+
puts a reload on the second line as well.
|
|
86
89
|
|
|
87
90
|
**The emptiness test is `data === undefined`, never `loading`.** Reading
|
|
88
91
|
`loading` as "there is nothing to display" blanks the screen on every refresh —
|
package/docs/navigation.md
CHANGED
|
@@ -431,6 +431,47 @@ param that qualifies a screen is not a place, and one entry per write turns a
|
|
|
431
431
|
single back-press into as many as the user moved. A state whose values ARE
|
|
432
432
|
places says so — see [`history: "push"`](#a-state-whose-values-are-places-history-push).
|
|
433
433
|
|
|
434
|
+
#### A value written per frame
|
|
435
|
+
|
|
436
|
+
Every write of the signal reaches the address, synchronously, and browsers
|
|
437
|
+
refuse an address written too often: Safari throws a `SecurityError` past 100
|
|
438
|
+
writes per 10 seconds, and the state then runs ahead of an address that stopped
|
|
439
|
+
following. A value dragged at 60 Hz gets there in under two seconds. Chromium
|
|
440
|
+
drops the writes silently, so a developer working there never sees it; navi
|
|
441
|
+
warns in dev when the budget is reached.
|
|
442
|
+
|
|
443
|
+
**An address is not a recording of a gesture.** What the address records is
|
|
444
|
+
where the value was PUT DOWN, not the path the finger took. So the value one
|
|
445
|
+
drags belongs to the gesture, and the state bound to the url is written when the
|
|
446
|
+
gesture ends:
|
|
447
|
+
|
|
448
|
+
- **A gesture with a release** — a disc turned, a slider dragged: hold the value
|
|
449
|
+
in the component while the pointer is down, and write the signal on release.
|
|
450
|
+
Re-reading the signal meanwhile would pull the control back mid-gesture, so
|
|
451
|
+
the held value is the one drawn.
|
|
452
|
+
- **A movement with no release** — a wheel zooming, a fly-to animation: write
|
|
453
|
+
the signal when the movement has settled (a short idle timer, or the end of
|
|
454
|
+
the animation).
|
|
455
|
+
|
|
456
|
+
When every intermediate value IS a valid address — the centre of a panned map,
|
|
457
|
+
where each frame is a place one could reload into — the state can ask navi to
|
|
458
|
+
debounce the browser's address instead, in milliseconds:
|
|
459
|
+
|
|
460
|
+
```js
|
|
461
|
+
const eastSignal = stateSignal(0, {
|
|
462
|
+
id: "e",
|
|
463
|
+
type: "number",
|
|
464
|
+
debounceUrl: 200,
|
|
465
|
+
});
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
The routes, the document url and the renders still follow every write; only the
|
|
469
|
+
browser's copy of the address (`window.location`) waits for the state to stay
|
|
470
|
+
still that long, and is then written with the last value. A push is never
|
|
471
|
+
debounced, and a push or a state written onto the entry flushes what is pending
|
|
472
|
+
first. A back pressed before the write keeps the entry at the last address
|
|
473
|
+
written: the drag in progress, and nothing else.
|
|
474
|
+
|
|
434
475
|
## Rendering routes
|
|
435
476
|
|
|
436
477
|
`<Route>` is the only primitive. With `children` it is a container that renders
|
package/docs/network_policy.md
CHANGED
|
@@ -195,8 +195,11 @@ one says why is something navi has nothing to decide it with.
|
|
|
195
195
|
- **What a screen says on a `NetworkPolicyError`** — "no network" is a fact
|
|
196
196
|
about the device, "offline mode" a decision, "we are only looking" neither;
|
|
197
197
|
`error.reason` is there so the screen says the right one.
|
|
198
|
-
- **
|
|
199
|
-
|
|
198
|
+
- **Which answer is worth keeping across a reload.** A resource declaring
|
|
199
|
+
[`persist`](./resource.md#persist-the-last-answer-drawn-again-after-a-reload)
|
|
200
|
+
has its last `GET` answer back in the store at the first run, and under the
|
|
201
|
+
policy that row answers the `GET`: a reload offline reopens on it. Which
|
|
202
|
+
resource, into which signal, and for which session, is the app's to say.
|
|
200
203
|
- **A queue of writes to replay** once the network is back. Deliberately not
|
|
201
204
|
navi's: what a replayed write means (a score entered twice? a seat taken
|
|
202
205
|
since?) is the app's business.
|
package/docs/resource.md
CHANGED
|
@@ -34,6 +34,7 @@ Actions are read in components through the action system (`useAsyncData`,
|
|
|
34
34
|
looks like ([data_states.md](./data_states.md#data-and-loading-are-independent)).
|
|
35
35
|
|
|
36
36
|
- [`store.upsert()` is not how data enters the store](#storeupsert-is-not-how-data-enters-the-store)
|
|
37
|
+
- [`persist`: the last answer, drawn again after a reload](#persist-the-last-answer-drawn-again-after-a-reload)
|
|
37
38
|
- [`GET_RANGE`: feeding a list that loads as it scrolls](#get_range-feeding-a-list-that-loads-as-it-scrolls)
|
|
38
39
|
- [Searching the same collection](#searching-the-same-collection)
|
|
39
40
|
- [Relations: pick one of the four methods](#relations-pick-one-of-the-four-methods)
|
|
@@ -62,7 +63,8 @@ that does not return what it should.
|
|
|
62
63
|
it yourself is the point:
|
|
63
64
|
|
|
64
65
|
- seeding it with data that came from elsewhere — state rendered by the server,
|
|
65
|
-
a
|
|
66
|
+
a websocket message (the resource's own last answer is not "elsewhere": that
|
|
67
|
+
is [`persist`](#persist-the-last-answer-drawn-again-after-a-reload), below);
|
|
66
68
|
- absorbing the parent object a relation route answered with: the relation
|
|
67
69
|
callback only writes the relation, so the parent's own fields have nowhere
|
|
68
70
|
else to land (see [When the backend answers a sub-route with the whole
|
|
@@ -71,6 +73,98 @@ it yourself is the point:
|
|
|
71
73
|
A list that loads its rows a slice at a time is **not** one of those cases — that is
|
|
72
74
|
`GET_RANGE`, right below.
|
|
73
75
|
|
|
76
|
+
## `persist`: the last answer, drawn again after a reload
|
|
77
|
+
|
|
78
|
+
Some `GET`s are the app. `GET /me` answers who is signed in, their things, what
|
|
79
|
+
the header and the tab bar draw; until it lands, a page that is rendered has
|
|
80
|
+
nothing to draw. On a reload, or when the app is reopened from the home screen,
|
|
81
|
+
that wait is pure loss: the previous answer was on screen a moment ago and is
|
|
82
|
+
almost always still true, and a server waking up makes it last seconds.
|
|
83
|
+
|
|
84
|
+
What is wanted is the second line of the table in
|
|
85
|
+
[data_states.md](./data_states.md#data-and-loading-are-independent) reached on
|
|
86
|
+
a reload: the last known row drawn, the request out behind it, the fresh answer
|
|
87
|
+
replacing it. `persist` names the resource whose answer is worth keeping, and
|
|
88
|
+
the signal that keeps it — a `stateSignal` that persists, the primitive navi
|
|
89
|
+
has for a state that survives a reload:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
const mePersistedSignal = stateSignal(undefined, {
|
|
93
|
+
id: `me@${APP_VERSION}`,
|
|
94
|
+
persists: true,
|
|
95
|
+
type: "object",
|
|
96
|
+
});
|
|
97
|
+
const ME = resource("me", {
|
|
98
|
+
persist: {
|
|
99
|
+
signal: mePersistedSignal,
|
|
100
|
+
when: () => !viewAsSignal.value,
|
|
101
|
+
},
|
|
102
|
+
GET: () => fetchJson(`/me`),
|
|
103
|
+
});
|
|
104
|
+
ME.one("user", USER);
|
|
105
|
+
ME.many("my_games", GAME);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
From there navi does the rest:
|
|
109
|
+
|
|
110
|
+
- **the rows `GET` lands with are written into the signal** — one per params
|
|
111
|
+
the `GET` was asked with, so a `GET` without params keeps one row. Where the
|
|
112
|
+
signal keeps its value is the signal's business: `persists: true` is
|
|
113
|
+
localStorage, and a plain `signal()` keeps it for the page's lifetime, which
|
|
114
|
+
is what a test wants;
|
|
115
|
+
- **the first run of the `GET` draws the kept row.** It enters the store and
|
|
116
|
+
becomes the action's provisional value: `data` is set while `loading` is
|
|
117
|
+
`true`, the row is on screen and the request is out behind it. Nothing else
|
|
118
|
+
moves — `runningState` is `running`, the answer replaces the row, an error
|
|
119
|
+
lands in `errorSignal` beside it;
|
|
120
|
+
- **the copy follows the store.** It is written with its relations inline — the
|
|
121
|
+
shape the `GET` callback returns, so it re-enters through the same setters a
|
|
122
|
+
real answer goes through — and rewritten whenever the row or a row it reaches
|
|
123
|
+
through a relation changes: a `PUT` on the item, a list upserting one of its
|
|
124
|
+
children, a game joining `my_games`. A rewrite that changes nothing is
|
|
125
|
+
skipped, so the signal moves only when the copy does;
|
|
126
|
+
- **under a network policy answering reads from the store, the kept row answers
|
|
127
|
+
the `GET`** ([network_policy.md](./network_policy.md)): a reload with no
|
|
128
|
+
network reopens on the last answer rather than on nothing.
|
|
129
|
+
|
|
130
|
+
What stays the app's, because only the app knows:
|
|
131
|
+
|
|
132
|
+
- **the key.** A stored copy is only as good as the code reading it: a deploy
|
|
133
|
+
that changes the shape of the answer must not draw the old one. Put in the
|
|
134
|
+
signal's `id` whatever makes a copy unusable — the deployed version — and a
|
|
135
|
+
new id starts clean.
|
|
136
|
+
- **`when`: is the copy about this session?** A tab viewing as someone else, an
|
|
137
|
+
account being switched — the row in the store is somebody's, and it must not
|
|
138
|
+
be kept for the next person. `when` is read whenever the copy is about to be
|
|
139
|
+
read or written: `false` reads nothing, writes nothing, and empties the
|
|
140
|
+
signal. It is a function so it can read a signal, and it has to: a plain
|
|
141
|
+
variable is only looked at again when the store changes.
|
|
142
|
+
- **the moment, not the state.** Sign-out is not a condition that holds, it is
|
|
143
|
+
something that happens: writing `undefined` into the signal
|
|
144
|
+
(`mePersistedSignal.value = undefined`) forgets the copy at once, and nothing
|
|
145
|
+
is written until a `GET` lands again. The store rows stay — the screen keeps
|
|
146
|
+
what it shows until the app resets the action, as it does today.
|
|
147
|
+
- **what the `GET` feeds beside its return value.** Counts, badges, a feature
|
|
148
|
+
flag set from the response are the app's signals, not the row; they are not
|
|
149
|
+
kept, and the callback does not run on a kept row. Anything the app wants
|
|
150
|
+
back before the answer, it stores itself.
|
|
151
|
+
- **the veil.** A loading screen that waits on the `GET` being `running` stays
|
|
152
|
+
over the drawn row for the whole request. It waits on `data` being set —
|
|
153
|
+
the emptiness test of data_states.md — and the refresh is shown as usual.
|
|
154
|
+
|
|
155
|
+
What the copy is not:
|
|
156
|
+
|
|
157
|
+
- **an answer.** The `GET` goes out every time; the copy is a drawing aid. A
|
|
158
|
+
screen must not read `data` as "the server confirmed": that is `loading`,
|
|
159
|
+
and the two are independent.
|
|
160
|
+
- **a store on disk.** One row per params key, `GET` only — a resource whose
|
|
161
|
+
`GET` is asked for every id would keep every id. It is meant for a singleton
|
|
162
|
+
or a handful of rows.
|
|
163
|
+
- **seeded at declaration.** The row enters the store at the first run of the
|
|
164
|
+
`GET`, once the relations are declared, so that its relation values are
|
|
165
|
+
normalized into their stores as a real answer's are. `ME.store` read before
|
|
166
|
+
that first run is empty.
|
|
167
|
+
|
|
74
168
|
## `GET_RANGE`: feeding a list that loads as it scrolls
|
|
75
169
|
|
|
76
170
|
A `<List.Items>` asks for the rows it is about to draw and keeps what it gets.
|