foldkit-remote 0.20.0 → 0.22.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.
Files changed (198) hide show
  1. package/README.md +987 -198
  2. package/dist/answerGaps.d.ts +16 -0
  3. package/dist/answerGaps.d.ts.map +1 -0
  4. package/dist/answerGaps.js +60 -0
  5. package/dist/answerGaps.js.map +1 -0
  6. package/dist/client-B055kQuP.d.ts +110 -0
  7. package/dist/client-B055kQuP.d.ts.map +1 -0
  8. package/dist/client.d.ts +2 -0
  9. package/dist/client.js +142 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/coalesce-CLAr92Nk.d.ts +34 -0
  12. package/dist/coalesce-CLAr92Nk.d.ts.map +1 -0
  13. package/dist/coalesce.d.ts +2 -0
  14. package/dist/coalesce.js +131 -0
  15. package/dist/coalesce.js.map +1 -0
  16. package/dist/connection-BRyaJy89.d.ts +45 -0
  17. package/dist/connection-BRyaJy89.d.ts.map +1 -0
  18. package/dist/connection.d.ts +2 -0
  19. package/dist/connection.js +170 -0
  20. package/dist/connection.js.map +1 -0
  21. package/dist/data.d.ts +7 -0
  22. package/dist/data.d.ts.map +1 -0
  23. package/dist/data.js +21 -0
  24. package/dist/data.js.map +1 -0
  25. package/dist/diagnostics-DHhVYflV.d.ts +125 -0
  26. package/dist/diagnostics-DHhVYflV.d.ts.map +1 -0
  27. package/dist/diagnostics.d.ts +2 -0
  28. package/dist/diagnostics.js +60 -0
  29. package/dist/diagnostics.js.map +1 -0
  30. package/dist/entity-CgsG5Uel.d.ts +196 -0
  31. package/dist/entity-CgsG5Uel.d.ts.map +1 -0
  32. package/dist/entity-DuDxaa3n.js +279 -0
  33. package/dist/entity-DuDxaa3n.js.map +1 -0
  34. package/dist/entity.d.ts +2 -0
  35. package/dist/entity.js +1 -0
  36. package/dist/held.d.ts +19 -0
  37. package/dist/held.d.ts.map +1 -0
  38. package/dist/held.js +70 -0
  39. package/dist/held.js.map +1 -0
  40. package/dist/impact.d.ts +79 -0
  41. package/dist/impact.d.ts.map +1 -0
  42. package/dist/impact.js +166 -0
  43. package/dist/impact.js.map +1 -0
  44. package/dist/index.d.ts +1542 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +2791 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/inspect-DtRQGCRP.d.ts +134 -0
  49. package/dist/inspect-DtRQGCRP.d.ts.map +1 -0
  50. package/dist/inspect.d.ts +2 -0
  51. package/dist/inspect.js +33 -0
  52. package/dist/inspect.js.map +1 -0
  53. package/dist/json-DowLl1eB.d.ts +52 -0
  54. package/dist/json-DowLl1eB.d.ts.map +1 -0
  55. package/dist/json.d.ts +2 -0
  56. package/dist/json.js +123 -0
  57. package/dist/json.js.map +1 -0
  58. package/dist/law.d.ts +56 -0
  59. package/dist/law.d.ts.map +1 -0
  60. package/dist/law.js +201 -0
  61. package/dist/law.js.map +1 -0
  62. package/dist/live-BpTzlCnY.d.ts +79 -0
  63. package/dist/live-BpTzlCnY.d.ts.map +1 -0
  64. package/dist/live.d.ts +2 -0
  65. package/dist/live.js +126 -0
  66. package/dist/live.js.map +1 -0
  67. package/dist/matching-DF5F9Us8.d.ts +100 -0
  68. package/dist/matching-DF5F9Us8.d.ts.map +1 -0
  69. package/dist/matching.d.ts +2 -0
  70. package/dist/matching.js +172 -0
  71. package/dist/matching.js.map +1 -0
  72. package/dist/model-B6zk1NMB.d.ts +710 -0
  73. package/dist/model-B6zk1NMB.d.ts.map +1 -0
  74. package/dist/model.d.ts +2 -0
  75. package/dist/model.js +1308 -0
  76. package/dist/model.js.map +1 -0
  77. package/dist/mutation.d.ts +2 -0
  78. package/dist/mutation.js +612 -0
  79. package/dist/mutation.js.map +1 -0
  80. package/dist/optimistic-CECsfNGY.d.ts +523 -0
  81. package/dist/optimistic-CECsfNGY.d.ts.map +1 -0
  82. package/dist/optimistic.d.ts +2 -0
  83. package/dist/optimistic.js +336 -0
  84. package/dist/optimistic.js.map +1 -0
  85. package/dist/outbox-DNuc4kZp.d.ts +259 -0
  86. package/dist/outbox-DNuc4kZp.d.ts.map +1 -0
  87. package/dist/outbox.d.ts +2 -0
  88. package/dist/outbox.js +326 -0
  89. package/dist/outbox.js.map +1 -0
  90. package/dist/patchValues.d.ts +10 -0
  91. package/dist/patchValues.d.ts.map +1 -0
  92. package/dist/patchValues.js +57 -0
  93. package/dist/patchValues.js.map +1 -0
  94. package/dist/persistence-CCMnvg7-.d.ts +114 -0
  95. package/dist/persistence-CCMnvg7-.d.ts.map +1 -0
  96. package/dist/persistence.d.ts +2 -0
  97. package/dist/persistence.js +205 -0
  98. package/dist/persistence.js.map +1 -0
  99. package/dist/placement-jZdsIpmj.d.ts +50 -0
  100. package/dist/placement-jZdsIpmj.d.ts.map +1 -0
  101. package/dist/placement.d.ts +2 -0
  102. package/dist/placement.js +116 -0
  103. package/dist/placement.js.map +1 -0
  104. package/dist/plan-D9cftxC_.d.ts +35 -0
  105. package/dist/plan-D9cftxC_.d.ts.map +1 -0
  106. package/dist/plan.d.ts +2 -0
  107. package/dist/plan.js +100 -0
  108. package/dist/plan.js.map +1 -0
  109. package/dist/policy-DsWHmIAq.d.ts +42 -0
  110. package/dist/policy-DsWHmIAq.d.ts.map +1 -0
  111. package/dist/policy.d.ts +2 -0
  112. package/dist/policy.js +32 -0
  113. package/dist/policy.js.map +1 -0
  114. package/dist/{port.d.mts → port.d.ts} +3 -2
  115. package/dist/port.d.ts.map +1 -0
  116. package/dist/{port.mjs → port.js} +3 -2
  117. package/dist/port.js.map +1 -0
  118. package/dist/query-BuIU93xA.d.ts +141 -0
  119. package/dist/query-BuIU93xA.d.ts.map +1 -0
  120. package/dist/query.d.ts +2 -0
  121. package/dist/query.js +0 -0
  122. package/dist/query.js.map +1 -0
  123. package/dist/record.d.ts +11 -0
  124. package/dist/record.d.ts.map +1 -0
  125. package/dist/record.js +21 -0
  126. package/dist/record.js.map +1 -0
  127. package/dist/relation-BLBGOZom.d.ts +59 -0
  128. package/dist/relation-BLBGOZom.d.ts.map +1 -0
  129. package/dist/relation.d.ts +2 -0
  130. package/dist/relation.js +93 -0
  131. package/dist/relation.js.map +1 -0
  132. package/dist/remoteData-mK1suYi1.d.ts +148 -0
  133. package/dist/remoteData-mK1suYi1.d.ts.map +1 -0
  134. package/dist/remoteData.d.ts +2 -0
  135. package/dist/remoteData.js +179 -0
  136. package/dist/remoteData.js.map +1 -0
  137. package/dist/reporting.d.ts +12 -0
  138. package/dist/reporting.d.ts.map +1 -0
  139. package/dist/reporting.js +45 -0
  140. package/dist/reporting.js.map +1 -0
  141. package/dist/requirement-D57S2LpH.d.ts +64 -0
  142. package/dist/requirement-D57S2LpH.d.ts.map +1 -0
  143. package/dist/requirement.d.ts +2 -0
  144. package/dist/requirement.js +114 -0
  145. package/dist/requirement.js.map +1 -0
  146. package/dist/resume-Ct5R6yli.d.ts +92 -0
  147. package/dist/resume-Ct5R6yli.d.ts.map +1 -0
  148. package/dist/resume.d.ts +2 -0
  149. package/dist/resume.js +222 -0
  150. package/dist/resume.js.map +1 -0
  151. package/dist/retain-BfNTvBRb.d.ts +48 -0
  152. package/dist/retain-BfNTvBRb.d.ts.map +1 -0
  153. package/dist/retain.d.ts +2 -0
  154. package/dist/retain.js +107 -0
  155. package/dist/retain.js.map +1 -0
  156. package/dist/selection.d.ts +2 -0
  157. package/dist/selection.js +1 -0
  158. package/dist/shown.d.ts +20 -0
  159. package/dist/shown.d.ts.map +1 -0
  160. package/dist/shown.js +40 -0
  161. package/dist/shown.js.map +1 -0
  162. package/dist/sse-C7LhdbiQ.d.ts +17 -0
  163. package/dist/sse-C7LhdbiQ.d.ts.map +1 -0
  164. package/dist/sse.d.ts +2 -0
  165. package/dist/sse.js +127 -0
  166. package/dist/sse.js.map +1 -0
  167. package/dist/store-ah2qjcJW.d.ts +100 -0
  168. package/dist/store-ah2qjcJW.d.ts.map +1 -0
  169. package/dist/store.d.ts +2 -0
  170. package/dist/store.js +218 -0
  171. package/dist/store.js.map +1 -0
  172. package/dist/testing.d.ts +133 -0
  173. package/dist/testing.d.ts.map +1 -0
  174. package/dist/testing.js +0 -0
  175. package/dist/testing.js.map +1 -0
  176. package/dist/why-kgp_Wr3z.d.ts +483 -0
  177. package/dist/why-kgp_Wr3z.d.ts.map +1 -0
  178. package/dist/why.d.ts +2 -0
  179. package/dist/why.js +293 -0
  180. package/dist/why.js.map +1 -0
  181. package/dist/whyReasons.d.ts +42 -0
  182. package/dist/whyReasons.d.ts.map +1 -0
  183. package/dist/whyReasons.js +211 -0
  184. package/dist/whyReasons.js.map +1 -0
  185. package/dist/wire-DthPcTM6.d.ts +787 -0
  186. package/dist/wire-DthPcTM6.d.ts.map +1 -0
  187. package/dist/wire.d.ts +2 -0
  188. package/dist/wire.js +329 -0
  189. package/dist/wire.js.map +1 -0
  190. package/package.json +19 -14
  191. package/dist/index-BEvVAL76.d.mts +0 -4097
  192. package/dist/index-BEvVAL76.d.mts.map +0 -1
  193. package/dist/index.d.mts +0 -2
  194. package/dist/index.mjs +0 -1
  195. package/dist/port.d.mts.map +0 -1
  196. package/dist/port.mjs.map +0 -1
  197. package/dist/src-BDqn7Ocr.mjs +0 -6344
  198. package/dist/src-BDqn7Ocr.mjs.map +0 -1
package/README.md CHANGED
@@ -22,8 +22,10 @@ Remote fetches only the fields the Model lacks, stores each entity once, and
22
22
  hands the view one of six states rather than an `undefined`. Every result is a
23
23
  Message through one reducer, so a screen replays from a recorded Model.
24
24
 
25
- Remote is for facts the **server owns**. An edit the client authors, which
26
- must survive being offline, belongs to [`foldkit-sync`](../sync).
25
+ Remote is for facts the **server owns**. A write that must reach the server
26
+ even if the page goes first is a durable write, which Remote's outbox sends
27
+ later. An edit the client authors, which must converge with other devices'
28
+ edits while offline, belongs to [`foldkit-sync`](../sync).
27
29
 
28
30
  ## If you know TanStack Query, TanStack DB or fate
29
31
 
@@ -53,9 +55,11 @@ screen is a function of the Model, replays from a recording, and is tested
53
55
  without a network.
54
56
 
