@jsenv/navi 0.30.24 → 0.30.26
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 +194 -24
- package/dist/dev/jsenv_navi.js.map +5 -5
- package/dist/jsenv_navi.js +194 -24
- package/dist/jsenv_navi.js.map +5 -5
- package/docs/AI_INSTRUCTIONS.md +5 -1
- package/docs/actions.md +4 -0
- package/docs/dynamic_import.md +18 -0
- package/docs/resource.md +119 -10
- package/package.json +1 -1
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -89,7 +89,11 @@ start when unsure which export solves a problem.
|
|
|
89
89
|
(`{ run: true }` is the fallback), `action` versus `uiAction`, an action and a
|
|
90
90
|
command on one press. Before running an action from a component.
|
|
91
91
|
- `resource.md` — `resource()` and its relations (never an `op` discriminator),
|
|
92
|
-
|
|
92
|
+
a row pairing an entity with fields of its own (the entity under `.one()`,
|
|
93
|
+
never spread in), what a copy of a relation keeps, what a parent shows once
|
|
94
|
+
its `.one()` child is deleted (`onDelete: "cascade"` for a parent that is
|
|
95
|
+
nothing without it), what each read shows while
|
|
96
|
+
its params move, `GET_RANGE` (its inclusive `end`,
|
|
93
97
|
the count for the screen around the list, a question following a search
|
|
94
98
|
field), a search as the same `GET_MANY`, `withParams()` scopes and
|
|
95
99
|
`dependencies`, `persist` (and `keepPageOnScreen` for the page on screen), a
|
package/docs/actions.md
CHANGED
|
@@ -229,6 +229,10 @@ delay to hide it:
|
|
|
229
229
|
|
|
230
230
|
A "check now" wired to `run()` therefore checks nothing, silently.
|
|
231
231
|
|
|
232
|
+
`run()`, `rerun()` and `prerun()` answer the way the callback does: a promise when it is asynchronous, even
|
|
233
|
+
when `run()` joins an answer the instance already holds, so `ACTION.run().catch(…)`
|
|
234
|
+
holds in every state; the data itself, synchronously, when it is synchronous.
|
|
235
|
+
|
|
232
236
|
### Aborting saves resources, it does not undo
|
|
233
237
|
|
|
234
238
|
`abort()` cancels what can still be cancelled — a `fetch` wired to the
|
package/docs/dynamic_import.md
CHANGED
|
@@ -112,6 +112,24 @@ render.**
|
|
|
112
112
|
};
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
+
The binding `actionRunEffect` returns holds the state's question: while the
|
|
116
|
+
state says no, it has nothing to ask, and a reader on it waits forever. That
|
|
117
|
+
is right when the state is the only thing that asks. When something else asks
|
|
118
|
+
too (a reader with `run: true` that does not follow that state, the code of a
|
|
119
|
+
section every profile with cups draws, asked ahead because mine has some),
|
|
120
|
+
the effect runs the action and the readers read the question that always
|
|
121
|
+
stands, `true` for a param-less action. Both are the same instance, so the
|
|
122
|
+
code is fetched once:
|
|
123
|
+
|
|
124
|
+
```jsx
|
|
125
|
+
const PALMARES_CODE_ACTION = createAction(() =>
|
|
126
|
+
import("./palmares.jsx").then((m) => m.Palmares),
|
|
127
|
+
);
|
|
128
|
+
actionRunEffect(PALMARES_CODE_ACTION, () => myCupsSignal.value.length > 0);
|
|
129
|
+
const PALMARES_CODE = PALMARES_CODE_ACTION.bindParams(true);
|
|
130
|
+
// useAsyncData(PALMARES_CODE, { run: hasPalmares, loading: true })
|
|
131
|
+
```
|
|
132
|
+
|
|
115
133
|
3. **Nothing above the component asks** → `useAsyncData` with a function, the
|
|
116
134
|
fallback: the component's own existence is the only thing that asks, so the
|
|
117
135
|
request is its own, for the life of the instance.
|
package/docs/resource.md
CHANGED
|
@@ -42,6 +42,8 @@ Actions are read in components through the action system (`useAsyncData`,
|
|
|
42
42
|
- [Callback return contracts](#callback-return-contracts)
|
|
43
43
|
- [When the backend answers a sub-route with the whole parent](#when-the-backend-answers-a-sub-route-with-the-whole-parent)
|
|
44
44
|
- [Relations and autorerun](#relations-and-autorerun)
|
|
45
|
+
- [Reading a relation](#reading-a-relation)
|
|
46
|
+
- [When the child is deleted](#when-the-child-is-deleted)
|
|
45
47
|
- [What a read shows while its params move](#what-a-read-shows-while-its-params-move)
|
|
46
48
|
- [`withParams()`: a scope with reruns of its own](#withparams-a-scope-with-reruns-of-its-own)
|
|
47
49
|
- [`dependencies`: rerun after another resource writes](#dependencies-rerun-after-another-resource-writes)
|
|
@@ -434,7 +436,7 @@ A row counts as written only when something in it changed:
|
|
|
434
436
|
- **A relation** changes its row when the row points at other children; the
|
|
435
437
|
children themselves are followed through the relation — `game.place.label`
|
|
436
438
|
read in a render re-renders it when the place is written, and the game row
|
|
437
|
-
stays the one it was.
|
|
439
|
+
stays the one it was ([what a copy of it keeps](#reading-a-relation)).
|
|
438
440
|
- **A value written with `store.upsert()`** is compared by reference: a new
|
|
439
441
|
object is a change, whatever it holds.
|
|
440
442
|
|
|
@@ -461,18 +463,45 @@ second coat: a verb on one relation carrying an id from a **different** relation
|
|
|
461
463
|
people — is two relations spelled as one, and the one the gesture is about
|
|
462
464
|
(game × group) ends up with no rows and nothing for its row on screen to read.
|
|
463
465
|
|
|
464
|
-
| Situation
|
|
465
|
-
|
|
|
466
|
-
| The child is a first-class entity with its own store, shared across parents (a user referenced by many games)
|
|
467
|
-
| The child only exists inside its owner, with no identity outside it (a game's candidates, a table's columns)
|
|
468
|
-
| The relation itself carries fields (`candidate_since`, `seen_at`, `slot`)
|
|
469
|
-
|
|
|
470
|
-
|
|
|
471
|
-
|
|
|
466
|
+
| Situation | Use |
|
|
467
|
+
| ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
468
|
+
| The child is a first-class entity with its own store, shared across parents (a user referenced by many games) | `.one()` / `.many()` |
|
|
469
|
+
| The child only exists inside its owner, with no identity outside it (a game's candidates, a table's columns) | `.scopedOne()` / `.scopedMany()` |
|
|
470
|
+
| The relation itself carries fields (`candidate_since`, `seen_at`, `slot`) | `.scopedMany()` — those fields belong to the pair; putting them in a shared child store corrupts that entity for every other reader |
|
|
471
|
+
| A list whose items pair another resource's entity with fields of their own (my contacts, a club's members, a tournament's registrations) | the pair is the row, under a key of its own, and the entity goes under `.one()` on it; never spread into the row (see below) |
|
|
472
|
+
| The backend answers every sub-route with the whole refreshed parent | still model the relation; absorb the response with a plural callback (see below) |
|
|
473
|
+
| A genuine partial update of the parent itself (cancel a game) | plain `PATCH` on the parent |
|
|
474
|
+
| Several writes on the item itself, none of them a sub-object (cancel, archive, mark as seen) | one named in [`writes`](#several-writes-on-one-item-writes) each |
|
|
472
475
|
|
|
473
476
|
Singular vs plural is about the relation, not the verb: `.one`/`.scopedOne` for a
|
|
474
477
|
single sub-object, `.many`/`.scopedMany` for a collection.
|
|
475
478
|
|
|
479
|
+
**A row pairing an entity with fields of its own holds the entity as a
|
|
480
|
+
relation.** One of my contacts is what links me to someone (games together,
|
|
481
|
+
favourite) and that someone, the same user their profile shows. The row is the
|
|
482
|
+
pair, keyed by it: a resource of its own, or a `.scopedMany()` of an owner the
|
|
483
|
+
store holds (a club's members). The person goes under `.one()`:
|
|
484
|
+
|
|
485
|
+
```js
|
|
486
|
+
// GET /me/contacts → [{ key: "user:12", contact: { … }, user: { id: 12, … } }, …]
|
|
487
|
+
const CONTACT = resource("contact", { idKey: "key", GET_MANY });
|
|
488
|
+
CONTACT.one("user", USER, { onDelete: "cascade" });
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
The row is nothing without the person, so it leaves when they are deleted
|
|
492
|
+
(`onDelete: "cascade"`, see [When the child is deleted](#when-the-child-is-deleted)).
|
|
493
|
+
|
|
494
|
+
Spread into the row (`{ ...user, key, contact }`), the person is a second copy
|
|
495
|
+
in another store. Their profile read again, a write on them, a search that finds
|
|
496
|
+
them: none of these reach the row, and the list shows the old person until it is
|
|
497
|
+
read again. A [`dependency`](#dependencies-rerun-after-another-resource-writes)
|
|
498
|
+
rereading the list after every write that might change the person is the same
|
|
499
|
+
copy, refreshed by hand, and it misses the writes nobody listed. A backend
|
|
500
|
+
answering the row flat is reshaped in the callback
|
|
501
|
+
(`({ key, contact, ...user }) => ({ key, contact, user })`). It is the mirror of
|
|
502
|
+
the row above it: the pair's fields written into the entity's store corrupt it
|
|
503
|
+
for every reader, the entity's fields written into the pair's row go stale.
|
|
504
|
+
|
|
476
505
|
**A write whose effect the response does not carry is a modelling bug, not a
|
|
477
506
|
case for the client to remember.** The row on screen claiming "this happened"
|
|
478
507
|
reads the store, which holds only what responses carried; whatever the screen
|
|
@@ -649,7 +678,9 @@ Relationship mutations do **not** invalidate their parent by default. The rules:
|
|
|
649
678
|
- `.scopedOne` mutations rerun nothing, ever — the result is the new value,
|
|
650
679
|
and whoever reads the property is drawn again with it.
|
|
651
680
|
- `.one` / `.many` children live in an independent store; mutating them never
|
|
652
|
-
reruns the parent.
|
|
681
|
+
reruns the parent. Deleting a `.one()` child removes its parents only when
|
|
682
|
+
the relation says so ([When the child is deleted](#when-the-child-is-deleted)).
|
|
683
|
+
A `.many()` write changes the parent's array only through
|
|
653
684
|
the answers naming it (`[parentId, …]`, see
|
|
654
685
|
[Callback return contracts](#callback-return-contracts)); otherwise declare
|
|
655
686
|
it with [`dependencies`](#dependencies-rerun-after-another-resource-writes)
|
|
@@ -667,6 +698,84 @@ is a resource of its own, refreshed only by these rules. When a parent field
|
|
|
667
698
|
depends on a child mutation, say so with `dependencies` rather than relying on a
|
|
668
699
|
rerun that will not happen.
|
|
669
700
|
|
|
701
|
+
## Reading a relation
|
|
702
|
+
|
|
703
|
+
`contact.user` is read through the relation each time: in a render, it is the
|
|
704
|
+
child as the store holds it, and the render that read it is drawn again when the
|
|
705
|
+
child is written. The row, and the list holding it, stay the objects they were
|
|
706
|
+
when only a child changes
|
|
707
|
+
([a row counts as written only when something in it changed](#searching-the-same-collection)).
|
|
708
|
+
So anything taken out of a row is a snapshot of that moment, and stays as it
|
|
709
|
+
was:
|
|
710
|
+
|
|
711
|
+
- `{ ...contact }`, which holds the child the spread found;
|
|
712
|
+
- a child kept in a variable;
|
|
713
|
+
- a value derived in `useMemo(…, [contacts])`: the list does not move when a
|
|
714
|
+
child does, so the derivation never runs again.
|
|
715
|
+
|
|
716
|
+
Read the relation where it is drawn. A derivation that has to flatten the rows
|
|
717
|
+
(`{ ...contact.user, contact: contact.contact }`) reads the children at render
|
|
718
|
+
and is keyed on them: a `.one()` child is the same object until it is written,
|
|
719
|
+
and a new one after.
|
|
720
|
+
|
|
721
|
+
## When the child is deleted
|
|
722
|
+
|
|
723
|
+
A `.one()` child leaves its store when it is deleted: its `DELETE`, its
|
|
724
|
+
`DELETE_MANY`, or a write named in `writes` with verb `DELETE` (a profile merged
|
|
725
|
+
into another account). Whether its parents leave with it is declared on the
|
|
726
|
+
relation. Declared wrong, the screen draws dead rows: a parent with nothing in
|
|
727
|
+
it, until the parent is read again. A `.many()` needs nothing: a deleted child
|
|
728
|
+
leaves every array holding it. Decide it for every `.one()` by asking what the
|
|
729
|
+
parent is without its child:
|
|
730
|
+
|
|
731
|
+
| The parent without its child | Declare | Once the child is deleted |
|
|
732
|
+
| -------------------------------------------------------------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------ |
|
|
733
|
+
| Still something: a game whose organizer deleted their account, a message whose author left | nothing | the parent stays, and its property reads an empty object until the parent is read again |
|
|
734
|
+
| Nothing: a row of my contacts (the person), a favourite (the place), a notification about one game | `onDelete: "cascade"` | the parent leaves its store in the same write, and with it every list and range holding it |
|
|
735
|
+
|
|
736
|
+
The server answers the same question with its foreign key: `ON DELETE CASCADE`
|
|
737
|
+
on the parent's table is `onDelete: "cascade"` here, and `SET NULL` (or a row
|
|
738
|
+
kept pointing at nothing) is the default.
|
|
739
|
+
|
|
740
|
+
**`onDelete: "cascade"`.** The parent leaves as on a `DELETE` of its own,
|
|
741
|
+
without the action:
|
|
742
|
+
|
|
743
|
+
- a `GET_MANY` or a `GET_RANGE` holding it stops drawing it, and a `GET` of that
|
|
744
|
+
row has nothing left to show;
|
|
745
|
+
- nothing listening to the parent's own writes hears it (its `rerunOn`, a
|
|
746
|
+
`dependencies` naming it): the parent wrote nothing;
|
|
747
|
+
- the parent leaving is a deletion too: a resource pointing at it through a
|
|
748
|
+
cascading `.one()` of its own loses its rows in turn;
|
|
749
|
+
- a read of the parent that answers the row again brings it back: the server
|
|
750
|
+
has the last word.
|
|
751
|
+
|
|
752
|
+
Keep a [`dependencies`](#dependencies-rerun-after-another-resource-writes)
|
|
753
|
+
entry on the child's delete when that delete changes other parents too (a
|
|
754
|
+
merge moves my links onto the account it merged into): the cascade takes away
|
|
755
|
+
at once what is gone, the reread brings what moved. The relation's own `DELETE`
|
|
756
|
+
is not a deletion of the child. It unlinks: the property becomes `null` and the
|
|
757
|
+
parent stays. On a `.one()` declared on a scoped child, `onDelete` throws: a
|
|
758
|
+
scoped item leaves through its owner's callbacks, so the owner is read again
|
|
759
|
+
after the child's delete (`dependencies`), and the item reads an empty object
|
|
760
|
+
until that read answers.
|
|
761
|
+
|
|
762
|
+
**Left out: the empty object.** A parent outliving its child reads its property
|
|
763
|
+
as an object with no fields, whose `valueOf()` is `null`. A relation that is
|
|
764
|
+
`null` reads the same. It is an object on purpose: `game.organizer.name` reads
|
|
765
|
+
`undefined` rather than throwing, and a binding handed `game.organizer` follows
|
|
766
|
+
the relation, a child arriving later included. But it is **truthy**, and that is
|
|
767
|
+
where screens draw dead things:
|
|
768
|
+
|
|
769
|
+
- `game.organizer ? <Avatar user={game.organizer} /> : null` draws an avatar of
|
|
770
|
+
nobody;
|
|
771
|
+
- `{ ...contact.user, key }` spreads into a row holding a key and nothing else,
|
|
772
|
+
which a name component fills with its fallback ("Anonymous").
|
|
773
|
+
|
|
774
|
+
Test the child by a field it always has, its id:
|
|
775
|
+
`game.organizer.id ? … : …`. A list that must never draw a parent without its
|
|
776
|
+
child needs the cascade, not a filter in the screen: the filter guesses what the
|
|
777
|
+
store knows, and every other list reading the relation needs the same filter.
|
|
778
|
+
|
|
670
779
|
## What a read shows while its params move
|
|
671
780
|
|
|
672
781
|
A binding following a signal — a route action, `bindParams(signal)` — moves to
|