@jsenv/navi 0.29.225 → 0.29.227
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/README.md +2 -0
- package/dist/dev/jsenv_navi.js +168 -115
- package/dist/dev/jsenv_navi.js.map +13 -12
- package/dist/jsenv_navi.js +168 -115
- package/dist/jsenv_navi.js.map +13 -12
- package/docs/AI_INSTRUCTIONS.md +8 -6
- package/docs/dynamic_import.md +118 -69
- package/docs/interactions.md +7 -0
- package/docs/popup_open.md +31 -4
- package/package.json +1 -1
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -115,12 +115,14 @@ to start when unsure which export solves a problem.
|
|
|
115
115
|
- `offline.md` — `setNetworkPolicy`: answer from the store, ask nothing, refuse
|
|
116
116
|
writes politely. Read before caching responses in the app.
|
|
117
117
|
- `dynamic_import.md` — a screen's code arrives like its data: a page's
|
|
118
|
-
import is a `routeAction
|
|
119
|
-
|
|
120
|
-
`
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
118
|
+
import is a `routeAction` read with `loading: true` (a branch, not a
|
|
119
|
+
`<Loading>`), a component's is started by what asks — `actionRunEffect` on a
|
|
120
|
+
state, `useAsyncData(() => import(…))` only as the fallback;
|
|
121
|
+
links prerun on intent what asks nothing of the address; a transition
|
|
122
|
+
photographs the pending screen; a chunk's `import.meta.css`; a chunk that
|
|
123
|
+
does not come is never a bug and never comes twice in one document — offer
|
|
124
|
+
`reload()`; why not preact's `lazy`. Read before an `import()` in a component
|
|
125
|
+
or a page, and before `prefetch={false}`.
|
|
124
126
|
|
|
125
127
|
### Controls and forms
|
|
126
128
|
|
package/docs/dynamic_import.md
CHANGED
|
@@ -7,14 +7,18 @@ the network is gone or a deploy happened in between — and nothing on screen
|
|
|
7
7
|
should have to know whether what it waits for is bytes of data or bytes of
|
|
8
8
|
code.
|
|
9
9
|
|
|
10
|
-
So navi has nothing lazy-shaped: **the import is an action**,
|
|
11
|
-
|
|
10
|
+
So navi has nothing lazy-shaped: **the import is an action**, started by what
|
|
11
|
+
asks for it — the address through `routeAction`, a state through
|
|
12
|
+
`actionRunEffect` — and read by the hook a screen already reads its data
|
|
13
|
+
with. Running it from the hook itself is the fallback, as for data.
|
|
12
14
|
|
|
13
15
|
## A page: its code is a route action
|
|
14
16
|
|
|
15
17
|
The address asks for the page's data through a route action; it asks for the
|
|
16
18
|
page's code the same way, and the two are in flight together from the url
|
|
17
|
-
change
|
|
19
|
+
change. The page component in the entry reads the code like data — with
|
|
20
|
+
`loading: true` and `error: true`, drawing the page's own pending screen and
|
|
21
|
+
its own unavailable screen where the page will stand:
|
|
18
22
|
|
|
19
23
|
```jsx
|
|
20
24
|
const GAME_PAGE_ACTION = routeAction(GAME_ROUTE, GAME.GET, () => ({
|
|
@@ -25,75 +29,111 @@ const GAME_PAGE_CODE = routeAction(GAME_ROUTE, () =>
|
|
|
25
29
|
);
|
|
26
30
|
|
|
27
31
|
const GamePage = () => {
|
|
28
|
-
const [Page] = useAsyncData(GAME_PAGE_CODE
|
|
32
|
+
const [Page, loading, error] = useAsyncData(GAME_PAGE_CODE, {
|
|
33
|
+
loading: true,
|
|
34
|
+
error: true,
|
|
35
|
+
});
|
|
36
|
+
if (loading) {
|
|
37
|
+
return <GameScreenPending />;
|
|
38
|
+
}
|
|
39
|
+
if (error) {
|
|
40
|
+
return <GameScreenUnavailable />;
|
|
41
|
+
}
|
|
29
42
|
return <Page />;
|
|
30
43
|
};
|
|
31
44
|
|
|
32
|
-
<Route
|
|
33
|
-
<ErrorBoundary fallback={PageError}>
|
|
34
|
-
<Loading fallback={<PageSkeleton />}>
|
|
35
|
-
<Route route={GAME_ROUTE} element={GamePage} />
|
|
36
|
-
</Loading>
|
|
37
|
-
</ErrorBoundary>
|
|
38
|
-
</Route>;
|
|
45
|
+
<Route route={GAME_ROUTE} element={GamePage} />;
|
|
39
46
|
```
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
Why a branch rather than a `<Loading>` around it: suspending takes the
|
|
49
|
+
subtree away and puts the boundary's fallback in its place, then mounts the
|
|
50
|
+
page again — a cost worth paying when a page already on screen re-reads its
|
|
51
|
+
data (see [data_states.md](./data_states.md)), and worth nothing here, where
|
|
52
|
+
the page is not there yet and has nothing to keep. `loading: true` never
|
|
53
|
+
suspends: one component, no wrapper, the pending screen is the page's own
|
|
54
|
+
shape, and the failure sits beside it (what it offers: below, "When the code
|
|
55
|
+
does not come"). What is delegated stays delegated — the page's data, read inside
|
|
56
|
+
`game_page.jsx` with `useAsyncData(GAME_PAGE_ACTION)`, still waits in the
|
|
57
|
+
`<Loading>` written between the routes, as [navigation.md](./navigation.md)
|
|
58
|
+
says.
|
|
59
|
+
|
|
60
|
+
Everything the data layer decides then holds for the code:
|
|
61
|
+
|
|
62
|
+
- **One failure.** A rejected import fails the run like a failed read; drawn
|
|
63
|
+
by the branch above, or by the nearest `<ErrorBoundary>` when the page reads
|
|
64
|
+
its code without `error: true`.
|
|
50
65
|
- **Fetched once.** A route action left behind is aborted, not reset, and a
|
|
51
66
|
completed one is not run again: coming back to the page renders
|
|
52
67
|
synchronously, only the data is asked for per params.
|
|
53
68
|
- **Fetched ahead.** A route action that asks nothing of the address is prerun
|
|
54
69
|
on intent (below).
|
|
70
|
+
- **Counted.** The routing is busy until the code is there, like for the
|
|
71
|
+
data.
|
|
55
72
|
|
|
56
73
|
The page module itself is ordinary — it reads its data with
|
|
57
74
|
`useAsyncData(GAME_PAGE_ACTION)` like any page — and imports the routes module
|
|
58
75
|
back for it; a dynamic import is not a load-order cycle.
|
|
59
76
|
|
|
60
|
-
## A component inside a page that stays:
|
|
77
|
+
## A component inside a page that stays: who asks for its code
|
|
61
78
|
|
|
62
79
|
A plan drawn on demand inside a page that stays knows where it is drawn and
|
|
63
|
-
what stands there — a frame, a line, a button.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
80
|
+
what stands there — a frame, a line, a button. The wait and the failure are
|
|
81
|
+
drawn where the frame is, with `loading: true` and `error: true`, the same way
|
|
82
|
+
as for a page. What differs is **who starts the import**, and the order of
|
|
83
|
+
preference is the one `useAsyncData` states for data: **the run belongs to
|
|
84
|
+
what asks, not to the render.**
|
|
85
|
+
|
|
86
|
+
1. **The address asks** → `routeAction`, above. A section of a page that has
|
|
87
|
+
an address (`/places/:id/plan`) is a page.
|
|
88
|
+
2. **A state asks** → `actionRunEffect`. A plan drawn once a mode is entered,
|
|
89
|
+
a tab chosen, a popup opened: the signal that says so is what starts the
|
|
90
|
+
import, declared once at module scope — one action for every instance,
|
|
91
|
+
started by the state change rather than by the render that follows it,
|
|
92
|
+
and holding its answer across mounts:
|
|
93
|
+
|
|
94
|
+
```jsx
|
|
95
|
+
const PLAN_CODE = actionRunEffect(
|
|
96
|
+
() => import("./plan.jsx").then((m) => m.Plan),
|
|
97
|
+
() => planShownSignal.value,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const PlanSection = () => {
|
|
101
|
+
const [Plan, loading, error] = useAsyncData(PLAN_CODE, {
|
|
102
|
+
loading: true,
|
|
103
|
+
error: true,
|
|
104
|
+
});
|
|
105
|
+
return (
|
|
106
|
+
<PlanFrame>
|
|
107
|
+
{loading ? <Text>…</Text> : null}
|
|
108
|
+
{error ? <Button action={() => reload()}>Recharger</Button> : null}
|
|
109
|
+
{Plan ? <Plan /> : null}
|
|
110
|
+
</PlanFrame>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
3. **Nothing above the component asks** → `useAsyncData` with a function, the
|
|
116
|
+
fallback: the component's own existence is the only thing that asks, so the
|
|
117
|
+
request is its own, made into an action once on the first render and kept
|
|
118
|
+
for the life of the instance.
|
|
119
|
+
|
|
120
|
+
```jsx
|
|
121
|
+
const [Plan, loading, error] = useAsyncData(
|
|
122
|
+
() => import("./plan.jsx").then((m) => m.Plan),
|
|
123
|
+
{ loading: true, error: true },
|
|
124
|
+
);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
It starts one render late, and per instance: a component fetched this way
|
|
128
|
+
in three places asks three times (the browser fetches the module once), and
|
|
129
|
+
coming back to it suspends for the microtask the import takes to answer
|
|
130
|
+
from the module map. Reach for it when there is genuinely no state to read
|
|
131
|
+
the request off — not to save declaring one.
|
|
132
|
+
|
|
133
|
+
Delegating instead (the hook without `loading: true`, under a `<Loading>`
|
|
134
|
+
around the frame) follows the rule for data: **the `<Loading>` goes where the
|
|
135
|
+
fallback should land.** A boundary around the whole router takes the router
|
|
136
|
+
away while a page loads — top bar, tabs and all.
|
|
97
137
|
|
|
98
138
|
Nothing has to be kept out of a subtree loaded this way: the suspension
|
|
99
139
|
happens before the module's components exist, so nothing inside it is parked
|
|
@@ -111,8 +151,9 @@ prefetched: a prefetch has no address, the id may still be chosen.
|
|
|
111
151
|
`routeAction(route, action, undefined, { prefetch: false })` keeps a
|
|
112
152
|
param-less read that is not worth a hover out of it.
|
|
113
153
|
|
|
114
|
-
A prefetch that fails is
|
|
115
|
-
|
|
154
|
+
A prefetch that fails is not reported: nothing on screen asked for it. The
|
|
155
|
+
failure stays on the action, where the arrival asks again — a failed action is
|
|
156
|
+
run, only a running or completed one is left alone — and shows its own.
|
|
116
157
|
|
|
117
158
|
The same from code: `route.preload()` for a route something is about to
|
|
118
159
|
navigate to, `preloadUrl(url)` for an address. A press that computes its
|
|
@@ -120,9 +161,10 @@ destination has nothing to preload — the navigation itself asks.
|
|
|
120
161
|
|
|
121
162
|
## What a transition photographs
|
|
122
163
|
|
|
123
|
-
A route element still waiting is
|
|
124
|
-
|
|
125
|
-
|
|
164
|
+
A route element still waiting is what the container rendered — its pending
|
|
165
|
+
screen, or the fallback of the boundary it suspended into — so a route
|
|
166
|
+
transition or a `RouteTravel` takes its picture of **that**, code and data
|
|
167
|
+
alike. Navi does not hold a navigation for a chunk: the honest answer
|
|
126
168
|
to "a chunk is a hundred milliseconds" is the prefetch above — the code is
|
|
127
169
|
there before the press — and a skeleton that is the page's own shape is what
|
|
128
170
|
gets photographed when it is not.
|
|
@@ -154,15 +196,22 @@ is a 404; or the network is gone. The browser says both the same way — a
|
|
|
154
196
|
`TypeError` with no status — and neither is a bug of the page: there is
|
|
155
197
|
nothing in the app's code to point at.
|
|
156
198
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
screen says it.
|
|
199
|
+
**Asking again never fetches.** The document remembers a module whose fetch
|
|
200
|
+
failed as failed: a second `import()` of the same address is refused without a
|
|
201
|
+
request, for the life of the document. So a retry on an import has nothing to
|
|
202
|
+
do — `error.action.rerun()` fails again at once, on the same failure — and
|
|
203
|
+
the one way out is a fresh document: **`reload()`**, which the screen offers
|
|
204
|
+
in words the reader understands. Navi does not reload on its own: a document
|
|
205
|
+
reloading itself is a loop waiting to happen. It is also why a prefetch that
|
|
206
|
+
failed is not asked again on the arrival in any useful sense: the run is made,
|
|
207
|
+
refused at once, and the screen says it.
|
|
208
|
+
|
|
209
|
+
The page or the component reading its code says it where it stands, as
|
|
210
|
+
above. A failure left to a boundary — a page reading its code without
|
|
211
|
+
`error: true` — reaches it like a failed read does; a boundary that shows only
|
|
212
|
+
what came from the network and rethrows the rest as a bug recognises an import
|
|
213
|
+
by its action — `error.action` is the route action the app declared for the
|
|
214
|
+
code — rather than by the browser's message.
|
|
166
215
|
|
|
167
216
|
## Preact's `lazy()` is not this
|
|
168
217
|
|
package/docs/interactions.md
CHANGED
|
@@ -325,6 +325,13 @@ being sent: everything — the popup is the only place its failure can be read,
|
|
|
325
325
|
and `actionStandalone` there is how a save fails behind a closed popup. Something
|
|
326
326
|
the app watches from somewhere else: nothing — it was never being watched here.
|
|
327
327
|
|
|
328
|
+
And when the answer being sent may never arrive at all — a request over a
|
|
329
|
+
network that stopped answering — the hold outlives what it was protecting:
|
|
330
|
+
nothing can close the popup, ever. `actionAbortable` keeps the hold and gives it
|
|
331
|
+
a release, so closing calls the run off rather than being refused. What that
|
|
332
|
+
costs, and where it may be said, is
|
|
333
|
+
[popup_open.md](./popup_open.md#the-popup-owns-its-open-state).
|
|
334
|
+
|
|
328
335
|
### On something you draw yourself
|
|
329
336
|
|
|
330
337
|
`selfInteractions` is a `Box` prop too, so an affordance does not have to become
|
package/docs/popup_open.md
CHANGED
|
@@ -29,10 +29,10 @@ something a `useState` in the parent cannot give back: **a popup refuses to
|
|
|
29
29
|
close while a control inside it is mid-action**.
|
|
30
30
|
|
|
31
31
|
```js
|
|
32
|
-
// what both do on every close request
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
35
|
-
dispatchRequestInteraction(
|
|
32
|
+
// what both do on every close request (popup_busy.js)
|
|
33
|
+
const controlsHolding = findControlsHoldingPopup(popupEl);
|
|
34
|
+
if (controlsHolding.length) {
|
|
35
|
+
dispatchRequestInteraction(controlsHolding[0].element, { ... });
|
|
36
36
|
requestCloseEvent.preventDefault();
|
|
37
37
|
}
|
|
38
38
|
```
|
|
@@ -58,6 +58,33 @@ It is still busy for itself — the spinner, the second press refused, the error
|
|
|
58
58
|
callout — which is the whole point of keeping `action` (see
|
|
59
59
|
[interactions.md](./interactions.md#the-fourth-question-whose-wait-is-it)).
|
|
60
60
|
|
|
61
|
+
Between the two sits a run that IS being waited on and may never end. A request
|
|
62
|
+
over a network that stopped answering settles neither way, and the hold then
|
|
63
|
+
lasts exactly as long as the run: the popup closes for nothing and nobody, and
|
|
64
|
+
the page has to be reloaded. `actionAbortable` gives that hold a release — the
|
|
65
|
+
person waiting decides the answer is not coming, and closing is how they say
|
|
66
|
+
it:
|
|
67
|
+
|
|
68
|
+
```jsx
|
|
69
|
+
<Form action={saveScore} actionAbortable>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Every close request then goes through and calls the run off on the way out:
|
|
73
|
+
Escape, the backdrop, the cross, the phone's back gesture. No gesture is singled
|
|
74
|
+
out, because the cancel/keep distinction below is about the VALUE a popup holds,
|
|
75
|
+
and there is nothing left to keep or put back once the answer is already on the
|
|
76
|
+
wire — each of those gestures means "I want out of here", which is the only
|
|
77
|
+
thing being answered.
|
|
78
|
+
|
|
79
|
+
What it costs is written down: aborting frees the client and nothing more, so
|
|
80
|
+
the write may have landed anyway (see
|
|
81
|
+
[actions.md](./actions.md#aborting-saves-resources-it-does-not-undo)). Say it
|
|
82
|
+
where the screen can be re-opened on what is actually there, never where the
|
|
83
|
+
popup is the only place the outcome could be read. Everything inside the form
|
|
84
|
+
inherits it along with the wait itself; a popup holding one run that may be
|
|
85
|
+
given up on and one that may not still refuses, since calling off half of them
|
|
86
|
+
would cost an answer and change nothing on screen.
|
|
87
|
+
|
|
61
88
|
So the question is never "should this popup be controlled?" but "what triggers
|
|
62
89
|
the opening?" — and, when the answer is the application rather than a gesture,
|
|
63
90
|
where that state lives:
|