foldkit-remote 0.12.0 → 0.14.0
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 +265 -12
- package/dist/{index-njA5CxbS.d.mts → index-CXAGo7Dz.d.mts} +484 -50
- package/dist/index-CXAGo7Dz.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/port.d.mts +1 -1
- package/dist/port.mjs +1 -1
- package/dist/{src-9NIJGDTJ.mjs → src-CcPPtNuH.mjs} +1121 -131
- package/dist/src-CcPPtNuH.mjs.map +1 -0
- package/package.json +4 -4
- package/dist/index-njA5CxbS.d.mts.map +0 -1
- package/dist/src-9NIJGDTJ.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -361,7 +361,15 @@ const overHttp = Remote.clientLayer(
|
|
|
361
361
|
```
|
|
362
362
|
|
|
363
363
|
`headers` is read for each request, so a token that changes reaches the next
|
|
364
|
-
call.
|
|
364
|
+
call. A 403 whose body does not say `forbidden` itself, as Remote's server's
|
|
365
|
+
does, comes from middleware in front of it refusing a session (a bare
|
|
366
|
+
`{ error }` included): it fails a mutation as `Forbidden` and any other call
|
|
367
|
+
with the status. Neither carries live data. For live data over the same `POST`s,
|
|
368
|
+
`Remote.httpWithLive` keeps reads, queries, and mutations as `http` sends
|
|
369
|
+
them and streams live requirements as a `text/event-stream` of changes;
|
|
370
|
+
[`serveFetch`](../remote-server/README.md#over-fetch-workers) is the other
|
|
371
|
+
end. Abandoning the stream aborts the request, which interrupts the
|
|
372
|
+
subscription server-side.
|
|
365
373
|
|
|
366
374
|
For a server in a worker or in the page (a sandbox), `foldkit-remote/port`
|
|
367
375
|
speaks Effect's own RPC worker protocol over a `MessagePort`, so live data,
|
|
@@ -534,7 +542,8 @@ When a read sits at `Initial` and you expected data, ask the Model why with
|
|
|
534
542
|
`Data.why`; see [Why is it still `Initial`?](#why-is-it-still-initial).
|
|
535
543
|
|
|
536
544
|
`RemoteData.match` is exhaustive, so adding or omitting a state is visible at
|
|
537
|
-
compile time.
|
|
545
|
+
compile time. `RemoteData.answered(data)` is whether a read has an answer yet:
|
|
546
|
+
`Ready`, `Refreshing`, `Failed` or `NotFound`, not `Initial` or `Loading`.
|
|
538
547
|
|
|
539
548
|
### Drawing one: `RemoteData.render`
|
|
540
549
|
|
|
@@ -669,8 +678,8 @@ the same way, with the request's error; see
|
|
|
669
678
|
Live events carry cursors. One behind the Model's is a duplicate and is
|
|
670
679
|
dropped; one ahead of the next is a **gap**, recorded in `RemoteModel.gaps`
|
|
671
680
|
and not applied, so the screen keeps what the server last said rather than
|
|
672
|
-
a history with a hole in it. The
|
|
673
|
-
[
|
|
681
|
+
a history with a hole in it. The live entry then restarts the stream and
|
|
682
|
+
refetches what it covers; see [When a stream breaks](#when-a-stream-breaks).
|
|
674
683
|
|
|
675
684
|
## Normalized entities and selections
|
|
676
685
|
|
|
@@ -868,9 +877,12 @@ const ready = await Effect.runPromise(
|
|
|
868
877
|
)
|
|
869
878
|
```
|
|
870
879
|
|
|
871
|
-
`actives` is the record `Data.wiring` takes.
|
|
880
|
+
`actives` is the record `Data.wiring` takes. A `Surface.each` family is one
|
|
881
|
+
entry of it: its parent's requirements join its instances', so the parent is
|
|
882
|
+
fetched before what it reveals. The passes are bounded (8 by
|
|
872
883
|
default, `{ passes }` to change it); a Surface still reading after the last
|
|
873
|
-
fails the Effect with `RemoteUnsatisfied`, which names it
|
|
884
|
+
fails the Effect with `RemoteUnsatisfied`, which names it — a still-reading
|
|
885
|
+
family instance as `Child[key]` — rather than
|
|
874
886
|
rendering it loading. A read the server leaves unanswered settles as missing,
|
|
875
887
|
so it ends the loop; what runs out the passes is a chain whose every read
|
|
876
888
|
reveals one more.
|
|
@@ -1065,7 +1077,9 @@ could match — predicate containment, which Remote deliberately does not do.
|
|
|
1065
1077
|
missing a field the *body* reads), every match could be shown (no row missing a
|
|
1066
1078
|
field the *Selection* reads), and the list is terminal at both ends. So
|
|
1067
1079
|
empty-and-complete and empty-and-partial stay different answers — one means
|
|
1068
|
-
"none", the other means "none that I can see yet".
|
|
1080
|
+
"none", the other means "none that I can see yet". An input no interpreter may
|
|
1081
|
+
answer (a search holding NUL, which the server refuses too) gives no items and
|
|
1082
|
+
`complete: false`, rather than throwing in `update`.
|
|
1069
1083
|
|
|
1070
1084
|
Nothing is created: no connection, nothing new to retain, nothing new to fetch.
|
|
1071
1085
|
The server stays authoritative for which rows exist.
|
|
@@ -1204,7 +1218,8 @@ identity and by `entity\0id\0field` mark.
|
|
|
1204
1218
|
A broken live stream is not a failed read. Its `ReadFailed` carries the
|
|
1205
1219
|
`stream`, and records a gap in `RemoteModel.gaps` rather than failing any
|
|
1206
1220
|
field: nothing was being read, and the values on screen are still what the
|
|
1207
|
-
server last said.
|
|
1221
|
+
server last said. The stream restarts on its own; see
|
|
1222
|
+
[When a stream breaks](#when-a-stream-breaks).
|
|
1208
1223
|
|
|
1209
1224
|
### When a field is withheld
|
|
1210
1225
|
|
|
@@ -1295,11 +1310,126 @@ A mutation that deletes says so. The server's outcome carries
|
|
|
1295
1310
|
server names no list. Patches apply first, so an entity named both ways is
|
|
1296
1311
|
deleted; a retry of the same request deletes nothing again.
|
|
1297
1312
|
|
|
1313
|
+
**Which lists a mutation changed is worked out from its answer.** Each patch
|
|
1314
|
+
says which row it wrote; the fields whose value differs from what the client
|
|
1315
|
+
held are what changed (an answer returning a whole row changes only what is
|
|
1316
|
+
new). Every loaded list over that Entity is judged by its query's body against
|
|
1317
|
+
the store the answer left:
|
|
1318
|
+
|
|
1319
|
+
- a list whose body reads none of the changed fields stands;
|
|
1320
|
+
- a row it holds that still matches, or a row it does not hold that still does
|
|
1321
|
+
not, leaves it standing;
|
|
1322
|
+
- a held row that stops matching leaves it, with nothing fetched: whether a
|
|
1323
|
+
row belongs is the body's, judged exactly;
|
|
1324
|
+
- a row that starts matching, a row new to the client, a held row whose
|
|
1325
|
+
ordering field changed, or a row whose order moves it in from past what a
|
|
1326
|
+
list has loaded, is placed where the body's order puts it among the rows
|
|
1327
|
+
loaded (`placeIn`), when
|
|
1328
|
+
the order is placeable (`Query.placement`); a row that sorts past what is
|
|
1329
|
+
loaded is left out of it (and out of what showed it), for the next page. A
|
|
1330
|
+
row that was itself where a page met rows not loaded (the last before a
|
|
1331
|
+
cursor, the first after one) cannot be told to have moved past it, its old
|
|
1332
|
+
values being gone, so it invalidates the list instead;
|
|
1333
|
+
- anything else invalidates it, and its read entry fetches it again: a row the
|
|
1334
|
+
client cannot judge (it lacks a field the body reads), an order it cannot
|
|
1335
|
+
place (text with no declared collation, the server's own), or a row an
|
|
1336
|
+
optimistic overlay shows; a read of it already in flight is restarted, since
|
|
1337
|
+
it could answer with the rows before the write;
|
|
1338
|
+
- a list over a `Query.make` query, which declares no body, is invalidated by
|
|
1339
|
+
any change to its Entity;
|
|
1340
|
+
- a row a list does not hold that changes a member the server's own read rule
|
|
1341
|
+
filters on (an owner, a team: declared on the Entity as `ReadFilter`, which
|
|
1342
|
+
`foldkit-guard` attaches) invalidates it: whether the row now belongs is the
|
|
1343
|
+
server's to say, since the client runs no such rule. A row it holds is
|
|
1344
|
+
judged by the body as usual. A new row counts, so under such a rule a create
|
|
1345
|
+
refetches the lists over its Entity;
|
|
1346
|
+
- a list whose body declares no order leaves its order to the server, by
|
|
1347
|
+
fields the client cannot see, so any change to a row it holds invalidates
|
|
1348
|
+
it. Declare the order in the body (`Query.orderBy`) to keep a list that a
|
|
1349
|
+
change provably did not move.
|
|
1350
|
+
|
|
1351
|
+
**A placement can be checked against the server.** `Remote.make({ ...,
|
|
1352
|
+
verifyPlacement: true })` places as usual, and also fetches each list it placed
|
|
1353
|
+
what the server said in (an answer, as above, or a read or a live patch, as
|
|
1354
|
+
below); when the server's first page arrives it is compared with the order
|
|
1355
|
+
placed, and a disagreement is kept in `model.remote.placementMismatches` (and
|
|
1356
|
+
`Data.inspect`): an order the page does not share, or a row placing took out
|
|
1357
|
+
that the page still lists. A pending edit is not checked: the server does not have it
|
|
1358
|
+
yet. Turn it on in development and tests: a collation the client
|
|
1359
|
+
and the server disagree on then fails an assertion instead of misplacing a row
|
|
1360
|
+
for a reader.
|
|
1361
|
+
|
|
1362
|
+
So a rename updates every view of the row and fetches nothing, a status change
|
|
1363
|
+
moves a row out of the lists filtered by status and into the others in their
|
|
1364
|
+
order, and a price change moves the row within a list sorted by price, all
|
|
1365
|
+
without a request.
|
|
1366
|
+
|
|
1367
|
+
**A pending edit, and the server's word on a row, are placed the same way.**
|
|
1368
|
+
While a request's `optimistic` patch or a `Data.overlay` is pending, a list
|
|
1369
|
+
that holds the row shows it where the edit puts it, or leaves it out once it
|
|
1370
|
+
stops matching; the server's pages are untouched, and lifting the edit puts
|
|
1371
|
+
the row back. A read or a live `EntityPatched` that brings a new value for a
|
|
1372
|
+
row a list holds moves it in the list's pages, as an answer does. Neither
|
|
1373
|
+
brings a row into a list, since what the list selects of it may not be
|
|
1374
|
+
loaded, and neither fetches: a list it cannot place stands as it is.
|
|
1375
|
+
|
|
1376
|
+
**An inserted row is drawn in order.** A `ConnectionChange.prepend` or
|
|
1377
|
+
`append` (a pending create's, a confirmed one's) and a live
|
|
1378
|
+
`ConnectionInsert` say a row is in a list; they do not say where. In a list
|
|
1379
|
+
the client can place, a read draws the row where the body's order puts it,
|
|
1380
|
+
or leaves it out when it sorts past what is loaded; one it cannot place (a
|
|
1381
|
+
field the order reads is missing, the order is the server's) stays at the end
|
|
1382
|
+
its change names. `Data.visibleItems(remote, connection)` gives a list's edges
|
|
1383
|
+
as its reads draw them, for a persistence `snapshot` that stores a list as
|
|
1384
|
+
shown.
|
|
1385
|
+
`Data.refresh` stays for what an answer cannot say, such as a server-side
|
|
1386
|
+
effect on rows it did not return.
|
|
1387
|
+
|
|
1298
1388
|
`Data.mutation(model, requestId)` reads what became of it from the Model:
|
|
1299
1389
|
`Pending`, `Applied`, `Failed` with the error the server or transport gave, or
|
|
1300
1390
|
`Unknown` for an id never started here (or settled so long ago it left the
|
|
1301
1391
|
bounded ledger). A retry reuses its id, and the latest outcome wins.
|
|
1302
1392
|
|
|
1393
|
+
A mutation can say what the server may refuse it with, as data rather than a
|
|
1394
|
+
message to parse:
|
|
1395
|
+
|
|
1396
|
+
```ts
|
|
1397
|
+
const SetPrice = Mutation.make('SetPrice', {
|
|
1398
|
+
Input: { id: Schema.String, price: Schema.Number },
|
|
1399
|
+
Output: {},
|
|
1400
|
+
Refusal: Schema.Union([Refusal.field('price', Schema.String), Refusal.conflict]),
|
|
1401
|
+
})
|
|
1402
|
+
|
|
1403
|
+
Data.refusal(model, requestId, SetPrice) // Option<{ _tag: 'Field', key: 'price', reason: string } | { _tag: 'Conflict' }>
|
|
1404
|
+
```
|
|
1405
|
+
|
|
1406
|
+
A mutation can also be a declared write: `Mutation.write(name, write)` over a
|
|
1407
|
+
`foldkit-entity` `Write.update`, `Write.insert` or `Write.delete`. Its input is
|
|
1408
|
+
the write's, it answers nothing of its own, and `Data.mutate` shows an update's
|
|
1409
|
+
or an insert's row until the server answers, so it needs no `optimistic` list;
|
|
1410
|
+
a delete shows nothing until its answer removes the row everywhere. `keys` names the input keys
|
|
1411
|
+
an author changed: only those are shown, and the request carries them for the
|
|
1412
|
+
server to write; the input is still sent and validated whole.
|
|
1413
|
+
|
|
1414
|
+
```ts
|
|
1415
|
+
const EditProject = Mutation.write(
|
|
1416
|
+
'EditProject',
|
|
1417
|
+
Write.update(Entity.input(Project, EditProjectInput), { id: 'id' }),
|
|
1418
|
+
)
|
|
1419
|
+
Data.mutate(model, EditProject, { id: 'p1', name: 'Apollo II', status: 'active' }, { keys: ['name'] })
|
|
1420
|
+
```
|
|
1421
|
+
|
|
1422
|
+
`Refusal.field(key, reason)` names an input key and why; `Refusal.conflict`
|
|
1423
|
+
says the row moved on. The server sends the refusal encoded by this schema and
|
|
1424
|
+
`Data.refusal` decodes it by the same one; it is none while the mutation is
|
|
1425
|
+
pending or applied, or when it failed for another reason.
|
|
1426
|
+
|
|
1427
|
+
A server may also refuse who is asking, whatever the mutation
|
|
1428
|
+
(`RemoteServer.forbid`). Then `Data.mutation(model, requestId)` is `Failed`
|
|
1429
|
+
with an error of `_tag: 'Forbidden'`, and `reason` when the server gave one,
|
|
1430
|
+
for the person who asked. Asking again as the same person is refused again,
|
|
1431
|
+
so show it rather than retry.
|
|
1432
|
+
|
|
1303
1433
|
Optimistic entity patches are **layers over the base store**, not inverse
|
|
1304
1434
|
patches. If multiple mutations overlap, the visible cache is recomputed as base
|
|
1305
1435
|
plus the still-pending layers, so settling one does not require trying to undo
|
|
@@ -1346,6 +1476,10 @@ const back = Data.lift(previewed, 'post-preview')
|
|
|
1346
1476
|
`Failed`, with an `Overlaid` error naming what is missing, since nothing will
|
|
1347
1477
|
fetch it. Overlay every field the Selection reads, or preview through a
|
|
1348
1478
|
smaller Selection.
|
|
1479
|
+
- `Data.overlays(model)` lists the ids shown now, in the order they are drawn.
|
|
1480
|
+
A caller that overlays from state of its own reconciles against it:
|
|
1481
|
+
`foldkit-sync/remote` shows a replica's pending cell edits this way, so the
|
|
1482
|
+
durable edit stays Sync's and Remote only draws it.
|
|
1349
1483
|
|
|
1350
1484
|
### Reading past what is only pending
|
|
1351
1485
|
|
|
@@ -1403,14 +1537,54 @@ ordered cursors:
|
|
|
1403
1537
|
- the next cursor is applied;
|
|
1404
1538
|
- a cursor ahead of the expected value is a **gap** and is not applied.
|
|
1405
1539
|
|
|
1406
|
-
A gap is recorded in `RemoteModel.gaps` so the host can resynchronize rather than
|
|
1407
|
-
silently accepting missing history. An in-order event or `GapCleared` clears it.
|
|
1408
|
-
|
|
1409
1540
|
Entity patches update normalized fields, deletes create tombstones, and
|
|
1410
1541
|
connection insert/remove/invalidate events reconcile the same connection model
|
|
1411
1542
|
used by query pages and optimistic overlays. A stream failure becomes a Remote
|
|
1412
1543
|
failure Message rather than mutating anything out of band.
|
|
1413
1544
|
|
|
1545
|
+
### When a stream breaks
|
|
1546
|
+
|
|
1547
|
+
A stream breaks when its transport fails or when an event arrives ahead of
|
|
1548
|
+
its cursor (a gap). Either way, what the server said meanwhile is lost: no
|
|
1549
|
+
server replays it, since `after` only numbers the events that follow. So
|
|
1550
|
+
recovery is a refetch, and the live entry does it on its own:
|
|
1551
|
+
|
|
1552
|
+
```text
|
|
1553
|
+
break -> gap marked, restarts + 1 -> backoff
|
|
1554
|
+
-> RefreshStarted (what the stream covers) + GapCleared -> resubscribe from the cursor
|
|
1555
|
+
\-> read entry refetches
|
|
1556
|
+
```
|
|
1557
|
+
|
|
1558
|
+
- **The backoff is Sync's transport policy:** 50 ms after the first break,
|
|
1559
|
+
doubling for each break in a row, at most 5 s, with ±20% jitter.
|
|
1560
|
+
`retryBase` and `maxRetryDelay` in the `Data.subscriptions` options change
|
|
1561
|
+
it. An event applied after a resubscribe starts it over.
|
|
1562
|
+
- **A gap stays until the restart.** An in-order event that a broken stream
|
|
1563
|
+
still delivers is applied, but does not clear it. The event that arrived
|
|
1564
|
+
ahead was dropped, and only the refetch brings back what it said.
|
|
1565
|
+
- **What a stream covers is its requirements.** A list the server watches
|
|
1566
|
+
for it (a `ConnectionInvalidate` it sends) is the source's to re-send when
|
|
1567
|
+
the stream opens; the cloudflare example's poll invalidates its list on
|
|
1568
|
+
every open for this reason.
|
|
1569
|
+
|
|
1570
|
+
`Data.liveStatus(model, active)` says how what one active entry reads live is
|
|
1571
|
+
doing, for a view that shows it:
|
|
1572
|
+
|
|
1573
|
+
```ts
|
|
1574
|
+
Match.value(Data.liveStatus(model, Watched)).pipe(
|
|
1575
|
+
Match.tagsExhaustive({
|
|
1576
|
+
Idle: () => 'not live',
|
|
1577
|
+
Live: () => 'live',
|
|
1578
|
+
Reconnecting: ({ attempt }) => `reconnecting (attempt ${attempt})`,
|
|
1579
|
+
}),
|
|
1580
|
+
)
|
|
1581
|
+
```
|
|
1582
|
+
|
|
1583
|
+
`Reconnecting` lasts from the break until the stream has resubscribed, and
|
|
1584
|
+
carries the error, which is none for a gap. `RemoteModel.streams` holds each
|
|
1585
|
+
stream's restarts, its breaks in a row and its last error. `forget` clears it
|
|
1586
|
+
with every other server-derived fact.
|
|
1587
|
+
|
|
1414
1588
|
## Retention and garbage collection
|
|
1415
1589
|
|
|
1416
1590
|
Remote is a cache, so it should be allowed to forget facts no active feature
|
|
@@ -1481,6 +1655,58 @@ snapshot from another version/scope, an oversized snapshot, or malformed data is
|
|
|
1481
1655
|
discarded and the planner refetches. `Hydrated` is itself a Remote Message, so
|
|
1482
1656
|
hydration still changes the application through the reducer.
|
|
1483
1657
|
|
|
1658
|
+
### A cache kept across reloads: `Data.persistence`
|
|
1659
|
+
|
|
1660
|
+
The calls above are the parts. `Data.persistence` puts them together as a
|
|
1661
|
+
wiring: it restores the snapshot stored under a key, and saves the cache under
|
|
1662
|
+
that key whenever it changes.
|
|
1663
|
+
|
|
1664
|
+
```ts
|
|
1665
|
+
const persistence = Data.persistence({
|
|
1666
|
+
key: model => `remote-cache:${model.userId}`,
|
|
1667
|
+
scope: model => model.userId,
|
|
1668
|
+
maxBytes: 512_000,
|
|
1669
|
+
})
|
|
1670
|
+
|
|
1671
|
+
// Assembled beside `Data.wiring`; the runtime provides a `KeyValueStore`,
|
|
1672
|
+
// such as `BrowserKeyValueStore.layerLocalStorage`.
|
|
1673
|
+
const placements = Page.assemble(Data.wiring(actives), persistence)
|
|
1674
|
+
```
|
|
1675
|
+
|
|
1676
|
+
The restore and the save each run as Subscriptions, when the key changes or the
|
|
1677
|
+
cache does:
|
|
1678
|
+
|
|
1679
|
+
```text
|
|
1680
|
+
key -> restore -> Hydrated { from: key } -> restoredFrom = key -> save on each change
|
|
1681
|
+
```
|
|
1682
|
+
|
|
1683
|
+
- **A restore keeps what the Model already holds.** It hydrates with
|
|
1684
|
+
`merge: 'preserve-existing'`, so a fact resumed from the server, or read since
|
|
1685
|
+
the page started, wins over the older snapshot.
|
|
1686
|
+
- **So a change of principal is `Data.forget`, in the same `update`.** What one
|
|
1687
|
+
person read, left in memory, would be kept over the next one's snapshot and
|
|
1688
|
+
then saved under their key; `forget` clears it as the key changes. The
|
|
1689
|
+
`scope` check refuses a snapshot written for another reader; it cannot see
|
|
1690
|
+
facts that are only in memory.
|
|
1691
|
+
- **Nothing is saved before its own key is restored.** `Hydrated` records the
|
|
1692
|
+
key it came from in `model.remote.restoredFrom`. Until that key is restored,
|
|
1693
|
+
nothing is saved, so the empty store at startup cannot overwrite the
|
|
1694
|
+
snapshot it has not read yet. When the key changes (a different principal),
|
|
1695
|
+
saving stops again until the new key's snapshot is in.
|
|
1696
|
+
- **A store that cannot be read restores nothing, and so nothing is saved over
|
|
1697
|
+
it.** A stored snapshot from another version or scope, or one that will not
|
|
1698
|
+
decode, is removed, and an empty snapshot is restored in its place.
|
|
1699
|
+
- **Saving is debounced** (`debounce`, 250 ms by default) and best-effort: a
|
|
1700
|
+
full store keeps the snapshot it had, and the next change tries again. The
|
|
1701
|
+
write waits; the snapshot does not: it is serialized on each change of the
|
|
1702
|
+
cache, so under a steady stream of live changes keep it small, with
|
|
1703
|
+
`maxBytes` or a `snapshot` that stores only what a page shows. A
|
|
1704
|
+
cache grown past `maxBytes` removes its key, so an older, smaller snapshot
|
|
1705
|
+
cannot outlive it.
|
|
1706
|
+
|
|
1707
|
+
`snapshot` replaces `snapshotOf` when an application keeps something else, such
|
|
1708
|
+
as only the edges a page shows.
|
|
1709
|
+
|
|
1484
1710
|
## Server rendering: `Remote.resume`
|
|
1485
1711
|
|
|
1486
1712
|
A snapshot is built for a cache that survived a reload: it keeps the whole
|
|
@@ -1635,6 +1861,8 @@ Data.explain(model, projects, { surfaces: actives })
|
|
|
1635
1861
|
// activation: [{ surface: 'ProjectPage', path: ['route'], tag: 'Project' }]
|
|
1636
1862
|
```
|
|
1637
1863
|
|
|
1864
|
+
A `Surface.each` family reports each reading instance as `Child[key]`.
|
|
1865
|
+
|
|
1638
1866
|
There is still no **executor**, because what answers a query is a
|
|
1639
1867
|
`RemoteClient` Layer in the runtime rather than a value in the Model — the same
|
|
1640
1868
|
purity that lets this be replayed from a recorded Model is what keeps it out of
|
|
@@ -1773,7 +2001,7 @@ const ref = Query.first(25)(ProjectsByOwner.ref({ ownerId }))
|
|
|
1773
2001
|
const page = yield* Remote.query(ref)
|
|
1774
2002
|
|
|
1775
2003
|
updateRemote(remote, Remote.queryMessage(ref, page))
|
|
1776
|
-
|
|
2004
|
+
Data.visibleItems(remote, ref)
|
|
1777
2005
|
```
|
|
1778
2006
|
|
|
1779
2007
|
`items`, `hasNext`, `hasPrevious`, and `isGapped` describe the known region.
|
|
@@ -1782,6 +2010,31 @@ a refresh/query result settles it. A `ConnectionMerged` page with
|
|
|
1782
2010
|
`refreshes: true` replaces a stale connection's pages rather than merging into
|
|
1783
2011
|
them.
|
|
1784
2012
|
|
|
2013
|
+
### Where a row falls: `placeIn`
|
|
2014
|
+
|
|
2015
|
+
A query whose body declares a placeable order (`Query.placement` in
|
|
2016
|
+
`foldkit-entity`: numbers, booleans, and text under a portable collation) can
|
|
2017
|
+
have a changed row placed in its loaded pages by the client, by the same order
|
|
2018
|
+
the server compiles. Such a connection also reads every field its body filters
|
|
2019
|
+
and orders by, so the rows it holds can be compared with one that changed.
|
|
2020
|
+
|
|
2021
|
+
```ts
|
|
2022
|
+
// Schematic: `ProjectsByRank` is a Query.define ordered by a number, and
|
|
2023
|
+
// `connection` one of `remote.connections`. test/placement.test.ts runs it.
|
|
2024
|
+
placeIn(remote.entities, connection, ProjectsByRank.body!, {}, 'Project:p7')
|
|
2025
|
+
// Before { segment, edge } | End { segment } | Outside | Unknown { reason }
|
|
2026
|
+
```
|
|
2027
|
+
|
|
2028
|
+
- `Before` and `End` are positions among loaded edges: before an edge, or at
|
|
2029
|
+
the end of a segment that ends the list (`Terminal`).
|
|
2030
|
+
- `Outside` is a row that sorts past a `Cursor` boundary or into a gap between
|
|
2031
|
+
segments: it belongs to rows the client has not read, so it is not shown.
|
|
2032
|
+
- `Unknown` is anything not judged exactly: a body that is not placeable, a
|
|
2033
|
+
row or an edge lacking a field the order reads, keys that do not compare.
|
|
2034
|
+
|
|
2035
|
+
It is pure and changes nothing. A mutation's answer is placed through it, as
|
|
2036
|
+
above.
|
|
2037
|
+
|
|
1785
2038
|
### Mutations by hand
|
|
1786
2039
|
|
|
1787
2040
|
`Remote.mutateInto(bound, model, mutation, input, requestId, { optimistic, now })` is
|