55
57
  What Remote does not have: a component hook (it has Surfaces and Foldkit
56
- Subscriptions), a devtools panel of its own (`Data.inspect` and `Data.why` are
57
- the data one would show), and retries on failure (a failed read stays failed
58
- until something asks again; see [When a read fails](#when-a-read-fails)).
58
+ Subscriptions), a devtools panel of its own (`Data.inspect`, `Data.why` and
59
+ `Data.timeline` are the data one would show), and retries of a failed read (it
60
+ stays failed until something asks again; see
61
+ [When a read fails](#when-a-read-fails)). Only a durable write is sent again
62
+ on its own, and only when nothing answered it.
59
63
 
60
64
  ### And TanStack DB, and fate
61
65
 
@@ -66,8 +70,9 @@ Its unit is the collection; a screen queries what is already local. Remote's
66
70
  unit is the field: a screen declares which fields of which entities it reads,
67
71
  and only those are fetched, so there is no collection to load first and a page
68
72
  carries what it shows. Remote has no live query engine: `Data.filtered` and
69
- `Remote.matching` judge the rows a list already holds, and a new list is a
70
- server query. Where TanStack DB syncs client-authored writes through a sync
73
+ `matching` judge the rows a list already holds, a new list is a server query,
74
+ and a list read `live` is kept current by the server's live source, not by
75
+ the client. Where TanStack DB syncs client-authored writes through a sync
71
76
  engine, that job here belongs to [`foldkit-sync`](../sync), and Remote stays
72
77
  the disposable cache of what the server owns.
73
78
 
@@ -89,13 +94,19 @@ what you are doing:
89
94
  | --- | --- |
90
95
  | See it work before there is a server | [Step 5](#5-provide-a-client) |
91
96
  | A read stays `Initial` | [Why is it still `Initial`?](#why-is-it-still-initial) |
97
+ | A read is `Loading` or `Refreshing` and you did not expect it | [Why is it `Loading`?](#why-is-it-loading) |
92
98
  | A list shows old rows | [Why is it stale?](#why-is-it-stale) |
93
99
  | A saved change showed, then vanished | [Why did my change disappear?](#why-did-my-change-disappear) |
94
- | Data was gone when I came back | [Why is it gone?](#why-is-it-gone-after-navigating-away) |
100
+ | A page I came back to reads `Refreshing` or `Loading` | [Why `Refreshing`?](#why-does-a-page-i-went-back-to-read-refreshing) |
101
+ | Ask the Model why a read shows what it shows | [`Data.why`](#why-is-it-loading) |
102
+ | Test that a list never blinks to `Loading`, without a browser | [`Trace`](#testing-what-a-reader-sees-trace) |
103
+ | Test that what a write shows is what the server stores | [`Law`](#testing-that-a-prediction-is-the-truth-law) |
95
104
  | Show a failed read, and let the user retry | [When a read fails](#when-a-read-fails) |
96
105
  | Load an ordered, paged list | [Queries and pagination](#queries-and-pagination) |
97
106
  | Filter a list already on screen | [Filtering a loaded list](#filtering-a-loaded-list-without-asking-the-server) |
98
107
  | Save, and show the result before the server answers | [Mutations](#mutations-and-optimistic-state) |
108
+ | Save a write that must reach the server across a reload or offline | [`Data.outbox`](#a-write-that-outlives-the-page-dataoutbox) |
109
+ | Keep a list current as the server changes it | [Live data](#live-data) |
99
110
  | Fetch outside an active screen | [Policies and prefetch](#reading-policies-and-prefetch) |
100
111
  | Keep data across a reload | [Hydration](#persistence-and-hydration) |
101
112
  | Hand a server render's data to the browser | [Server rendering](#server-rendering-remoteresume) |
@@ -109,6 +120,7 @@ One owner per datum:
109
120
  | --- | --- |
110
121
  | Route, selected item, local form state, transient UI errors | the application Model and `update` |
111
122
  | Server-derived facts that can be refetched | `foldkit-remote` |
123
+ | A write the server decides, which must be sent even after a reload | `foldkit-remote`, `durable` (its outbox) |
112
124
  | Client-authored state that must survive offline and converge | `foldkit-sync` |
113
125
  | Local state represented in the URL or another store | the Model, observed by `foldkit-mirror` |
114
126
 
@@ -117,6 +129,7 @@ Remote Sync
117
129
  server owns the fact client owns the edit
118
130
  cache is disposable intent must survive offline/restart
119
131
  refetch is recovery replay + reconciliation is recovery
132
+ outbox: send it later converge
120
133
  ```
121
134
 
122
135
  A Surface may project all of these owners at once. Observation does not transfer
@@ -308,12 +321,13 @@ while there is nothing to read.
308
321
 
309
322
  ```ts
310
323
  const detail = Data.active('ProjectDetail', model =>
311
- Option.map(model.openProjectId, id => Data.get(Project.select({ name: true }), id)),
324
+ Option.map(model.openProjectId, id => Data.get(Entity.select(Project, { name: true }), id)),
312
325
  )
313
326
  foldData.subscriptions({ detail })
314
327
  ```
315
328
 
316
- It belongs to the domain's application and sends no Messages. A domain bound
329
+ Its `projectionOf(model)` gives back that read, with its value's type. It
330
+ belongs to the domain's application and sends no Messages. A domain bound
317
331
  to a raw optic (`ModelRef.fromOptic`) names no application, so `Data.active`
318
332
  throws for one rather than guess.
319
333
 
@@ -351,8 +365,7 @@ const clientLayer = Layer.unwrap(
351
365
  Or, with no Effect RPC on the server, plain JSON over HTTP: each call a `POST`
352
366
  of `{ operation, payload }`, answered `{ result }` or `{ error }`.
353
367
  [`RemoteServer.answer`](../remote-server/README.md#over-plain-json) is the
354
- other end. `Remote.json(send)` is the same client over any other way of
355
- sending the request, a worker or a server running in the page:
368
+ other end:
356
369
 
357
370
  ```ts
358
371
  const overHttp = Remote.clientLayer(
@@ -360,53 +373,13 @@ const overHttp = Remote.clientLayer(
360
373
  )
361
374
  ```
362
375
 
363
- `headers` is read for each request, so a token that changes reaches the next
364
- call. A 401 fails every call kind, a read, a query, a mutation or a live
365
- stream, with `_tag: 'Unauthenticated'` (and `reason` when the server gave
366
- one): the server could not tell who is asking. `RemoteData.denied(error)`
367
- reads a failure as that or a 403 (`Forbidden`), typed, and as none for any
368
- other, so a view tells "sign in again" from "could not load" without
369
- comparing tags. Remote treats it as any failure: a read or query is `Failed`, and a live stream restarts with its
370
- backoff, refused again each time, until the application signs in again and
371
- calls `Remote.forget`. A 403 whose body does not
372
- say `forbidden` itself, as Remote's server's does, comes from middleware in
373
- front of it refusing a session (a bare `{ error }` included): it fails a
374
- mutation as `Forbidden` and any other call with the status. A 401 is read the
375
- same whatever its body says. Neither carries live data. For live data over the same `POST`s,
376
- `Remote.httpWithLive` keeps reads, queries, and mutations as `http` sends
377
- them and streams live requirements as a `text/event-stream` of changes;
378
- [`serveFetch`](../remote-server/README.md#over-fetch-workers) is the other
379
- end. Abandoning the stream aborts the request, which interrupts the
380
- subscription server-side.
381
-
382
- For a server in a worker or in the page (a sandbox), `foldkit-remote/port`
383
- speaks Effect's own RPC worker protocol over a `MessagePort`, so live data,
384
- typed errors and interruption cross it too.
385
- [`servePort`](../remote-server/README.md#over-a-messageport) is the other end:
386
-
387
- ```ts
388
- import { port } from 'foldkit-remote/port'
389
-
390
- // A new conversation each time the protocol connects: one port here, the other to the server.
391
- const overPort = port(() => {
392
- const channel = new MessageChannel()
393
- worker.port.postMessage({ open: 'remote' }, [channel.port2])
394
- return channel.port1
395
- })
396
- ```
397
-
398
- It is a `Layer<RemoteClient>` like the others; a port that cannot be set up is
399
- a defect, not a failed read.
400
-
401
- The stock client is accepted as it is. When its transport fails, the call
402
- fails with its own Remote error (`RemoteReadError`, `RemoteQueryError`,
403
- `RemoteMutationError`, `RemoteLiveError`), which the UI shows and retries like
404
- any other failure.
405
-
406
- Either is a Layer the runtime provides to `subscriptions` and Commands. The
376
+ Each is a Layer the runtime provides to `subscriptions` and Commands; the
407
377
  transport itself is not owned by Remote. See
408
378
  [`RemoteServer.memory`](../remote-server/README.md#a-backend-held-in-memory)
409
- for what the in-memory one can and cannot do.
379
+ for what the in-memory one can and cannot do, and
380
+ [Clients and transports](#clients-and-transports) for the others (a worker or
381
+ a `MessagePort`, live data over HTTP), what a refused or unreachable call
382
+ does, and testing a server over HTTP.
410
383
 
411
384
  ### 6. Watch it arrive
412
385
 
@@ -461,8 +434,11 @@ function update(model: Model, message: Message): Update.Return<Model, Message, R
461
434
  }
462
435
  ```
463
436
 
464
- The rest of this guide uses the fold. With the spread, read `Data.subscriptions`
465
- for `foldData.subscriptions` and `Data.mutate` for `foldData.mutate`.
437
+ The snippets below call the domain itself (`Data.subscriptions`,
438
+ `Data.mutate`, `Data.retry`, `Data.preload`), as the spread form does. Under
439
+ the fold, call the fold's members of the same names (`foldData.mutate`, …),
440
+ which yield the wrapper Message; the members that only read or write the Model
441
+ (`Data.get`, `Data.refresh`, `Data.overlay`, …) stay on the domain either way.
466
442
 
467
443
  ### One value with `foldkit-bundle`: `Data.wiring`
468
444
 
@@ -527,7 +503,8 @@ The states are:
527
503
  - **`Loading`** — required data is absent and a read, or a list's query, is in
528
504
  flight.
529
505
  - **`Ready`** — every selected field is present and decodes.
530
- - **`Refreshing`** — the current value remains visible while it is being refetched.
506
+ - **`Refreshing`** — the current value remains visible while it is being
507
+ refetched, or, for a list, while some of its rows are still being read.
531
508
  - **`Failed`** — stored server data does not decode against the Selection, the
532
509
  read or query behind it failed, or the server settled a selected field
533
510
  without a value (an `Unavailable` error; see
@@ -574,7 +551,8 @@ that failed does not throw away what the reader was already looking at. Only a
574
551
  `Failed` with nothing to show reaches `failed`.
575
552
 
576
553
  The `data` branch is told which it got, as a `Freshness` — exported, so a view
577
- helper that takes one can name it:
554
+ helper that takes one can name it, and folds it with its exhaustive
555
+ `Freshness.match`:
578
556
 
579
557
  | `Freshness` | What it means |
580
558
  | --- | --- |
@@ -605,22 +583,75 @@ spinner here hides a wiring mistake. Ask the Model:
605
583
 
606
584
  ```ts
607
585
  Data.why(model, projection, { surfaces }) // the record you give Data.subscriptions
608
- // { state: 'Initial', reason: 'NotFetching', surfaces: ['ProjectPage'],
609
- // message: 'ProjectPage reads it and is active, yet nothing is fetching it. …' }
586
+ // { state: 'Initial',
587
+ // because: [Why.NotFetching({ surfaces: ['ProjectPage'] }), Why.Fields({ … })],
588
+ // message: 'ProjectPage reads it and is active, yet nothing is fetching it. …',
589
+ // surfaces: Option.some(['ProjectPage']) }
610
590
  ```
611
591
 
592
+ The first reason is the one that decides `Initial`:
593
+
612
594
  - `NotObserved`: no active Surface reads it. The Surface is not in the record
613
- you gave `Data.subscriptions`, or its params are `undefined` for this Model
614
- (the route is not there yet).
615
- - `NotFetching`: an active Surface reads it and nothing started a request,
616
- which almost always means Remote's Subscriptions are not installed in the
617
- runtime.
618
- - Without `surfaces` it can only say `Unknown`. For every other state it says
619
- what the state means in words.
595
+ you gave `Data.subscriptions`, or its `Surface.at` function returns none for
596
+ this Model (the route is not there yet).
597
+ - `NotFetching`: an active Surface reads it and nothing started a request.
598
+ Right after a Message that is the moment before the read entry asks; if it
599
+ stays so, Remote's Subscriptions are not installed in the runtime.
600
+ - `NoActiveRecord`: no `surfaces` were given, so it cannot tell which.
620
601
 
621
602
  `Data.plan(model, projection)` shows whether Remote believes anything is
622
603
  missing at all.
623
604
 
605
+ ### Why is it `Loading`?
606
+
607
+ A list reads `Loading` when its page is missing and its query is out, and
608
+ also when it holds its page and one row's field is being fetched. It reads
609
+ `Refreshing` when the page is stale, a row is stale, or a row is left out
610
+ until a field arrives. `Data.why` tells them apart. `because` lists every
611
+ fact the Model holds about what the read reads, by typed reference, never by
612
+ the store's internal marks. The reason that decides the state comes first.
613
+ Match them with `Why.match`:
614
+
615
+ | `Why` | What it says | What restarts it |
616
+ | --- | --- | --- |
617
+ | `List { list, held, inFlight }` | the list's own page is `Missing` or `Stale` | the read entry, while a Surface reads it |
618
+ | `Fields { row, via, fields, held, inFlight }` | fields of one row are `Missing` or `Stale`; `via` is the relation path (`['owner']`) | the read entry |
619
+ | `ListFailed`, `FieldsFailed` | a request failed | **nothing on its own**: `Data.refresh`, a write of the field, a new page |
620
+ | `Unavailable`, `Undecodable`, `OverlayOnly` | the store holds what cannot be shown | a change of Selection or overlay |
621
+ | `Deleted { row, by }` | the server deleted it (`by` none), or a pending write does | — |
622
+ | `LiveReconnecting`, `LiveRefused` | a live stream of a Surface reading it broke, or was refused | the live entry, with backoff; nothing, for a refusal |
623
+ | `Pending { row, fields, by }` | a pending write shows these fields | its answer |
624
+ | `Joined`, `Left { list, row, by, terms }` | the list shows a row its pages do not hold, or hides one they do | its answer |
625
+
626
+ `by` is a `ShownBy`: `Write { requestId, mutation }`, `Overlay { id }` or
627
+ `Live { cursor }`. `terms` are the list body's `where` predicates judged over
628
+ the row as the client placed it, so a row that joined says which term put it
629
+ there, and one the client could not judge says what it lacked:
630
+
631
+ ```ts
632
+ // a project archived optimistically, in a list of active ones
633
+ Why.Left({
634
+ list: { query: 'ByStatus', identity },
635
+ row: { entity: 'Project', id: 'p1' },
636
+ by: Option.some(ShownBy.Write({ requestId: 'r7', mutation: Option.some('Archive') })),
637
+ terms: [{ term: 'Project.status = $status', truth: 'False', reason: Option.none() }],
638
+ })
639
+ ```
640
+
641
+ `truth` is `True`, `False`, `Null` (SQL's unknown, which keeps a row out) or
642
+ `Unknown` (the store lacks what it rests on). `ReadWhy` is a Schema:
643
+ `Schema.encodeSync(ReadWhy)(why)` is JSON for a log or a devtools client.
644
+ `Data.why` reads the Model and nothing else: it plans nothing and fetches
645
+ nothing.
646
+
647
+ **Across a history.** `Data.timeline(start, history, { feed, post },
648
+ { surfaces })` takes the Messages `update` handled, each with the Model it
649
+ left (what Foldkit's devtools records), and gives one step per change of a
650
+ read's tag: the Message that made it, `why` at the Model it changed to, and
651
+ `regression` when a read that showed a value stopped showing it with no
652
+ reason a reader can see. Foldkit's devtools has no place for a panel of
653
+ another package's, so the timeline is the data, not a panel.
654
+
624
655
  ### Why is it stale?
625
656
 
626
657
  Under the default `RemotePolicy.cacheFirst`, a present field is never asked for
@@ -634,8 +665,8 @@ server said last time. To see change:
634
665
  older than `maxAge` the next time it is planned.
635
666
  - **Always**: `RemotePolicy.networkOnly` requests every selected field, cached
636
667
  value visible meanwhile.
637
- - **As it happens**: `Data.live` instead of `Data.get`, with a server that
638
- publishes changes; see [Live data](#live-data).
668
+ - **As it happens**: `Data.live` instead of `Data.get`, or `live: true` on a
669
+ `Data.query`, with a server that publishes changes; see [Live data](#live-data).
639
670
 
640
671
  Connections have no age: a list re-queries when invalidated, refreshed, or
641
672
  under `networkOnly`. See [Reading, policies, and prefetch](#reading-policies-and-prefetch).
@@ -647,25 +678,42 @@ is in flight. When the server answers, the layers come off and the cache shows
647
678
  what the server said:
648
679
 
649
680
  - **It failed.** `MutationFailed` removes the layers; `Data.mutation(model,
650
- requestId)` reads `Failed` with the error. Nothing retries it: the mutation
651
- is not a durable queue, and if losing it is data loss the edit belongs to
681
+ requestId)` reads `Failed` with the error. Nothing retries it on its own
682
+ (`Data.retry` sends it again; see [When a write fails](#when-a-write-fails)).
683
+ A write survives a reload only when it is sent `durable`
684
+ ([`Data.outbox`](#a-write-that-outlives-the-page-dataoutbox)); an edit that
685
+ must converge with other devices' offline edits belongs to
652
686
  [`foldkit-sync`](../sync).
653
- - **It succeeded, but the server's result did not include the field.** The
654
- layer came off and the store still holds the old value, until a read or the
655
- result writes the new one. A mutation `Output` that returns the changed
656
- entity settles this at once.
687
+ - **It succeeded, but the answer did not name the row.** The answer carries
688
+ what the active Surfaces and the request's layer show of each row it names
689
+ ([What an answer carries](#what-an-answer-carries)); a row it changed
690
+ without naming (a post's count, after a like) keeps the old value until a
691
+ read writes the new one. Name the row in the answer.
657
692
  - **A preview** shown with `Data.overlay` stays until `Data.lift`; a settled
658
693
  mutation never takes it along.
659
694
 
660
695
  See [Mutations and optimistic state](#mutations-and-optimistic-state).
661
696
 
662
- ### Why is it gone after navigating away?
663
-
664
- Remote is a cache, and it forgets facts no active feature needs. Retention
665
- roots come from the active Surfaces; after the grace period, `RetentionChanged`
666
- collects everything they do not reach. A screen you left, and nothing else
667
- reads, loses its rows, which is safe because coming back refetches them. To
668
- keep something a screen does not read, name it in a retain entry or a
697
+ ### Why does a row leave its list for a frame after a mutation?
698
+
699
+ The answer left out fields the row showed while the request was pending. Once
700
+ the layer comes off, the store lacks them, so the read entry fetches them:
701
+ until they arrive, a list leaves the row out (and reads `Refreshing`), and a
702
+ read of that entity alone reads `Loading`. An answer carries what the active
703
+ Surfaces read and the layer showed of every row it names, so this is a row
704
+ the answer did not name, a field the server's read left out, or a server
705
+ that answers without the read (one older than the client, or whose read
706
+ failed). `Remote.make({ verifyPredictions: true })` names each one: see
707
+ [Checking an answer against what was shown](#checking-an-answer-against-what-was-shown).
708
+
709
+ ### Why does a page I went back to read `Refreshing`?
710
+
711
+ Remote keeps what the screens you most recently left reached: the last 16
712
+ lists and 256 reads (`departedLimit`). A list left is held **stale**, so going
713
+ back shows its rows at once and reads `Refreshing` while the planner refetches
714
+ it; the page that answers replaces what was held. Only past those limits is a
715
+ screen's data collected, and then coming back reads `Loading` and refetches.
716
+ To keep something no screen reads, name it in a retain entry or a
669
717
  `connections` list; see [Retention](#retention-and-garbage-collection).
670
718
 
671
719
  ### Why does a value the server has read `Failed`?
@@ -780,6 +828,12 @@ const card = Data.get(ProjectCard, 'p1') // Projection<Model, RemoteData<{ name;
780
828
  A cursor is not part of the name: a page read from a cursor continues its page.
781
829
  The server refuses a request that pages one relation more than four ways, and
782
830
  a field's own name may not contain `@`.
831
+ - A `many` relation read whole, with no window, is bounded on the server:
832
+ `foldkit-remote-drizzle`'s `source` reads at most `maxCollection` children of
833
+ one row (100 unless its Source says), and fails the read, naming the
834
+ relation, when a row has more, since a whole list cannot say it was cut
835
+ short. The read then reads `Failed`. Read a collection that can grow without
836
+ limit through `Entity.page`.
783
837
 
784
838
  `Entity.make` with `Entity.ref` keeps working, and both kinds can share one
785
839
  domain. Both packages export `Entity`; a module that needs `Entity.from` beside
@@ -817,8 +871,10 @@ becomes `Refreshing` without discarding the old value. Planning accepts `now`
817
871
  as an input, so tests can control time. It defaults to `Date.now`, read at each
818
872
  use, so fake timers (`vi.useFakeTimers`) move it; Effect's `TestClock` does not,
819
873
  because a plan runs outside Effect, so a test that drives `TestClock` passes
820
- `now`. A mutation's answer is dated the same way, by `Data.mutate`'s `now`
821
- option.
874
+ `now`. A mutation's answer is dated by another clock: Effect's `Clock`, read
875
+ when the answer lands, so a test of how written rows age drives `TestClock`.
876
+ `Data.mutate`'s `now` option is the write's time, sent to the server; it does
877
+ not date the answer.
822
878
 
823
879
  A policy belongs to one `subscriptions` call, and covers every read in it. Reads
824
880
  under different policies take a call each:
@@ -924,20 +980,23 @@ of it.
924
980
  ### Refreshing from `update`
925
981
 
926
982
  To revalidate what a screen already declares — a refresh button, a focus
927
- regained — hand its Projection (or a Surface without params) to `Data.refresh`:
983
+ regained — hand `Data.refresh` the active record the wiring takes:
928
984
 
929
- ```ts
930
- case 'ClickedRefresh': {
931
- if (model.route._tag !== 'project') return { model }
932
- const page = ProjectPage.projection({ projectId: model.route.projectId })
933
- return { model: Data.refresh(model, page) }
934
- }
985
+ ```ts sketch
986
+ case 'ClickedRefresh':
987
+ return { model: Data.refresh(model, actives) }
935
988
  ```
936
989
 
990
+ Each entry is resolved for this Model as the read entries resolve it, so a
991
+ `Surface.at` page reads with the params the route gives it, and an inactive
992
+ entry asks for nothing. Add a Surface to `actives` and its refresh comes with
993
+ it. One entry (`Data.refresh(model, actives.project)`), a Projection, or a
994
+ Surface without params narrows it.
995
+
937
996
  `Data.refresh` performs no I/O and restates no request. It returns the Model
938
997
  with every selected field the store holds reading `Refreshing`, every entity it
939
- knew to be absent forgotten, so `NotFound` reads `Loading` and is asked for
940
- again, and every loaded connection invalidated; the `Data.subscriptions` read entries then refetch it,
998
+ knew to be absent asked for again (it reads `NotFound`, and stays out of every
999
+ list, until the answer says it exists), and every loaded connection invalidated; the `Data.subscriptions` read entries then refetch it,
941
1000
  since stale data is planned again under every policy, so the page is requested
942
1001
  once. The Projection must be observed, as it is while it is on screen; for data
943
1002
  nothing observes, use `Data.prefetch` with `RemotePolicy.networkOnly`.
@@ -948,6 +1007,25 @@ nothing observes, use `Data.prefetch` with `RemotePolicy.networkOnly`.
948
1007
  - A read already in flight restarts instead of landing after the refresh.
949
1008
  - Refreshing what is already refreshing, or nothing, returns the same Model.
950
1009
 
1010
+ `{ failed: true }` asks again only what failed: each entity read whose fields
1011
+ (or a related row's) failed, and each list whose query did. That is a "Try
1012
+ again": what loaded keeps its value, and a list keeps its later pages, which a
1013
+ whole refresh would replace with one page.
1014
+
1015
+ ```ts sketch
1016
+ case 'ClickedTryAgain':
1017
+ return { model: Data.refresh(model, actives, { failed: true }) }
1018
+ ```
1019
+
1020
+ One list can also be named by its query and input, with no select or window
1021
+ to restate: `Data.refresh(model, Everyone.ref({}))` invalidates that list for
1022
+ every window reading it. It marks no row's fields, since a ref names no
1023
+ selection; the refetched page carries what each read selects. That suits a
1024
+ list another page reads, after a write whose answer cannot say it changed: a
1025
+ list the Model never loaded is left alone, and one nothing observes now is
1026
+ fetched when it is next read. With `{ failed: true }` it is asked again only
1027
+ if its query failed. A query the domain does not register is refused.
1028
+
951
1029
  ### Forgetting everything: a change of principal
952
1030
 
953
1031
  Everything Remote knows, it knows for a principal: a value the server gave
@@ -956,7 +1034,7 @@ settled as unavailable, the rows of a list. Log in, log out, switch
956
1034
  organization, and none of it is trustworthy. `Data.forget` is the one
957
1035
  boundary:
958
1036
 
959
- ```ts
1037
+ ```ts sketch
960
1038
  case 'SignedOut': {
961
1039
  return { model: Data.forget({ ...model, session: Option.none() }) }
962
1040
  }
@@ -999,7 +1077,8 @@ const Data = Remote.make({
999
1077
  something the client should be able to state, declare it with a body instead:
1000
1078
 
1001
1079
  ```ts
1002
- import { Entity, Expr, Order, Query } from 'foldkit-entity'
1080
+ import { Entity, Expr, Order } from 'foldkit-entity'
1081
+ import { Query } from 'foldkit-remote'
1003
1082
 
1004
1083
  const Task = Entity.define(
1005
1084
  'Task',
@@ -1031,6 +1110,23 @@ can compile that body rather than be told the same thing again in its own
1031
1110
  dialect; one that would rather answer the query its own way still can, and a
1032
1111
  descriptor from `Query.make` has no body at all.
1033
1112
 
1113
+ **Which to declare.** A list the client should keep right after a write needs
1114
+ a body: the body is what lets the client place a new or changed row in the
1115
+ pages it holds instead of fetching the list again. A `Query.make` list is
1116
+ invalidated by any change to its Entity, so it is fetched again after every
1117
+ write that touches one. A body can follow relations
1118
+ (`Expr.path`, `Expr.exists`, a path from `Expr.viewer`), so a feed of the
1119
+ people you follow is a body too. Keep `Query.make` for a list whose meaning
1120
+ only the server can state, such as a search ranking.
1121
+
1122
+ When part of a filter is something a body cannot say, put what it can say in
1123
+ the body and the rest in the server's own `where`
1124
+ (`foldkit-remote-drizzle`'s `query(descriptor, { entity, where })`), which is
1125
+ conjoined with the body. The client judges rows by the body alone, so write
1126
+ the body to admit no row the client makes or moves that the server's `where`
1127
+ refuses: such a row is placed in the list and stays there until the list is
1128
+ read again.
1129
+
1034
1130
  The body is built **once**, when the query is declared. Inside it `input.ownerId`
1035
1131
  is a placeholder for the value the query will be given, not the value — there is
1036
1132
  nothing there yet to branch on. A condition that depends on what was passed is a
@@ -1062,8 +1158,14 @@ flat array. That matters when page 1 and page 3 are loaded but page 2 is not:
1062
1158
  Remote represents an honest gap instead of pretending the visible rows are
1063
1159
  adjacent.
1064
1160
 
1065
- A query Projection is `Initial` until the page **and every selected field of its
1066
- visible items** are present. The `select` travels with the query: the server
1161
+ A query Projection is `Initial` until the page and the selected fields of at
1162
+ least one visible item are present. Then it shows the rows it can assemble: one
1163
+ whose fields have not arrived yet (a live insert, a row paged in without its
1164
+ fields) is left out, and the list reads `Refreshing` until it is, so the rows
1165
+ already shown stay on screen. A deleted row is left out too, so a list whose
1166
+ shown rows were all deleted shows none (`Refreshing` while others are read),
1167
+ never `Loading`. It reads
1168
+ `Ready` once every visible item is assembled. The `select` travels with the query: the server
1067
1169
  returns the selected fields of the page's items with the edges, and one
1068
1170
  `ConnectionMerged` merges the page and writes them, so a fresh list usually
1069
1171
  lands `Ready` in one response. What the server leaves out (withheld fields,
@@ -1115,6 +1217,12 @@ empty-and-complete and empty-and-partial stay different answers — one means
1115
1217
  answer (a search holding NUL, which the server refuses too) gives no items and
1116
1218
  `complete: false`, rather than throwing in `update`.
1117
1219
 
1220
+ A body that reads other rows (`Expr.path(Member.relations.team,
1221
+ Team.fields.name)`, a path from the viewer) is judged over the whole store: a
1222
+ row whose path the store can follow is judged, and one whose path reaches a row
1223
+ or field the store lacks is not judged either way, so the answer is
1224
+ `complete: false`.
1225
+
1118
1226
  Nothing is created: no connection, nothing new to retain, nothing new to fetch.
1119
1227
  The server stays authoritative for which rows exist.
1120
1228
 
@@ -1122,13 +1230,14 @@ The server stays authoritative for which rows exist.
1122
1230
 
1123
1231
  A connection's rows are edges the server delivered, and a query body describes
1124
1232
  *which rows* — so until something evaluates the body on the client, nothing here
1125
- can say whether a row belongs to a query. `Remote.matching` says:
1233
+ can say whether a row belongs to a query. `matching`, exported by
1234
+ `foldkit-remote`, says:
1126
1235
 
1127
1236
  ```ts
1128
- const judged = Remote.matching(Data.storeOf(model), ProjectsByOwner, { ownerId: 'u1' })
1237
+ const judged = matching(Data.storeOf(model), ProjectsByOwner, { ownerId: 'u1' })
1129
1238
 
1130
1239
  judged.matched // keys satisfying the body, in the order it asks for
1131
- judged.skipped // keys held, but missing a field the body reads
1240
+ judged.skipped // keys held, but not judged either way
1132
1241
  ```
1133
1242
 
1134
1243
  It is pure, and it runs the **same interpreter the server checks itself
@@ -1145,7 +1254,9 @@ keys — so it is in the shape. A caller that knows it holds the whole populatio
1145
1254
  complete answer.
1146
1255
 
1147
1256
  **`skipped` is the honest half.** A row missing a field the body reads cannot be
1148
- judged: it is neither a match nor a non-match. Dropping it silently would turn
1257
+ judged: it is neither a match nor a non-match. Nor can a row holding a value no
1258
+ interpreter answers for (text with a NUL); each row is judged alone, so one such
1259
+ row is skipped and the others are still answered. Dropping it silently would turn
1149
1260
  "I could not tell" into "no", so it is named instead, and the caller decides
1150
1261
  whether to fetch the field, ask the server, or say the answer is partial.
1151
1262
 
@@ -1238,8 +1349,10 @@ again every time some unrelated read restarted the entry. What failed is left
1238
1349
  out of the plan, and everything else is still fetched: the rows a failed list
1239
1350
  holds, the other fields of an entity. What asks again:
1240
1351
 
1241
- - `Data.refresh(model, projection)` — the retry button. It works on something
1242
- that never loaded, too.
1352
+ - `Data.refresh(model, actives, { failed: true })` — the retry button. It
1353
+ works on something that never loaded, too. A read with nothing to show reads
1354
+ `Loading` while it is asked again, and `Failed` again if it fails again, so
1355
+ the button does not blink away.
1243
1356
  - The value arriving anyway: a page for a list, or a read, live patch or
1244
1357
  mutation result that writes the field.
1245
1358
  - The server invalidating a list over a live stream, or deleting the entity.
@@ -1305,7 +1418,7 @@ const RenameProject = Mutation.make('RenameProject', {
1305
1418
 
1306
1419
  A mutation starts from ordinary application `update`:
1307
1420
 
1308
- ```ts
1421
+ ```ts sketch
1309
1422
  case 'ClickedRename': {
1310
1423
  const { id, name } = message
1311
1424
 
@@ -1331,20 +1444,80 @@ case 'ClickedRename': {
1331
1444
  `Remote.patch(entity, id, values)` builds one entity's patch, and
1332
1445
  `Remote.ref(entity, id)` names one row. Both take the entity you declared,
1333
1446
  whether with `foldkit-entity` or Remote's `Entity.make`, and check `values`
1334
- against its fields in their wire shape: a relation is written as its ref key,
1335
- such as `'User:u1'`.
1447
+ against its fields in their wire shape, except a relation, which is a ref:
1448
+ `owner: Remote.ref(User, 'u1')`, an array of refs for a `many`, or `null`
1449
+ where it is optional. The patch holds the ref key the store keeps. A ref key
1450
+ typed by hand (`'User:u1'`) or a ref to another entity is refused, by the type
1451
+ and when the patch is made.
1336
1452
 
1337
1453
  Those settlement Messages go through `Data.reduce` like every other Remote fact.
1338
1454
  The request id comes from the Remote Model's sequence, so `update` stays pure.
1339
1455
  Settling is idempotent per `requestId`.
1340
1456
 
1457
+ **Answers settle in the order their requests started, not the order they
1458
+ arrive.** The server applied the requests in that order, so an answer does not
1459
+ overwrite a field, or clear a row's deletion, that a request started after it
1460
+ has already settled; and an answer's `deleted` does not delete a row a later
1461
+ request has written. Two renames answered out of order keep the second name,
1462
+ and an edit answered after the row's delete leaves it deleted. Reads and live
1463
+ patches are the server's word at the time they arrive and land as they come.
1464
+ The ledger keeps each pending request's place in `mutations.order`, and the
1465
+ place of the last answer to write each field in `mutations.settled`, only
1466
+ while an earlier request is still pending.
1467
+
1341
1468
  A mutation that deletes says so. The server's outcome carries
1342
1469
  `deleted: [{ entity, id }]`, and settling it tombstones those entities: they read
1343
1470
  `NotFound`, and they leave every connection and relation they were in, so the
1344
1471
  server names no list. Patches apply first, so an entity named both ways is
1345
1472
  deleted; a retry of the same request deletes nothing again.
1346
1473
 
1347
- **Which lists a mutation changed is worked out from its answer.** Each patch
1474
+ A delete can show before the answer, with no list named either.
1475
+ `Remote.tombstone(entity, id)` in `optimistic` is a pending tombstone: while
1476
+ the request is in flight the row reads `NotFound` and leaves every list and
1477
+ every relation that lists it, as a settled delete does. It is a layer like a
1478
+ patch, so the server's store is untouched (`Data.confirmed` still reads the
1479
+ row), a failure brings the row back everywhere, and the answer's `deleted`
1480
+ keeps it gone. A pending delete wins over every pending patch of its row.
1481
+
1482
+ ```ts
1483
+ Data.mutate(model, DeletePost, { id }, { optimistic: [Remote.tombstone(Post, id)] })
1484
+ ```
1485
+
1486
+ A pending patch shows a field before the server has sent it, but does not stand
1487
+ in for it: a read still fetches a field of a row the server holds that only a
1488
+ layer shows, so a failure that takes the layer away leaves the server's value,
1489
+ not `Initial`. A row only a request holds (one the store has never read, or
1490
+ read as absent: a pending create) is not fetched while the request is pending,
1491
+ since the server has nothing of it yet; once the answer lands, the fields it
1492
+ did not carry (a guard's `may`) are.
1493
+
1494
+ Optimistic entity patches are **layers over the base store**, not inverse
1495
+ patches. If multiple mutations overlap, the visible cache is recomputed as base
1496
+ plus the still-pending layers, so settling one does not require trying to undo
1497
+ its old value manually.
1498
+
1499
+ Connections can be optimistic too:
1500
+
1501
+ ```ts
1502
+ import { ConnectionChange } from 'foldkit-remote'
1503
+
1504
+ const { model: started, command } = Data.mutate(model, AddComment, input, {
1505
+ optimistic: ({ tempId }) => [
1506
+ Remote.patch(Comment, tempId, { id: tempId, body: input.body }),
1507
+ ConnectionChange.prepend(
1508
+ CommentsForPost.ref({ postId }),
1509
+ Remote.ref(Comment, tempId),
1510
+ ),
1511
+ ],
1512
+ })
1513
+ ```
1514
+
1515
+ A successful server result can replace the request's temporary connection
1516
+ overlays with confirmed ones in place.
1517
+
1518
+ ### Which lists a write moves
1519
+
1520
+ Which lists a mutation changed is worked out from its answer. Each patch
1348
1521
  says which row it wrote; the fields whose value differs from what the client
1349
1522
  held are what changed (an answer returning a whole row changes only what is
1350
1523
  new). Every loaded list over that Entity is judged by its query's body against
@@ -1371,6 +1544,10 @@ the store the answer left:
1371
1544
  it could answer with the rows before the write;
1372
1545
  - a list over a `Query.make` query, which declares no body, is invalidated by
1373
1546
  any change to its Entity;
1547
+ - a list whose body reads other rows (a path, an exists, a path from the
1548
+ viewer) is invalidated by a change to any member those read, or a deletion
1549
+ of such a row: a team renamed moves every member listed by its name. A
1550
+ change to the link a path starts from re-judges that row alone;
1374
1551
  - a row a list does not hold that changes a member the server's own read rule
1375
1552
  filters on (an owner, a team: declared on the Entity as `ReadFilter`, which
1376
1553
  `foldkit-guard` attaches) invalidates it: whether the row now belongs is the
@@ -1389,12 +1566,13 @@ does not run is judged by the server, as any row the client cannot judge is:
1389
1566
  `Data.filtered` answers it incomplete and a change to it refetches.
1390
1567
 
1391
1568
  **A placement can be checked against the server.** `Remote.make({ ...,
1392
- verifyPlacement: true })` places as usual, and also fetches each list it placed
1569
+ verifyPredictions: true })` places as usual, and also fetches each list it placed
1393
1570
  what the server said in (an answer, as above, or a read or a live patch, as
1394
1571
  below); when the server's first page arrives it is compared with the order
1395
- placed, and a disagreement is kept in `model.remote.placementMismatches` (and
1396
- `Data.inspect`): an order the page does not share, or a row placing took out
1397
- that the page still lists. A pending edit is not checked: the server does not have it
1572
+ placed, and a disagreement is a `PredictionMismatch` [diagnostic](#diagnostics)
1573
+ whose `part` is a `Placement`:
1574
+ an order the page does not share, or a row placing took out that the page
1575
+ still lists. A pending edit is not checked: the server does not have it
1398
1576
  yet. Turn it on in development and tests: a collation the client
1399
1577
  and the server disagree on then fails an assertion instead of misplacing a row
1400
1578
  for a reader.
@@ -1425,6 +1603,8 @@ shown.
1425
1603
  `Data.refresh` stays for what an answer cannot say, such as a server-side
1426
1604
  effect on rows it did not return.
1427
1605
 
1606
+ ### What became of a write
1607
+
1428
1608
  `Data.mutation(model, requestId)` reads what became of it from the Model:
1429
1609
  `Pending`, `Applied`, `Failed` with the error the server or transport gave, or
1430
1610
  `Unknown` for an id never started here (or settled so long ago it left the
@@ -1439,8 +1619,65 @@ operation, and a retry with the same `requestId` (`Data.mutate(model, M, input,
1439
1619
  can recognise the retry. After a reload the record of it is gone, and the same
1440
1620
  request id is a new operation.
1441
1621
 
1442
- A mutation can say what the server may refuse it with, as data rather than a
1443
- message to parse:
1622
+ ### When a write fails
1623
+
1624
+ A write that fails releases its optimistic layer, so its change disappears.
1625
+ Remote keeps the failure in the ledger, with the mutation and the input it was
1626
+ started with, until the application answers it. The application reads it from
1627
+ the Model; it keeps no list of its own:
1628
+
1629
+ ```ts sketch
1630
+ // The view: a notice for each post that did not send, with its text to give back.
1631
+ Data.failedWrites(model, CreatePost) // ReadonlyArray<{ requestId, mutation, input, error, refusal }>
1632
+
1633
+ // update
1634
+ case 'ClickedRetry':
1635
+ return Option.match(Data.retry(model, message.requestId), {
1636
+ onNone: () => ({ model }),
1637
+ onSome: started => ({ model: started.model, commands: [started.command] }),
1638
+ })
1639
+ case 'ClickedDiscard':
1640
+ return { model: Data.dismiss(model, message.requestId) }
1641
+ ```
1642
+
1643
+ - `Data.failedWrites(model)` lists every failed write, oldest first, and
1644
+ `Data.failedWrites(model, CreatePost)` only that mutation's, with its `input`
1645
+ and `refusal` typed by it. A write in flight or applied is not listed.
1646
+ - `Data.retry(model, requestId)` sends a failed write again as itself: the same
1647
+ input, request id and operation, with its optimistic change shown again. It
1648
+ is `None` for a write that is not failed, so a second press does nothing.
1649
+ - `Data.dismiss(model, requestId)` forgets it.
1650
+
1651
+ Only a write in flight can fail: a `MutationFailed` repeated, or arriving after
1652
+ the write applied, was dismissed or was settled by `forget`, returns the Model
1653
+ it was given, as a repeated `MutationSucceeded` does.
1654
+
1655
+ A failed write stays listed until one of those, or `Data.forget` at a change of
1656
+ principal. Nothing drops one on a timer: that would hide a failure the reader
1657
+ has not seen. The ledger is bounded (the last 1024 failures), and is never
1658
+ persisted, so a reload forgets it.
1659
+
1660
+ To act on a failure in `update` (log it, move focus, put text back), have the
1661
+ wiring share it. Remote reduces it first, then the application's own update
1662
+ receives it with a Model that already lists it:
1663
+
1664
+ ```ts
1665
+ Page.assemble(Data.wiring(actives, { shared: ['MutationFailed'] }))
1666
+ // own update: MutationFailed is now one of its Messages, so its match must answer it
1667
+ ```
1668
+
1669
+ A wiring that does not share it records each failed write as a `WriteUnheard`
1670
+ [diagnostic](#diagnostics), which by default logs it: the change went, and
1671
+ nothing in the application's update was told.
1672
+
1673
+ **Refused for who asks.** A server may also refuse who is asking, whatever the mutation
1674
+ (`RemoteServer.forbid`). Then `Data.mutation(model, requestId)` is `Failed`
1675
+ with an error of `_tag: 'Forbidden'`, and `reason` when the server gave one,
1676
+ for the person who asked. Asking again as the same person is refused again,
1677
+ so show it rather than retry.
1678
+
1679
+ **Refusals as data.** A mutation can say what the server may refuse it with,
1680
+ as data rather than a message to parse:
1444
1681
 
1445
1682
  ```ts
1446
1683
  const SetPrice = Mutation.make('SetPrice', {
@@ -1452,17 +1689,115 @@ const SetPrice = Mutation.make('SetPrice', {
1452
1689
  Data.refusal(model, requestId, SetPrice) // Option<{ _tag: 'Field', key: 'price', reason: string } | { _tag: 'Conflict' }>
1453
1690
  ```
1454
1691
 
1455
- A mutation can also be a declared write: `Mutation.write(name, write)` over a
1692
+ `Refusal.field(key, reason)` names an input key and the Schema of why;
1693
+ `Refusal.conflict` says the row moved on, and `Refusal.unmet` that it does not
1694
+ hold what a [declared write](#declared-writes-what-the-client-predicts)'s
1695
+ `when` asks of it. A `Mutation.write` is refused as unmet only when its write
1696
+ has a `when`, and its `Refusal` type says so: without one it stays `Conflict`,
1697
+ so a match over it needs no `Unmet` arm. `Refusal.taken` says an insert's id
1698
+ is already a row's: its `entity` and `id`, the caller's own, and nothing of the
1699
+ row, which may be hidden from them. A `Mutation.write` over `Write.insert` is
1700
+ refused as taken and never as a conflict, since an insert expects no revision.
1701
+ `Refusal.invalid` says the row it
1702
+ would leave breaks an invariant of its Entity (`Entity.invariant`): its
1703
+ `entity` and `rule` (an id is unique only within its Entity, so the two name
1704
+ it) and its `path`, which a form shows the `message` beside, or none and empty
1705
+ when the invariant reads something the caller may not see. Every `Mutation.write` may be
1706
+ refused so, since the server's Entity may hold invariants the client's does not
1707
+ show. `Refusal.isInvalidBy(refusal, invariant)` asks whether it names one
1708
+ invariant, by its Entity and its id, given as `Entity.invariant` returned it
1709
+ (export it beside the Entity), so its id is never spelled again; an unnamed
1710
+ refusal matches none, and so does another Entity's invariant of the same id. The
1711
+ server sends the refusal encoded by this schema and `Data.refusal` decodes it
1712
+ by the same one; it is none while the mutation is pending or applied, or when
1713
+ it failed for another reason.
1714
+
1715
+ ### Declared writes: what the client predicts
1716
+
1717
+ A mutation can be a declared write: `Mutation.write(name, write)` over a
1456
1718
  `foldkit-entity` `Write.update`, `Write.insert` or `Write.delete`. Its input is
1457
1719
  the write's, it answers nothing of its own, and `Data.mutate` shows an update's
1458
1720
  or an insert's row until the server answers, so it needs no `optimistic` list;
1459
- a delete shows nothing until its answer removes the row everywhere. An update's
1721
+ a delete shows its row deleted, as `Remote.tombstone` does. An update's
1460
1722
  `increment` is shown as a prediction: its amount is added to the value the store
1461
1723
  holds whenever the row is read, so a change that reaches the store meanwhile is
1462
- added to rather than hidden, and two pending increments add up. A field the
1463
- client has not read is not guessed; it shows the server's sum when the answer
1464
- comes. The prediction is never authority: the server decides, and its answer
1465
- replaces it. The [declared writes guide](../../docs/declared-writes.md) follows
1724
+ added to rather than hidden, and two pending increments add up. A computed
1725
+ field is predicted the same way, by `Remote.make`'s `operators` over the row as
1726
+ it is read (`Write.apply`), so a live change to what it reads changes the
1727
+ prediction. So is a count another registered Entity derives over the written
1728
+ row's relation (`likeCount: Derived.count(Like.relations.post)`): inserting a
1729
+ Like adds one to the post it points at, deleting a Like the client holds takes
1730
+ one away from the post it pointed at, and pointing it elsewhere does both, each
1731
+ added as an increment is. Where the client does not hold where the row pointed,
1732
+ or holds no count on that post, nothing is predicted.
1733
+
1734
+ A write shows what its Entity sets from the context (`Entity.sets`) as well,
1735
+ which no input sets: `Expr.viewer` as the viewer `Remote.make` is told of, and
1736
+ `Expr.now` as `Data.mutate`'s `now`, the time the reader acted. An insert sets
1737
+ every such member, an update those set `on: 'write'` (an `editedAt`). A new
1738
+ row's counts read 0 and its exists none.
1739
+
1740
+ `update` reads no clock, so the time is read where the action is, by a
1741
+ Command, and carried in the Message that starts the write. `Remote.now(toMessage)`
1742
+ is that Command; it reads Effect's `Clock`, so a test pins it:
1743
+
1744
+ ```ts sketch
1745
+ // the action asks for the time…
1746
+ Posted: ({ post }) => ({ model, commands: [Remote.now(now => Message.Clocked({ post, now }))] }),
1747
+ // …and the Message it yields starts the write with it
1748
+ Clocked: ({ post, now }) => {
1749
+ const sent = Data.mutate(model, PostChirp, post, { now })
1750
+ return { model: sent.model, commands: [sent.command] }
1751
+ },
1752
+ ```
1753
+
1754
+ The request sends the same number, and the server takes it as the row's time
1755
+ when it lies within its `clockSkew` of the server's own clock (60 seconds
1756
+ unless `RemoteServer.make` says), so the answer agrees with what was shown.
1757
+ A retry (`Data.retry`) sends it again. Without `now`, a time the write sets is
1758
+ not predicted: the row reads as missing it until the server's answer brings the
1759
+ server's time. A supplied member declared with a `predict` (`Derived.make(schema,
1760
+ { predict })`, as Guard's `may` is) is foreseen over the row the write leaves,
1761
+ the store's row with the write's values over it and a field the write moves
1762
+ unknown; where it foresees none, the row reads as missing it until the answer.
1763
+ An application that already mints the row's id in a Command can read the
1764
+ clock in the same one (`examples/reference`'s `NameNote`). The row then joins
1765
+ every loaded list whose body admits it, where the list's order puts it, while pending and once answered: a post
1766
+ set at its time goes to the top of a newest-first list. A list without a
1767
+ body, or one filtered by a read rule the client does not run, is the server's
1768
+ to place.
1769
+
1770
+ A viewer-relative member is predicted for the viewer `Remote.make` is told of.
1771
+ With a `Like` pointing at a `Post` and a `User` (`examples/chirp` declares the
1772
+ whole domain, its writes and its server):
1773
+
1774
+ ```ts sketch
1775
+ const Post = Social.Post.pipe(
1776
+ Entity.derived({
1777
+ liked: Derived.exists(Social.Like.relations.post, { viewer: Social.Like.relations.user }),
1778
+ }),
1779
+ )
1780
+ const Data = Remote.make({
1781
+ model: App.model.remote,
1782
+ entities: [User, Post, Like],
1783
+ mutations: [LikePost, Unlike],
1784
+ viewer: model => Option.some(Remote.ref(User, model.me)),
1785
+ })
1786
+ ```
1787
+
1788
+ `liked` is the id of the viewer's Like of the post, or none. Inserting a Like
1789
+ pointing at a post and at the viewer shows it on that post (where the post is
1790
+ held with `liked` read, and none yet); deleting a Like, or pointing it at
1791
+ another post or person, takes it from the post it is shown on. That post also
1792
+ says where the deleted Like pointed, so its count goes down with no Like row
1793
+ held: unliking is `Data.mutate(model, Unlike, { id })` with the id `liked`
1794
+ holds. The store holds one viewer's values, as it holds one principal's, so a
1795
+ change of viewer is a change of principal: `Data.forget`.
1796
+
1797
+ A field the client has not read, or a computation by an operator its table
1798
+ lacks, is not guessed; it shows the server's value when the answer comes. The
1799
+ prediction is never authority: the server decides, and its answer replaces
1800
+ it. The [declared writes guide](../../docs/declared-writes.md) follows
1466
1801
  one write from here to the statement that lands it. `keys` names the input keys
1467
1802
  an author changed: only those are shown, and the request carries them for the
1468
1803
  server to write; the input is still sent and validated whole.
@@ -1475,51 +1810,106 @@ const EditProject = Mutation.write(
1475
1810
  Data.mutate(model, EditProject, { id: 'p1', name: 'Apollo II', status: 'active' }, { keys: ['name'] })
1476
1811
  ```
1477
1812
 
1478
- `Refusal.field(key, reason)` names an input key and why; `Refusal.conflict`
1479
- says the row moved on, and `Refusal.unmet` that it does not hold what a declared
1480
- write's `when` asks of it. A `Mutation.write` is refused as unmet only when its
1481
- write has a `when`, and its `Refusal` type says so: without one it stays
1482
- `Conflict`, so a match over it needs no `Unmet` arm. `Refusal.invalid` says the
1483
- row it would leave breaks an invariant of its Entity (`Entity.invariant`): its
1484
- `rule` and `path`, which a form shows the `message` beside, or none and empty
1485
- when the invariant reads something the caller may not see. Every
1486
- `Mutation.write` may be refused so, since the server's Entity may hold
1487
- invariants the client's does not show. `Refusal.isInvalidBy(refusal, invariant)`
1488
- asks whether it names one invariant, given as `Entity.invariant` returned it
1489
- (export it beside the Entity), so its id is never spelled again; an unnamed
1490
- refusal matches none. The server sends the refusal encoded by this schema and
1491
- `Data.refusal` decodes it by the same one; it is none while the mutation is
1492
- pending or applied, or when it failed for another reason.
1813
+ ### Refused where it is made
1814
+
1815
+ Before it shows or sends a declared write, `Data.mutate` refuses the ones the
1816
+ client can already tell the server would refuse, in the order the server
1817
+ checks: who asks, then the row.
1818
+
1819
+ **A write the held `may` says no to.** A supplied member may say which write
1820
+ its value forbids (`Derived.make(schema, { forbids })`); Guard's `may` does:
1821
+ the row's `Update` or `Delete`, a written member's own rule, or a relation's
1822
+ `Link` or `Unlink`, where it reads `false` for the row as the store holds it.
1823
+ `Data.mutate` asks each such member it holds before it judges the invariants,
1824
+ as the server refuses who asks first, and answers `MutationFailed` with `_tag:
1825
+ 'Forbidden'` and the `explain`'s words as `reason`, showing and sending
1826
+ nothing. A `may` not held, or one that does not name the part, refuses nothing;
1827
+ a held `may` is the server's answer as of its read, so `Data.refresh` asks
1828
+ again.
1829
+
1830
+ `may` is a UI hint, and the server enforces every write. A `may` that says
1831
+ yes, stale after a change of role, foreseen for a row the client wrote, or
1832
+ edited in the page's Model, decides nothing: the request carries no `may`,
1833
+ the server checks the write by its rules as the request's principal, and a
1834
+ refusal comes back as `Forbidden`, which takes the shown change back. The
1835
+ client's refusal only saves the round trip for a no it already holds. See
1836
+ [`foldkit-guard`](../guard/README.md#what-may-tells-a-reader-and-what-it-does-not)
1837
+ for what `may` tells a reader.
1838
+
1839
+ **A write the client can tell is invalid.** `Data.mutate` judges a declared
1840
+ write's Entity invariants over the row it would leave, as the client holds it:
1841
+ the store's row with the write's values over it, a field it moves (an
1842
+ increment, a computed value) not held. Where one is definitely broken, it shows
1843
+ nothing and sends nothing; its Command answers `MutationFailed` at once with
1844
+ the very `Refusal.invalid` the server would send, so `Data.refusal`,
1845
+ `Data.failedWrites` and your `update` handle it as a server's. An invariant
1846
+ over a field the client lacks is undecided and refuses nothing: the server
1847
+ judges it. Passing `optimistic` yourself skips the check.
1848
+
1849
+ ### What an answer carries
1850
+
1851
+ When the answer comes, the request's layer comes off, so the answer must hold
1852
+ everything the layer showed. The client says what that is: `Data.mutate` sends,
1853
+ with the request, what every active Surface reads of each entity (each
1854
+ `Data.get`'s fields and each `Data.query`'s `select`, relation targets
1855
+ included), and every field the layer itself shows. The second matters when
1856
+ a write moves a count no Surface reads now: a note deleted on its own page
1857
+ lowers its author's `noteCount`, which only the notebook page reads, and
1858
+ without it the answer would leave the count as it was before the write. The
1859
+ server reads, of each row its answer names, the fields it
1860
+ shows that the answer left out, after the write and as any read is, and sends
1861
+ that read with the answer (`foldkit-remote-server`'s
1862
+ [What an answer carries](../remote-server/README.md#what-an-answer-carries)).
1863
+ The client writes it in the reduction that releases the layer:
1493
1864
 
1494
- A server may also refuse who is asking, whatever the mutation
1495
- (`RemoteServer.forbid`). Then `Data.mutation(model, requestId)` is `Failed`
1496
- with an error of `_tag: 'Forbidden'`, and `reason` when the server gave one,
1497
- for the person who asked. Asking again as the same person is refused again,
1498
- so show it rather than retry.
1499
-
1500
- Optimistic entity patches are **layers over the base store**, not inverse
1501
- patches. If multiple mutations overlap, the visible cache is recomputed as base
1502
- plus the still-pending layers, so settling one does not require trying to undo
1503
- its old value manually.
1504
-
1505
- Connections can be optimistic too:
1506
-
1507
- ```ts
1508
- import { ConnectionChange } from 'foldkit-remote'
1865
+ ```text
1866
+ answer (rows the write names) + read (what the Surfaces and the layer show of them) -> store, layer released
1867
+ ```
1509
1868
 
1510
- const { model: started, command } = Data.mutate(model, AddComment, input, {
1511
- optimistic: ({ tempId }) => [
1512
- Remote.patch(Comment, tempId, { id: tempId, body: input.body }),
1513
- ConnectionChange.prepend(
1514
- CommentsForPost.ref({ postId }),
1515
- Remote.ref(Comment, tempId),
1516
- ),
1517
- ],
1518
- })
1869
+ So a created post lands with the count, the `may` and the author's name the
1870
+ feed reads, though the write stored none of them, and the feed never reads
1871
+ `Loading` for it. There is nothing to configure: the Surfaces already say what
1872
+ is shown. A row the answer does not name is not read; the server names the
1873
+ rows a write changed. Each `Data.subscriptions` call registers what it reads, so
1874
+ a `Data.mutate` with none registered sends nothing to show.
1875
+
1876
+ ### Checking an answer against what was shown
1877
+
1878
+ When the answer comes, the request's layer comes off, and what the row shows
1879
+ is what the store holds. Two things can then differ from what was shown.
1880
+ `Remote.make({ ..., verifyPredictions: true })` settles every mutation as usual
1881
+ and records each as a `PredictionMismatch` [diagnostic](#diagnostics), its
1882
+ `part` naming which:
1883
+
1884
+ - **`Value`:** a field the layer showed that the answer carries another value
1885
+ of. The client's prediction and the server's write disagree: a count moved
1886
+ by two, a time from another clock, a hand-written `optimistic` patch that
1887
+ guessed. A field another pending request also shows is not judged, since
1888
+ that request's value shows before and after.
1889
+ - **`Gap`:** a field the layer showed that the answer left out, and the store
1890
+ does not hold. It is then missing: an active Surface that reads it fetches it
1891
+ again, and the row reads `Loading` meanwhile.
1892
+
1893
+ ```ts sketch
1894
+ // requestId: the one Data.mutate returned; mutation: its name
1895
+ { _tag: 'PredictionMismatch', part: { _tag: 'Value', requestId, mutation, entity: 'Post', id: 'p9', field: 'likeCount', shown: 3, answered: 4 }, … }
1896
+ { _tag: 'PredictionMismatch', part: { _tag: 'Gap', requestId, mutation, entity: 'Post', id: 'p9', fields: ['editedAt', 'author'] }, … }
1519
1897
  ```
1520
1898
 
1521
- A successful server result can replace the request's temporary connection
1522
- overlays with confirmed ones in place.
1899
+ Match on the part with `PredictionPart.$match`. `Law.check`
1900
+ ([below](#testing-that-a-prediction-is-the-truth-law)) is the same law, in a
1901
+ test, over every part rather than the ones an answer carries.
1902
+
1903
+ A row's shown fields are those the request's optimistic patch wrote, added
1904
+ to or computed (a declared write's own included), plus those the active
1905
+ Surfaces (every `Data.subscriptions` call) read of a row the request patched
1906
+ or put in a list. A field the store still holds (an older value, or another
1907
+ pending request's) is not reported, nor is a row the answer deleted, or a
1908
+ temporary id it replaced. A repeated answer is not checked again. It changes
1909
+ nothing but `diagnostics`, so turn it on in development and tests, and assert
1910
+ in a test that `Data.inspect(model).diagnostics` is empty. With [the answer's read](#what-an-answer-carries),
1911
+ a gap is a row the answer did not name: name it in the answer's `entities`
1912
+ (`Remote.patch(Post, id, {})` asks the server for all it shows of it).
1523
1913
 
1524
1914
  ### Showing a change nobody has made
1525
1915
 
@@ -1572,7 +1962,14 @@ capability that must not claim success before the server reflects it:
1572
1962
  ```ts
1573
1963
  Agent.when({
1574
1964
  projection: Data.confirmed(Data.get(ProjectSummary, projectId)),
1575
- predicate: (project, request) => project.name === request.name,
1965
+ // The projection reads a RemoteData: only a value the server holds can match.
1966
+ predicate: (project, request: { readonly name: string }) =>
1967
+ RemoteData.render(project, {
1968
+ loading: () => false,
1969
+ notFound: () => false,
1970
+ failed: () => false,
1971
+ data: value => value.name === request.name,
1972
+ }),
1576
1973
  })
1577
1974
  ```
1578
1975
 
@@ -1583,22 +1980,113 @@ Sync draws the same line over its replica, with the same words and a different
1583
1980
  mechanism: see [what a reader sees while a change is in
1584
1981
  flight](../../docs/state-model.md#what-a-reader-sees-while-a-change-is-in-flight).
1585
1982
 
1983
+ ### A write that outlives the page: `Data.outbox`
1984
+
1985
+ A like pressed on a train, a post written in a tunnel: the write must reach the
1986
+ server, but nothing needs to converge, since the server decides it once. Make
1987
+ it durable, and assemble the outbox beside `Data.wiring`:
1988
+
1989
+ ```ts
1990
+ const outbox = Data.outbox({
1991
+ key: model => `outbox:${model.userId}`,
1992
+ scope: model => model.userId,
1993
+ })
1994
+ const placements = Page.assemble(Data.wiring(actives), outbox)
1995
+
1996
+ // update: queued, not sent; its layer shows at once
1997
+ const sent = Data.mutate(model, PostChirp, post, { now, durable: true })
1998
+ return { model: sent.model, commands: [sent.command] }
1999
+
2000
+ // the view
2001
+ const { waiting } = Data.outboxStatus(model) // "3 changes waiting"
2002
+ ```
2003
+
2004
+ `outbox.restore(model)` is `persistence.restore`'s twin: resolve it as the
2005
+ runtime's `flags` and start `init` from it. It reads back what a reload left and
2006
+ mints the page's session, which names its writes; `Data.mutate` with `durable`
2007
+ before it has run throws.
2008
+
2009
+ ```text
2010
+ Data.mutate(…, { durable }) → queued in the Model, layer shown (its Command answers MutationQueued)
2011
+ outbox.save → the queue written to the store, merged with other tabs' → OutboxRead (stored)
2012
+ outbox.send → the oldest stored write sent, one at a time, by one tab → MutationSucceeded | MutationFailed
2013
+ reload → outbox.restore: the queue back, layers shown → sent again, same operation id, same now
2014
+ ```
2015
+
2016
+ - **Stored before sent.** A write is sent only once the store holds it, so a
2017
+ reload while it is on its way finds it and sends it again. Its request id is
2018
+ its operation id, so the writer's ledger answers the second send from its
2019
+ record: a durable Mutation needs a ledgered writer (`RemoteServer.write`, or
2020
+ remote-drizzle's `writer`), or a handler that answers by operation id.
2021
+ - **In order, one at a time.** A post and the like of it, made offline, reach
2022
+ the server in that order. A refused write, or one this version cannot read,
2023
+ does not hold back the rest.
2024
+ - **Sent again only when nothing answered.** A send that fails with
2025
+ `Unreachable` (the request did not go, or what came back was no Remote answer) keeps the
2026
+ write waiting and sends it again after 1 second doubling to 30 (`retry: {
2027
+ base, max }`), and as soon as the browser is `online`; each attempt is
2028
+ `OutboxDelayed`. A refusal, `Forbidden` or a server error settles it as
2029
+ `Failed`, as for any write: `Data.failedWrites`, `Data.retry` (queued again)
2030
+ and `Data.dismiss` (gone from the store too) work on it unchanged. A write
2031
+ still waiting cannot be dismissed: it may be on its way.
2032
+ - **One tab sends; every tab shows.** The store is shared by every tab on the
2033
+ key. A Web Lock picks the one that sends, which reads the store again first
2034
+ so a write another tab settled is not sent twice; a `BroadcastChannel` tells
2035
+ the others, which show each other's waiting writes and settle them with the
2036
+ sender's answer. Without Web Locks each tab sends what it holds.
2037
+ - **`now` is the reader's, still.** A stored write is sent with the time it was
2038
+ made at. Past the server's `clockSkew`, the server writes its own time and
2039
+ reports `ClockSkewed`, and the answer moves the row there: the bound that
2040
+ stops a client backdating rows cannot tell an honest wait from a forged one.
2041
+ - **After a deploy.** A stored write naming a Mutation this version lacks, or
2042
+ whose input no longer decodes, is kept as `Unreadable` in `outboxStatus`,
2043
+ never sent, reported once as `OutboxUnreadable`, and removed by
2044
+ `Data.dismiss`. A store that cannot be written reports `OutboxUnsaved`, and
2045
+ the writes are sent anyway.
2046
+ - **A change of principal** is `Data.forget` and another `key`, as for
2047
+ persistence: one person's waiting writes are never sent as another's. The
2048
+ page holds the queue it read for one `key` and `scope`; when either moves,
2049
+ it sends none of it and reads the new one's. A store holding another
2050
+ scope's waiting writes is not written over, so a `key` shared by
2051
+ principals reports `OutboxUnsaved` rather than losing them.
2052
+
1586
2053
  ### Remote mutation vs Sync operation
1587
2054
 
1588
- A Remote mutation requests the server now; its result updates a disposable
1589
- cache, and there is no durable outbox. If losing an unsent edit would be data
1590
- loss, it belongs to [`foldkit-sync`](../sync), not here (see [which state
1591
- belongs here](#which-state-belongs-here)). For the same reason, an optimistic
1592
- overlay is only a temporary view of server-owned data: do not persist one and
1593
- treat it as a queue of edits.
2055
+ A durable Remote write is still Remote's: the server decides it once, a
2056
+ refusal is final, and the client never reads its own effects before the server
2057
+ has answered. If edits must merge with other devices' edits made offline, or
2058
+ the application must build on its own unsent edits as if they were committed,
2059
+ that is [`foldkit-sync`](../sync)'s journal (see [which state belongs
2060
+ here](#which-state-belongs-here)). An optimistic overlay stays a temporary view
2061
+ of server-owned data: the outbox stores the operation, never the overlay as a
2062
+ queue of edits.
1594
2063
 
1595
2064
  ## Live data
1596
2065
 
1597
2066
  `Data.live` marks a Projection so active `Data.subscriptions` follow server
1598
- changes for it.
2067
+ changes for it. A list is marked by its read: `live: true` on `Data.query`
2068
+ follows the rows that join, change or leave it, with no row of its own to
2069
+ subscribe to:
2070
+
2071
+ ```ts
2072
+ Data.query(Notes, {}, { select: NoteRow, first: 50, live: true })
2073
+ ```
2074
+
2075
+ A Surface's live entry (`<key>.live`) subscribes the rows and the lists its
2076
+ reads mark live in one stream, sending each list as its read sends it (query,
2077
+ input, window, `select`). A list read live needs `first`, since a server reads
2078
+ it again for every change it checks; `Data.query` throws without one. The
2079
+ server admits a stream before it asks any live source: its lists' `first`
2080
+ count toward `maxIdsPerEntity` with the rows it follows, and it may carry at
2081
+ most `MAX_LIVE_QUERIES` (32) lists; past either it is refused
2082
+ (`RemoteLiveRefused`, below). A live
2083
+ source that watches lists (`foldkit-remote-drizzle`'s `pollLive`) diffs it;
2084
+ `RemoteServer.liveHub` watches none, so on a server with only a hub a live
2085
+ list changes only when read again.
1599
2086
 
1600
2087
  The Remote Model owns each stream's cursor. Live events carry monotonically
1601
- ordered cursors:
2088
+ ordered cursors, counting on from the cursor the stream was opened after (`0`
2089
+ for a new stream, so its first event is `1`):
1602
2090
 
1603
2091
  - an old/repeated cursor is a duplicate and is ignored;
1604
2092
  - the next cursor is applied;
@@ -1629,10 +2117,9 @@ break -> gap marked, restarts + 1 -> backoff
1629
2117
  - **A gap stays until the restart.** An in-order event that a broken stream
1630
2118
  still delivers is applied, but does not clear it. The event that arrived
1631
2119
  ahead was dropped, and only the refetch brings back what it said.
1632
- - **What a stream covers is its requirements.** A list the server watches
1633
- for it (a `ConnectionInvalidate` it sends) is the source's to re-send when
1634
- the stream opens; the cloudflare example's poll invalidates its list on
1635
- every open for this reason.
2120
+ - **What a stream covers is its requirements.** A list read live is the
2121
+ live source's to re-send when the stream opens: `pollLive` sends a
2122
+ `ConnectionInvalidate` for each list on every open for this reason.
1636
2123
 
1637
2124
  `Data.liveStatus(model, active)` says how what one active entry reads live is
1638
2125
  doing, for a view that shows it:
@@ -1643,15 +2130,26 @@ Match.value(Data.liveStatus(model, Watched)).pipe(
1643
2130
  Idle: () => 'not live',
1644
2131
  Live: () => 'live',
1645
2132
  Reconnecting: ({ attempt }) => `reconnecting (attempt ${attempt})`,
2133
+ Refused: ({ error }) => `not live: ${error.message}`,
1646
2134
  }),
1647
2135
  )
1648
2136
  ```
1649
2137
 
1650
2138
  `Reconnecting` lasts from the break until the stream has resubscribed, and
1651
- carries the error, which is none for a gap. `RemoteModel.streams` holds each
2139
+ carries the error, which is none for a gap. `Refused` is a stream the server
2140
+ would not open for what it asks (`RemoteLiveRefused`: more rows of one Entity
2141
+ than its `maxIdsPerEntity`, or more than 32 lists). Asking again would be refused again, so it is
2142
+ not restarted until the entry's live requirements change, which opens a new
2143
+ stream; a `LiveRefused` diagnostic records it once. `RemoteModel.streams` holds each
1652
2144
  stream's restarts, its breaks in a row and its last error. `forget` clears it
1653
2145
  with every other server-derived fact.
1654
2146
 
2147
+ The live ledger (`live`, `streams`, `gaps`) keeps the 256 streams most
2148
+ recently heard from. Retention cannot prune it by what is shown: its roots
2149
+ merge every Surface's requirements, and a stream is one Surface's. A stream
2150
+ dropped while still open has no cursor, so its next event, past `1`, is a
2151
+ gap: it restarts from the start and refetches what it covers.
2152
+
1655
2153
  ## Retention and garbage collection
1656
2154
 
1657
2155
  Remote is a cache, so it should be allowed to forget facts no active feature
@@ -1667,9 +2165,21 @@ reads; `connections` given to any call are kept the same way. A root keeps:
1667
2165
  - data touched by pending optimistic work.
1668
2166
 
1669
2167
  After the configured grace period, `RetentionChanged` enters the Model and the
1670
- pure Remote reducer collects unreachable cache data. An inactive Surface that is
1671
- not otherwise retained may therefore lose its cache, which is safe because the
1672
- server remains authoritative and the planner can refetch it.
2168
+ pure Remote reducer collects what no root reaches. The roots it keeps are the
2169
+ active ones and those of the Surfaces most recently left, up to
2170
+ `departedLimit` (16 lists and 256 entity reads, oldest dropped first), so going
2171
+ back to a page shows what it held rather than reading `Loading`:
2172
+
2173
+ ```text
2174
+ Surface left -> its lists held stale, its reads held
2175
+ Surface again -> rows read Refreshing; the planner refetches the stale list
2176
+ page arrives -> it replaces the held rows; Ready
2177
+ past the limit -> collected; going back reads Loading and refetches
2178
+ ```
2179
+
2180
+ A value nothing reached when retention ran, such as a prefetch no Surface
2181
+ showed, is still collected at once. Collecting is safe because the server
2182
+ remains authoritative and the planner can refetch.
1673
2183
 
1674
2184
  This is another important difference from Sync: **Remote recovery may discard
1675
2185
  and refetch; Sync recovery must preserve unsent client intent.**
@@ -1683,6 +2193,7 @@ cursors, optimistic layers, mutation bookkeeping, gaps, retention roots) is
1683
2193
  never in one.
1684
2194
 
1685
2195
  ```ts
2196
+ import { Option } from 'effect'
1686
2197
  import { RemotePersistence } from 'foldkit-remote'
1687
2198
 
1688
2199
  // A cache carried in the page: prefetch on the server, embed, then hydrate.
@@ -1693,8 +2204,11 @@ const text = RemotePersistence.dehydrate(
1693
2204
  { scope: userId },
1694
2205
  )
1695
2206
 
1696
- const restored =
1697
- RemotePersistence.hydrate(text, { scope: userId }) ?? RemotePersistence.emptySnapshot
2207
+ // None when the text is refused (another scope, version, or a corrupt text).
2208
+ const restored = Option.getOrElse(
2209
+ RemotePersistence.hydrate(text, { scope: userId }),
2210
+ () => RemotePersistence.emptySnapshot,
2211
+ )
1698
2212
 
1699
2213
  Data.reduce(model, {
1700
2214
  _tag: 'Hydrated',
@@ -1740,13 +2254,33 @@ const persistence = Data.persistence({
1740
2254
  const placements = Page.assemble(Data.wiring(actives), persistence)
1741
2255
  ```
1742
2256
 
1743
- The restore and the save each run as Subscriptions, when the key changes or the
1744
- cache does:
2257
+ The first restore belongs before the first frame, or a reload draws `Loading`
2258
+ until a Subscription brings the stored rows. `persistence.restore(model)` is
2259
+ that restore as an Effect: resolve it as the runtime's `flags`, which the
2260
+ runtime runs with its `resources` before `init`, and start `init` from the
2261
+ Model it gives (`examples/cloudflare/src/browser.ts` does):
2262
+
2263
+ ```ts
2264
+ Runtime.makeElement(
2265
+ placements.runtime({
2266
+ Model,
2267
+ Flags: Model,
2268
+ flags: persistence.restore(initial(actor)),
2269
+ initial: (model: Model) => ({ model }),
2270
+ // …
2271
+ }),
2272
+ )
2273
+ ```
2274
+
2275
+ After that the wiring restores only a key the Model is not restored from (a
2276
+ change of principal), and saves as the cache changes:
1745
2277
 
1746
2278
  ```text
1747
2279
  key -> restore -> Hydrated { from: key } -> restoredFrom = key -> save on each change
1748
2280
  ```
1749
2281
 
2282
+ Without `restore` in `init`, the wiring restores at start too, a frame late.
2283
+
1750
2284
  - **A restore keeps what the Model already holds.** It hydrates with
1751
2285
  `merge: 'preserve-existing'`, so a fact resumed from the server, or read since
1752
2286
  the page started, wins over the older snapshot.
@@ -1755,6 +2289,11 @@ key -> restore -> Hydrated { from: key } -> restoredFrom = key -> save on each c
1755
2289
  then saved under their key; `forget` clears it as the key changes. The
1756
2290
  `scope` check refuses a snapshot written for another reader; it cannot see
1757
2291
  facts that are only in memory.
2292
+ - **`forget` never brings back what it forgot.** It keeps `restoredFrom`, so
2293
+ a key that does not change (a sign-out that keeps the key) is not restored
2294
+ again; the emptied cache is saved over its snapshot after the debounce
2295
+ instead. A key that changes is restored as any new key is, so both
2296
+ `forget` alone and `forget` with a new key are safe at a change of principal.
1758
2297
  - **Nothing is saved before its own key is restored.** `Hydrated` records the
1759
2298
  key it came from in `model.remote.restoredFrom`. Until that key is restored,
1760
2299
  nothing is saved, so the empty store at startup cannot overwrite the
@@ -1854,12 +2393,92 @@ See [`foldkit-remote-server`](../remote-server) for Source and authorization
1854
2393
  rules, and [`examples/kitchen-sink`](../../examples/kitchen-sink) for the real
1855
2394
  server packages together.
1856
2395
 
2396
+ ## Clients and transports
2397
+
2398
+ Every client is a `Layer<RemoteClient>`, and Remote's semantics do not change
2399
+ with it: a read, a query, a write and a live stream mean the same over Effect
2400
+ RPC, JSON or a port. What differs is how each reports a request that did not
2401
+ get an answer.
2402
+
2403
+ `Remote.json(send)` is the JSON client over any way of sending the request
2404
+ text; `Remote.http(url, { headers, fetch })` is it over `fetch`. `headers` is
2405
+ read for each request, so a token that changes reaches the next call.
2406
+
2407
+ **Who is asking.** A 401 fails every call kind, a read, a query, a mutation or
2408
+ a live stream, with `_tag: 'Unauthenticated'` (and `reason` when the server
2409
+ gave one): the server could not tell who is asking. A 401 is read so whatever
2410
+ its body says. A 403 whose body does not say `forbidden` itself, as Remote's
2411
+ server's does, comes from middleware in front of it refusing a session (a
2412
+ bare `{ error }` included): it fails a mutation as `Forbidden` and any other
2413
+ call with the status. `RemoteData.denied(error)` reads a failure as
2414
+ `Unauthenticated` or `Forbidden`, typed, and as none for any other, so a view
2415
+ tells "sign in again" from "could not load" without comparing tags. Remote
2416
+ treats either as any failure: a read or query is `Failed`, and a live stream
2417
+ restarts with its backoff, refused again each time, until the application
2418
+ signs in again and calls `Data.forget`.
2419
+
2420
+ The JSON clients carry no live data. For live data over the same `POST`s,
2421
+ `Remote.httpWithLive` keeps reads, queries, and mutations as `http` sends
2422
+ them and streams live requirements as a `text/event-stream` of changes;
2423
+ [`serveFetch`](../remote-server/README.md#over-fetch-workers) is the other
2424
+ end. Abandoning the stream aborts the request, which interrupts the
2425
+ subscription server-side.
2426
+
2427
+ For a server in a worker or in the page (a sandbox), `foldkit-remote/port`
2428
+ speaks Effect's own RPC worker protocol over a `MessagePort`, so live data,
2429
+ typed errors and interruption cross it too.
2430
+ [`servePort`](../remote-server/README.md#over-a-messageport) is the other end:
2431
+
2432
+ ```ts
2433
+ import { port } from 'foldkit-remote/port'
2434
+
2435
+ // A new conversation each time the protocol connects: one port here, the other to the server.
2436
+ const overPort = port(() => {
2437
+ const channel = new MessageChannel()
2438
+ worker.port.postMessage({ open: 'remote' }, [channel.port2])
2439
+ return channel.port1
2440
+ })
2441
+ ```
2442
+
2443
+ It is a `Layer<RemoteClient>` like the others; a port that cannot be set up is
2444
+ a defect, not a failed read.
2445
+
2446
+ **Nothing answered.** The client `RpcClient.make(RemoteRpc)` builds is
2447
+ accepted as it is. When its transport fails, a read, a
2448
+ query or a live stream fails with its own Remote error (`RemoteReadError`,
2449
+ `RemoteQueryError`, `RemoteLiveError`), which the UI shows and retries like any
2450
+ other failure. A mutation fails with `RemoteUnreachable` (`_tag:
2451
+ 'Unreachable'`): nothing answered, so the server may or may not have run it.
2452
+ `RemoteData.isUnreachable(error)` asks that of a failure without comparing tags.
2453
+ `Remote.json`, `Remote.http` and `Remote.httpWithLive` say the same of a
2454
+ request they could not send, and of a response that is no Remote answer: a
2455
+ body that is not JSON, JSON that is not `{ result }` or `{ error }` (a
2456
+ gateway's `{}`), or a response with status 408, 429, 502, 503 or 504, which
2457
+ Remote's server never answers with. Each is a `RemoteJsonClient`, a
2458
+ `RemoteRpcClient` whose transport error is `RemoteUnreachable`. A mutation the
2459
+ server failed is a `RemoteMutationError`. The difference matters for a [durable write](#a-write-that-outlives-the-page-dataoutbox):
2460
+ an `Unreachable` one is sent again, a refused one is settled `Failed`.
2461
+
2462
+ **Testing a server over HTTP.** Call it as the application does: give
2463
+ `` Remote.http(`${origin}/remote`, { headers, fetch }) `` the server's address
2464
+ (miniflare's `(await mf.ready).origin` for a Worker) and call
2465
+ `FoldkitRemoteRead`, `FoldkitRemoteQuery` and `FoldkitRemoteMutate` on it, as
2466
+ `examples/cloudflare/src/client.ts` does over its local stack. To speak the
2467
+ wire yourself, each `POST` body is one of these, `version` being
2468
+ `REMOTE_PROTOCOL_VERSION`:
2469
+
2470
+ ```json
2471
+ { "operation": "read", "payload": { "version": 5, "requests": [{ "entity": "Post", "id": "p1", "fields": ["title"] }] } }
2472
+ { "operation": "query", "payload": { "query": "PostsByAuthor", "input": { "authorId": "a1" }, "window": { "first": 20 } } }
2473
+ { "operation": "mutate", "payload": { "requestId": "r1", "mutation": "RenamePost", "input": { "id": "p1", "title": "Hello" } } }
2474
+ ```
2475
+
1857
2476
  ## Introspection
1858
2477
 
1859
2478
  Remote exposes its decisions as data rather than requiring DevTools to reach
1860
2479
  into private state:
1861
2480
 
1862
- ```ts
2481
+ ```ts sketch
1863
2482
  Data.inspect(model) // serializable cache/domain summary
1864
2483
  Data.plan(model, projection) // the entity plan a read would execute
1865
2484
  Data.meta(model, projection) // when what is shown was received, stale/loading
@@ -1886,6 +2505,52 @@ asked. That is deliberate. Runtime activity is not a second source of truth
1886
2505
  here, and a view that rendered from it would stop being reproducible from the
1887
2506
  Model.
1888
2507
 
2508
+ ### Diagnostics
2509
+
2510
+ What Remote meets and keeps going past is recorded in the Model, typed, as
2511
+ `model.remote.diagnostics` (and `Data.inspect(model).diagnostics`), oldest
2512
+ first, the newest 256 kept:
2513
+
2514
+ | `_tag` | When |
2515
+ | --- | --- |
2516
+ | `PredictionMismatch` | Under `verifyPredictions`, an answer disagreed with what the client predicted: a field's value (`Value`), a list's order (`Placement`), or a field left out (`Gap`). |
2517
+ | `WriteUnheard` | A write failed, and the wiring does not share `MutationFailed`. |
2518
+
2519
+ Each carries its facts, `source: 'foldkit-remote'`, a `level` and a `message`
2520
+ to log. The reducer only records them, since it is pure. The wiring's route and
2521
+ `Remote.fold` report what each update recorded, with a Command that hands them
2522
+ to the `Diagnostics.Reporter` in the runtime's Effect context and answers
2523
+ `CompletedReportDiagnostics`, which changes nothing. With no reporter given,
2524
+ that is Effect's log in the browser's console: `console.error` for an `error`
2525
+ diagnostic, `console.log` for a `warning`, as all of Remote's are. An
2526
+ application that reduces Remote's Messages by hand (`Data.reduce`) has them in the Model, and
2527
+ nothing reports them.
2528
+
2529
+ `Diagnostics` is `foldkit-surface`'s, re-exported here; server packages report
2530
+ into the same channel. Another reporter goes in the runtime's resources:
2531
+
2532
+ ```ts
2533
+ const resources = Layer.mergeAll(
2534
+ Remote.clientLayer(Remote.http('/remote')),
2535
+ // Logged as by default, and sent on to the server.
2536
+ Layer.succeed(Diagnostics.Reporter, diagnostic =>
2537
+ Effect.andThen(
2538
+ Diagnostics.log(diagnostic),
2539
+ Effect.sync(() => navigator.sendBeacon('/diagnostics', diagnostic.message)),
2540
+ ),
2541
+ ),
2542
+ )
2543
+ ```
2544
+
2545
+ In a test, assert on the Model, `[]` included:
2546
+
2547
+ ```ts
2548
+ expect(Data.inspect(model).diagnostics).toEqual([])
2549
+ ```
2550
+
2551
+ Nothing a diagnostic records changes what Remote does: a read, a write and a
2552
+ placement behave the same whether one is recorded or not.
2553
+
1889
2554
  ### Explaining one query read
1890
2555
 
1891
2556
  A query read is assembled from more pieces than it looks like: a definition, an
@@ -1935,6 +2600,115 @@ There is still no **executor**, because what answers a query is a
1935
2600
  purity that lets this be replayed from a recorded Model is what keeps it out of
1936
2601
  reach.
1937
2602
 
2603
+ ## Testing what a reader sees: `Trace`
2604
+
2605
+ "A list on screen never blinks to `Loading`" depends on the reducer, the read
2606
+ entries and the order answers land in. `foldkit-remote/testing` checks it in
2607
+ Vitest, with no browser. `Trace.run` runs your `update` and the domain's own
2608
+ Subscriptions from a Model. It **holds** every request they make. Each step is
2609
+ one of your Messages, or a `Respond` that lets one held request go on, answered
2610
+ by the `RemoteClient` you pass. Every Model in between is recorded, so a
2611
+ regression that lasts one Message cannot fall between samples.
2612
+
2613
+ ```ts
2614
+ import { Respond, Trace } from 'foldkit-remote/testing'
2615
+
2616
+ const trace = await Trace.run(Data, {
2617
+ start: loaded, // a Model showing the list
2618
+ update,
2619
+ active,
2620
+ client: server, // a fake, or your server's in-process client
2621
+ steps: [
2622
+ Message.Created({ id: 'p2', name: 'Two' }),
2623
+ Respond.mutation(Create), // the answer lands first
2624
+ Respond.read(Project, 'p2'), // then the new row's fields
2625
+ ],
2626
+ })
2627
+
2628
+ expect(Trace.regressions(trace, { list })).toEqual([])
2629
+ expect(Trace.tags(trace, list)).toEqual(['Ready', 'Refreshing', 'Ready'])
2630
+ ```
2631
+
2632
+ - **The trace decides when; the client decides what.** `Respond.query(ref)`,
2633
+ `Respond.read(Entity, id)` and `Respond.mutation(M)` each answer the oldest
2634
+ held request of their kind, and each takes `{ fail }` to answer with an
2635
+ error instead. `Respond.all` answers everything held, and what that leads
2636
+ to, until nothing is. Reorder the steps to try another interleaving.
2637
+ - **`Trace.regressions`** names each time a read that showed a value read
2638
+ `Loading`, `Initial`, or `Failed` without it. It gives the step, the Message
2639
+ and `Data.why`; it is `Data.timeline` over the trace, kept to its
2640
+ regressions. `NotFound` is not a regression, since the server said the row
2641
+ is gone. **`Trace.tags`** is each change of a read's tag.
2642
+ - **It fails loudly.** A `Respond` that matches nothing held throws and lists
2643
+ what is held. A request abandoned by a restart is no longer held. A
2644
+ Subscription or Command that dies throws. An `update` that does not take
2645
+ Remote's Messages is a type error.
2646
+ - **What it leaves out.** Its clock stands still, at `now` (0 unless given):
2647
+ every Command it runs and every answer it asks for reads that time
2648
+ (`Remote.now`, an answer's date), and a retention `grace`, an age-out and a
2649
+ live restart never fire. A live stream sends nothing, so
2650
+ script its events as `LiveReceived` Messages. Only Remote's Commands run,
2651
+ and `entry.skipped` names the others. It drives a domain whose Messages are
2652
+ in your union, not a `Remote.fold`.
2653
+ - **Durable writes need the outbox.** Pass `outbox` (your `Data.outbox`
2654
+ wiring) and the trace starts from its `restore`, over an empty store of its
2655
+ own, and runs its Subscriptions as one tab: a durable write is stored and
2656
+ sent by it, and `Respond.mutation` answers that send.
2657
+
2658
+ ## Testing that a prediction is the truth: `Law`
2659
+
2660
+ A declared write is interpreted twice: by `Data.mutate`, in memory, over what
2661
+ the client holds (what it shows while the write is pending), and by the
2662
+ server, in SQL (what it stores). The law between them: **where the client
2663
+ predicts a part, the server's answer equals it**, while the write is pending
2664
+ and again once its answer has landed and the prediction is released.
2665
+ `Law.check` runs one operation through both, as an application does, and
2666
+ compares:
2667
+
2668
+ ```ts
2669
+ import { Law } from 'foldkit-remote/testing'
2670
+
2671
+ const report = await Effect.runPromise(
2672
+ Law.check(Data, {
2673
+ model: { remote: Remote.initial, me: 'u1' }, // whom Remote.make's viewer reads
2674
+ holds: [{ entity: Card, ids: ['c1', 'c2'] }], // read whole, as the principal
2675
+ lists: [ByRank.ref({})], // loaded whole; their placements compared
2676
+ mutation: AddCard,
2677
+ input: { id: 'c9', title: 'delta', rank: 3, votes: 0 },
2678
+ now: 1_000_000,
2679
+ }).pipe(
2680
+ Effect.provide(Remote.clientLayer(RemoteServer.handlers(server, 'u1')).pipe(Layer.provide(db))),
2681
+ ),
2682
+ )
2683
+ expect(report.disagreed).toEqual([])
2684
+ expect(report.agreed).toEqual(expect.arrayContaining(['Card:c9.createdAt', ByRank.ref({}).identity]))
2685
+ ```
2686
+
2687
+ - **Nothing is stubbed.** The rows in `holds` are read through the server as
2688
+ its principal sees them and reduced into the Model, so the client holds what
2689
+ the server would send it; the lists are queried and merged; `Data.mutate`
2690
+ predicts; its own Command sends the write. Each row the request's layer
2691
+ changed is read back, every list queried again, and compared twice: with
2692
+ the Model as it showed the write (`when: 'pending'`) and with the Model
2693
+ once the answer is reduced into it (`when: 'answered'`). A part is agreed
2694
+ only when both agree. A field the answer left out, which the client would
2695
+ read again, is not compared once answered; a list the answer left stale is
2696
+ fetched again, and is not either.
2697
+ - **A part is `Entity:id.field`, `Entity:id`** (whether the row is there) **or
2698
+ a list's identity.** What the client left to the server (a field it could not
2699
+ work out, a list with a row it could not judge) is in `unknown`, and is not
2700
+ compared.
2701
+ - **Pin the server's clock to `now`** (a `Clock` service, or `TestClock`), or a
2702
+ `now` outside the server's `clockSkew` is replaced by its own, and every time
2703
+ the write sets disagrees.
2704
+ - **A refusal is a part too.** A write the client refused itself (a broken
2705
+ invariant) is still sent to the server, which must refuse it alike; one the
2706
+ server refuses after the client showed it is a disagreement.
2707
+ - **It fails when it compared nothing**, as for a mutation the client foresees
2708
+ nothing of: two sides that both said nothing agree, and that is no evidence.
2709
+ Name the parts you expect compared, as above, so a prediction that silently
2710
+ stops is caught too.
2711
+
1938
2712
  ## Advanced: the kernel
1939
2713
 
1940
2714
  Everything above compiles to the lower-level exports below; there is not a
@@ -1949,7 +2723,9 @@ place in an application Model. It exposes the Remote `Model`, `initial`,
1949
2723
  `RemoteServer.validate`.
1950
2724
 
1951
2725
  `Remote.at(definition, modelRef)` binds that domain to a `ModelRef`;
1952
- `Remote.make` combines definition and binding in one step.
2726
+ `Remote.make` combines definition and binding in one step. Either one's
2727
+ `store` is the ref's `Focus` (`ModelRef.focus(modelRef)`): its `get`, `set`
2728
+ and `dependency`, which `foldkit-sync` and the law checks read.
1953
2729
 
1954
2730
  `Entity.make(name, schema)` is Remote's own entity descriptor, and what a
1955
2731
  `foldkit-entity` Entity is compiled to (`Entity.from`). Remote accepts either
@@ -2087,11 +2863,15 @@ and orders by, so the rows it holds can be compared with one that changed.
2087
2863
 
2088
2864
  ```ts
2089
2865
  // Schematic: `ProjectsByRank` is a Query.define ordered by a number, and
2090
- // `connection` one of `remote.connections`. test/placement.test.ts runs it.
2091
- placeIn(remote.entities, connection, ProjectsByRank.body!, {}, 'Project:p7')
2866
+ // `connection` one of `remote.connections`; `Operators` is foldkit-entity's.
2867
+ // test/placement.test.ts runs it.
2868
+ placeIn(remote.entities, connection, ProjectsByRank.body!, {}, 'Project:p7', Operators.kernel)
2092
2869
  // Before { segment, edge } | End { segment } | Outside | Unknown { reason }
2093
2870
  ```
2094
2871
 
2872
+ The last argument is the operator table the body's calls run by, the one
2873
+ `Remote.make`'s `operators` names (`Operators.kernel` by default).
2874
+
2095
2875
  - `Before` and `End` are positions among loaded edges: before an edge, or at
2096
2876
  the end of a segment that ends the list (`Terminal`).
2097
2877
  - `Outside` is a row that sorts past a `Cursor` boundary or into a gap between
@@ -2116,8 +2896,9 @@ a request passes the one it first sent.
2116
2896
  `MutationSucceeded` reconciles at most once per `requestId`; an unknown or
2117
2897
  already-applied settlement is a no-op. It carries `now`, the clock reading of
2118
2898
  the answer, which dates the entities it writes as a read's `now` does, so a
2119
- freshness policy ages them from then. `Data.mutate` and `Remote.mutateInto`
2120
- stamp it from their `now` option, `Date.now` by default.
2899
+ freshness policy ages them from then. `Data.mutate`, `Remote.mutateInto` and
2900
+ the outbox stamp it from Effect's `Clock` when the answer lands, never from the
2901
+ `now` option, which is the write's time sent to the server.
2121
2902
 
2122
2903
  ### The transport seam
2123
2904
 
@@ -2147,6 +2928,7 @@ Remote owns:
2147
2928
  - the `Remote.Model` Submodel and its reducer;
2148
2929
  - query connections and pagination boundaries;
2149
2930
  - optimistic mutation overlays and idempotent settlement bookkeeping;
2931
+ - the outbox of durable writes: stored, sent in order by one tab, sent again;
2150
2932
  - live cursors, duplicate suppression, and gap detection;
2151
2933
  - retention/garbage collection of disposable cache data;
2152
2934
  - deterministic, scoped cache snapshots;
@@ -2157,26 +2939,30 @@ Remote does **not** own:
2157
2939
 
2158
2940
  - your source database or authoritative server records;
2159
2941
  - local UI/application state outside `Remote.Model`;
2160
- - durable offline client intent or replica convergence;
2942
+ - client-authored state that converges across devices, or replica convergence;
2161
2943
  - HTTP/WebSocket deployment details;
2162
2944
  - authentication; server-side authorization belongs at the Source boundary;
2163
2945
  - a background scheduler or general-purpose database/query engine.
2164
2946
 
2165
2947
  ## Limits
2166
2948
 
2167
- - Effect RPC is the wire contract; the application supplies deployment layers.
2168
- `ReadBatch` and `LiveRequirement` carry `REMOTE_PROTOCOL_VERSION`, and a
2169
- mismatched version fails with `RemoteProtocolError` rather than silently
2170
- drifting.
2949
+ - `RemoteRpc`'s schemas are the wire contract, carried by Effect RPC, by JSON
2950
+ over `POST` (`Remote.http`), or over a `MessagePort`; the application
2951
+ supplies deployment layers. `ReadBatch` and `LiveRequirement` carry
2952
+ `REMOTE_PROTOCOL_VERSION`, and a mismatched version fails with
2953
+ `RemoteProtocolError` rather than silently drifting.
2171
2954
  - One request may name at most `MAX_FIELDS_PER_REQUEST` (256) fields of one
2172
2955
  entity and nest relations at most `MAX_RELATION_DEPTH` (8) levels; the wire
2173
- rejects larger inputs.
2956
+ rejects larger inputs. A live stream carries at most `MAX_LIVE_QUERIES` (32)
2957
+ lists, and a `many` relation read whole is bounded by the server's Source
2958
+ (`maxCollection` in `foldkit-remote-drizzle`).
2174
2959
  - The wire `LiveChange` union carries entity patches/deletes and connection
2175
2960
  insert/remove/invalidate changes; `RemoteServer.live` serves them as a stream.
2176
2961
  - Coalescing is scoped to one `RemoteClient` layer. Separate layers do not share
2177
2962
  batches.
2178
- - Retention keeps what active Surfaces (and explicit retain entries) reach. An
2179
- inactive/unretained Surface may lose its cache on `RetentionChanged`.
2963
+ - Retention keeps what active Surfaces (and explicit retain entries) reach, and
2964
+ what the most recently left ones reached, up to `departedLimit`. Past it, an
2965
+ inactive Surface loses its cache on `RetentionChanged`.
2180
2966
  - Two applications with the same Model type are not distinguishable by TypeScript
2181
2967
  alone; `Data.subscriptions` also checks the Surface owner token at runtime.
2182
2968
  - `staleWhileRevalidate({ maxAge })` ages entity data. Connections have no age;
@@ -2189,6 +2975,9 @@ Remote does **not** own:
2189
2975
 
2190
2976
  - [Server-derived state](../../docs/remote.md) — the deeper architectural guide
2191
2977
  and ownership model.
2978
+ - [Errors and failures](../../docs/errors.md) — how a read's, a write's and
2979
+ every other package's failures fit together: who owns each, what the reader
2980
+ sees, what the application does.
2192
2981
  - [`foldkit-remote-server`](../remote-server) — server Sources, resolution, and
2193
2982
  semantic-field authorization.
2194
2983
  - [`foldkit-remote-drizzle`](../remote-drizzle) — optional compilation of Remote