@jsenv/navi 0.30.23 → 0.30.25
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 +139 -17
- package/dist/dev/jsenv_navi.js.map +5 -5
- package/dist/jsenv_navi.js +139 -17
- package/dist/jsenv_navi.js.map +5 -5
- package/docs/AI_INSTRUCTIONS.md +5 -1
- package/docs/actions.md +2 -2
- package/docs/list_refresh.md +8 -0
- package/docs/resource.md +123 -13
- 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
|
@@ -554,8 +554,8 @@ const SUMMARY = createAction(fetchSummary, {
|
|
|
554
554
|
});
|
|
555
555
|
```
|
|
556
556
|
|
|
557
|
-
After a listed write, its instances that answered,
|
|
558
|
-
again; `on` is read as a resource's is
|
|
557
|
+
After a listed write, its instances that answered, are still asking, or failed
|
|
558
|
+
ask again; `on` is read as a resource's is
|
|
559
559
|
([resource.md](./resource.md#dependencies-rerun-after-another-resource-writes)).
|
|
560
560
|
|
|
561
561
|
What the server moves on its own — an email that may bounce, an account being
|
package/docs/list_refresh.md
CHANGED
|
@@ -236,6 +236,14 @@ changes what re-reads the slices and nothing else — `GET_MANY` still reruns af
|
|
|
236
236
|
`POST`. `false` turns a key off. A `withParams()` scope or a relation reads its
|
|
237
237
|
own `rerunOn` the same way, against the resource it is declared on.
|
|
238
238
|
|
|
239
|
+
A write asks again every read that answered, is still out, or failed. A failed
|
|
240
|
+
read has nothing else to bring it back: left out, one bad answer during a deploy
|
|
241
|
+
would keep a count on its old value until the page reloads. The status does not
|
|
242
|
+
matter, since a write can change what a `403` or a `404` answers, so a read that
|
|
243
|
+
keeps failing costs one request and one error per write, and never a retry loop.
|
|
244
|
+
Two reads stay out: an aborted or never-run one, which nobody is waiting for,
|
|
245
|
+
and a head start that failed, which the screen arriving asks for itself.
|
|
246
|
+
|
|
239
247
|
Adding `PUT`/`PATCH` to `GET_MANY` is the usual over-correction: it costs a
|
|
240
248
|
request and a `loading` pass to obtain something the response already contained.
|
|
241
249
|
When a `PATCH` "doesn't show", the fix is almost always server-side — return the
|
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
|
|
@@ -708,7 +817,7 @@ another collection, drawn from its first item
|
|
|
708
817
|
|
|
709
818
|
A resource reruns its own reads after its own writes (`rerunOn`, see
|
|
710
819
|
[list_refresh.md](./list_refresh.md#rerunon-verb-by-verb)), and it does so for
|
|
711
|
-
every read of the resource: a `POST` reruns every `GET_MANY`
|
|
820
|
+
every read of the resource: a `POST` reruns every `GET_MANY` already asked,
|
|
712
821
|
whatever params each one was bound to. Right for one collection, wrong for a
|
|
713
822
|
resource read under several fixed questions at once — the admins and the guests
|
|
714
823
|
of one `USER`, each list on its own screen. A guest created should not send the
|
|
@@ -767,8 +876,9 @@ const ROLE_WITH_OWNERS = ROLE.withParams(
|
|
|
767
876
|
```
|
|
768
877
|
|
|
769
878
|
Any write on a listed resource (`POST`, `PUT`, `PATCH`, `DELETE`) reruns the
|
|
770
|
-
|
|
771
|
-
|
|
879
|
+
`GET` and `GET_MANY` of the resource that declared it, those that failed included
|
|
880
|
+
([which ones](./list_refresh.md#rerunon-verb-by-verb)), and invalidates its
|
|
881
|
+
`GET_RANGE` readers: its own `rerunOn` is about its own writes. A
|
|
772
882
|
dependency is a facade: `DATABASE` and `DATABASE.withParams(…)` are two of them,
|
|
773
883
|
and only writes on the one listed count. Nothing goes the other way, and nothing
|
|
774
884
|
is inferred — a relation that really is a sub-route of the parent is modelled
|