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/dist/model.js ADDED
@@ -0,0 +1,1308 @@
1
+ import { PredictionPart, liveRefused, outboxUnreadable, outboxUnsaved, predictionMismatch, withDiagnostics } from "./diagnostics.js";
2
+ import { put } from "./record.js";
3
+ import { isRefPage, targetsOf } from "./relation.js";
4
+ import { askAgain, emptyStore, entityKey, entry, isTombstone, readField, remove, setStale, setUnavailable, tombstone, writeEntities } from "./store.js";
5
+ import { beginMutation, emptyMutationState, failMutation, forgetFailure, settledLater } from "./mutation.js";
6
+ import { AnswerRead, NormalizedEntity, ReadBatchResult, ReadRequest, RelationRequest, SettledFields } from "./wire.js";
7
+ import { emptyConnection, merge, unknown } from "./connection.js";
8
+ import { remoteErrorSchema } from "./remoteData.js";
9
+ import { beginOptimistic, emptyOptimistic, pruneOverlays, settleFailure, settleSuccess } from "./optimistic.js";
10
+ import { applyConnectionEvent, applyEntityEvent, emptyLiveState } from "./live.js";
11
+ import { Queued, emptyOutbox, operationOf, revisionOf, withDone } from "./outbox.js";
12
+ import { windowKey } from "./plan.js";
13
+ import { gc, reachable, recentRoots } from "./retain.js";
14
+ import { RemotePersistence } from "./persistence.js";
15
+ import { Option, Schema } from "effect";
16
+ // src/model.ts
17
+ /**
18
+ * The Remote submodel: the store, connections, mutations, live state, and
19
+ * optimistic layers an application keeps in its Model, the Messages that
20
+ * change it, and the pure reducer over them.
21
+ */
22
+ const noFailures = {
23
+ connections: {},
24
+ fields: {}
25
+ };
26
+ const emptyRefresh = {
27
+ generation: 0,
28
+ requested: /* @__PURE__ */ new Map(),
29
+ started: /* @__PURE__ */ new Map(),
30
+ floor: 0
31
+ };
32
+ /**
33
+ * The Model with everything the server told it forgotten: values, tombstones,
34
+ * unavailable fields, connections, live cursors, failures, and what is in
35
+ * flight. For a change of principal, whose facts these were. The mutation
36
+ * sequence is kept, so request ids stay unique, and a mutation in flight is
37
+ * treated as applied: its answer, when it comes, writes nothing here.
38
+ *
39
+ * `restoredFrom` is kept too: the emptied store is now what that key holds,
40
+ * so `Data.persistence` saves it over the old snapshot and never restores the
41
+ * key again. Restoring would bring back exactly what was forgotten.
42
+ */
43
+ const forgetRemote = (model) => {
44
+ const generation = model.refresh.generation + 1;
45
+ return {
46
+ ...initialRemoteModel,
47
+ restoredFrom: model.restoredFrom,
48
+ refresh: {
49
+ generation,
50
+ requested: /* @__PURE__ */ new Map(),
51
+ started: /* @__PURE__ */ new Map(),
52
+ floor: generation
53
+ },
54
+ mutations: {
55
+ ...emptyMutationState,
56
+ sequence: model.mutations.sequence,
57
+ applied: /* @__PURE__ */ new Set([...model.mutations.applied, ...model.mutations.pending])
58
+ },
59
+ outbox: {
60
+ ...emptyOutbox,
61
+ session: model.outbox.session
62
+ }
63
+ };
64
+ };
65
+ const initialRemoteModel = {
66
+ entities: emptyStore,
67
+ connections: {},
68
+ grown: {},
69
+ optimistic: emptyOptimistic,
70
+ live: {},
71
+ mutations: emptyMutationState,
72
+ outbox: emptyOutbox,
73
+ gaps: /* @__PURE__ */ new Set(),
74
+ streams: {},
75
+ loading: /* @__PURE__ */ new Set(),
76
+ refresh: emptyRefresh,
77
+ failures: noFailures,
78
+ restoredFrom: Option.none(),
79
+ placed: {},
80
+ diagnostics: [],
81
+ recent: {
82
+ requirements: [],
83
+ connections: []
84
+ }
85
+ };
86
+ /** The entity store and the mutation ledger are runtime values, not wire shapes. */
87
+ const runtimeSchema = Schema.Unknown;
88
+ const remoteModelSchema = () => Schema.Struct({
89
+ entities: runtimeSchema,
90
+ connections: Schema.Record(Schema.String, runtimeSchema),
91
+ grown: Schema.Record(Schema.String, Schema.Number),
92
+ optimistic: runtimeSchema,
93
+ live: Schema.Record(Schema.String, runtimeSchema),
94
+ mutations: runtimeSchema,
95
+ outbox: runtimeSchema,
96
+ gaps: runtimeSchema,
97
+ streams: Schema.Record(Schema.String, runtimeSchema),
98
+ loading: runtimeSchema,
99
+ refresh: runtimeSchema,
100
+ failures: Schema.Struct({
101
+ connections: Schema.Record(Schema.String, remoteErrorSchema),
102
+ fields: Schema.Record(Schema.String, remoteErrorSchema)
103
+ }),
104
+ restoredFrom: runtimeSchema,
105
+ placed: runtimeSchema,
106
+ diagnostics: runtimeSchema,
107
+ recent: runtimeSchema
108
+ });
109
+ const retentionRootsSchema = Schema.Struct({
110
+ requirements: Schema.Array(ReadRequest),
111
+ connections: Schema.Array(Schema.Struct({
112
+ identity: Schema.String,
113
+ select: Schema.optional(RelationRequest)
114
+ }))
115
+ });
116
+ /**
117
+ * The Messages' fields by tag, without `_tag`: what `defineMessageUnion` takes,
118
+ * so an application spreads them into its own union (`Remote.messages`).
119
+ */
120
+ const remoteMessageCases = {
121
+ ReadReceived: {
122
+ requests: Schema.Array(ReadRequest),
123
+ result: ReadBatchResult,
124
+ now: Schema.Number
125
+ },
126
+ ReadFailed: {
127
+ requests: Schema.Array(ReadRequest),
128
+ error: remoteErrorSchema,
129
+ stream: Schema.optional(Schema.String)
130
+ },
131
+ RefreshStarted: { requests: Schema.Array(ReadRequest) },
132
+ ReadStarted: {
133
+ requests: Schema.Array(ReadRequest),
134
+ refresh: Schema.optional(Schema.Number)
135
+ },
136
+ QueryStarted: { connections: Schema.Array(Schema.String) },
137
+ RetentionChanged: { roots: retentionRootsSchema },
138
+ Hydrated: {
139
+ connections: Schema.optional(Schema.Unknown),
140
+ from: Schema.optional(Schema.String),
141
+ entities: runtimeSchema,
142
+ merge: Schema.Union([Schema.Literal("replace"), Schema.Literal("preserve-existing")])
143
+ },
144
+ MutationStarted: {
145
+ requestId: Schema.String,
146
+ optimistic: Schema.optional(Schema.Array(Schema.Unknown)),
147
+ viewer: Schema.optional(Schema.Struct({
148
+ entity: Schema.String,
149
+ id: Schema.String
150
+ })),
151
+ write: Schema.optional(Schema.Struct({
152
+ mutation: Schema.String,
153
+ input: Schema.Unknown,
154
+ keys: Schema.optional(Schema.Array(Schema.String)),
155
+ now: Schema.optional(Schema.Number),
156
+ durable: Schema.optional(Schema.Struct({
157
+ input: Schema.Unknown,
158
+ shown: Schema.Array(RelationRequest)
159
+ }))
160
+ }))
161
+ },
162
+ MutationQueued: { requestId: Schema.String },
163
+ OutboxRead: {
164
+ key: Schema.String,
165
+ scope: Schema.String,
166
+ session: Schema.optional(Schema.String),
167
+ entries: Schema.Array(Schema.Unknown),
168
+ done: Schema.Array(Schema.String),
169
+ unsaved: Schema.optional(Schema.String)
170
+ },
171
+ OutboxDelayed: {
172
+ requestId: Schema.String,
173
+ attempt: Schema.Number,
174
+ error: remoteErrorSchema
175
+ },
176
+ MutationSucceeded: {
177
+ requestId: Schema.String,
178
+ entities: Schema.Array(NormalizedEntity),
179
+ connections: Schema.optional(Schema.Array(Schema.Unknown)),
180
+ deleted: Schema.optional(Schema.Array(Schema.Struct({
181
+ entity: Schema.String,
182
+ id: Schema.String
183
+ }))),
184
+ read: Schema.optional(AnswerRead),
185
+ now: Schema.Number
186
+ },
187
+ MutationFailed: {
188
+ requestId: Schema.String,
189
+ error: remoteErrorSchema,
190
+ operation: Schema.String
191
+ },
192
+ OverlayShown: {
193
+ id: Schema.String,
194
+ optimistic: Schema.Array(Schema.Unknown)
195
+ },
196
+ OverlayLifted: { id: Schema.String },
197
+ WindowGrown: {
198
+ window: Schema.String,
199
+ size: Schema.Number
200
+ },
201
+ LiveReceived: {
202
+ stream: Schema.String,
203
+ event: Schema.Unknown,
204
+ policy: Schema.optional(Schema.Unknown),
205
+ now: Schema.Number
206
+ },
207
+ GapCleared: { stream: Schema.String },
208
+ ConnectionMerged: {
209
+ connection: Schema.String,
210
+ page: Schema.Unknown,
211
+ refreshes: Schema.optional(Schema.Boolean),
212
+ select: Schema.optional(RelationRequest),
213
+ entities: Schema.optional(Schema.Array(NormalizedEntity)),
214
+ settled: Schema.optional(Schema.Array(SettledFields)),
215
+ now: Schema.optional(Schema.Number)
216
+ },
217
+ ConnectionInvalidated: { connection: Schema.String },
218
+ ConnectionRefreshed: { connection: Schema.String },
219
+ QueryFailed: {
220
+ connection: Schema.String,
221
+ error: remoteErrorSchema
222
+ },
223
+ Preloaded: { messages: Schema.Array(Schema.suspend(() => remoteMessageSchema)) },
224
+ CompletedReportDiagnostics: {}
225
+ };
226
+ /**
227
+ * Whether a Message is one of Remote's, by tag. Narrows an application's union
228
+ * to its Remote cases, and its complement to the application's own.
229
+ */
230
+ const isRemoteMessage = (message) => Object.hasOwn(remoteMessageCases, message._tag);
231
+ const remoteMessageSchema = Schema.Union(Object.entries(remoteMessageCases).map(([tag, fields]) => Schema.Struct({
232
+ _tag: Schema.Literal(tag),
233
+ ...fields
234
+ })));
235
+ /** An overlay's layer, named apart from every request's: a request id is the application's to choose too. */
236
+ const OVERLAY = "overlay:";
237
+ const overlayLayer = (id) => `${OVERLAY}${id}`;
238
+ /** The ids `OverlayShown` shows now, each once, in the order they are drawn. */
239
+ const overlaysOf = (model) => [...new Set([...model.optimistic.layers, ...model.optimistic.overlays].flatMap(({ id }) => id.startsWith(OVERLAY) ? [id.slice(8)] : []))];
240
+ /** The key a grown window is kept under: its connection and the window first asked for. */
241
+ const windowGrowthKey = (identity, window) => `${identity}\u0000${window}`;
242
+ /**
243
+ * Whether only overlays show this entity: the server has told the store
244
+ * nothing of it and no request's layer writes it, so nothing will fetch what an
245
+ * overlay leaves out.
246
+ */
247
+ const onlyOverlaid = (model, entity, id) => {
248
+ if (Object.hasOwn(model.entities, entityKey(entity, id))) return false;
249
+ const writers = model.optimistic.layers.filter((layer) => layer.patches.some((patch) => patch.entity === entity && patch.id === id));
250
+ return writers.length > 0 && writers.every((layer) => layer.id.startsWith("overlay:"));
251
+ };
252
+ const marksOf = (requests) => requests.map((request) => [entityKey(request.entity, request.id), request.fields]);
253
+ /**
254
+ * One mark per requested field, `entity\0id\0field`. A read is identified by
255
+ * what it asks for, not by a request id: two reads asking for one field share
256
+ * its mark, so the first answer clears it. Over-clearing shows `Initial` rather
257
+ * than a spinner, which is the safer way to be wrong. The refresh generations
258
+ * below key off the same marks, so "what a read asks for" means one thing.
259
+ */
260
+ const fieldMark = (entity, id, field) => `${entityKey(entity, id)}\u0000${field}`;
261
+ const fieldMarks = function* (requests) {
262
+ for (const request of requests) for (const field of request.fields) yield fieldMark(request.entity, request.id, field);
263
+ };
264
+ const withLoading = (loading, requests) => {
265
+ const next = new Set(loading);
266
+ for (const mark of fieldMarks(requests)) next.add(mark);
267
+ return next.size === loading.size ? loading : next;
268
+ };
269
+ const withoutLoading = (loading, requests) => {
270
+ if (loading.size === 0) return loading;
271
+ const next = new Set(loading);
272
+ for (const mark of fieldMarks(requests)) next.delete(mark);
273
+ return next.size === loading.size ? loading : next;
274
+ };
275
+ /**
276
+ * A connection's mark. The leading separator keeps it out of the field marks'
277
+ * space, whatever a connection identity happens to spell.
278
+ */
279
+ const connectionPrefix = "\0connection\0";
280
+ const connectionMark = (identity) => `${connectionPrefix}${identity}`;
281
+ /** Whether a query for this connection is in flight. */
282
+ const isQueryLoading = (model, identity) => model.loading.size !== 0 && model.loading.has(connectionMark(identity));
283
+ const withoutQueryLoading = (loading, identity) => {
284
+ if (!loading.has(connectionMark(identity))) return loading;
285
+ const next = new Set(loading);
286
+ next.delete(connectionMark(identity));
287
+ return next;
288
+ };
289
+ /**
290
+ * The in-flight marks of connections the roots still name. A query the entry
291
+ * stopped waiting for — it was restarted or deactivated before an answer came —
292
+ * leaves its mark behind, and a connection released with that mark would read
293
+ * `Loading` forever the next time something asked for it before sending.
294
+ */
295
+ const prunedLoading = (loading, roots, reached) => {
296
+ if (loading.size === 0) return loading;
297
+ const named = new Set(roots.connections.map((root) => connectionMark(root.identity)));
298
+ const kept = [...loading].filter((mark) => mark.startsWith(connectionPrefix) ? named.has(mark) : reached().has(markEntity(mark)));
299
+ return kept.length === loading.size ? loading : new Set(kept);
300
+ };
301
+ /**
302
+ * The windows grown on connections retention keeps; a dropped one starts over.
303
+ * The same record when every one is kept.
304
+ */
305
+ const keptWindows = (grown, connections) => {
306
+ const kept = Object.entries(grown).filter(([key]) => Object.hasOwn(connections, key.slice(0, key.lastIndexOf("\0"))));
307
+ return kept.length === Object.keys(grown).length ? grown : Object.fromEntries(kept);
308
+ };
309
+ /** The placements awaiting a page of a list retention still keeps; a dropped list's page never comes. */
310
+ const keptPlacements = (placed, connections) => {
311
+ const kept = Object.entries(placed).filter(([key]) => Object.hasOwn(connections, key));
312
+ return kept.length === Object.keys(placed).length ? placed : Object.fromEntries(kept);
313
+ };
314
+ /**
315
+ * Drops the refresh marks of entities and connections that were collected.
316
+ * Nothing observes a collected entity — the retention roots are the active
317
+ * Surfaces — so no read entry's generation can fall back when its mark goes.
318
+ * Without this the generations would be bounded by how many things the
319
+ * application has ever refreshed rather than by what it currently holds, which
320
+ * for a list refreshed on a timer is the same as not bounded at all.
321
+ */
322
+ const prunedRefresh = (refresh, retained) => {
323
+ const live = (mark) => mark.startsWith(connectionPrefix) ? retained.connections[mark.slice(12)] !== void 0 : retained.entities[mark.slice(0, mark.indexOf("\0"))] !== void 0;
324
+ const keep = (marks) => {
325
+ const next = /* @__PURE__ */ new Map();
326
+ for (const [mark, generation] of marks) if (live(mark)) next.set(mark, generation);
327
+ return next.size === marks.size ? marks : next;
328
+ };
329
+ const requested = keep(refresh.requested);
330
+ const started = keep(refresh.started);
331
+ return requested === refresh.requested && started === refresh.started ? refresh : {
332
+ ...refresh,
333
+ requested,
334
+ started
335
+ };
336
+ };
337
+ /**
338
+ * The generation a read is planned under: the highest any of the fields it
339
+ * reads or connections it runs was refreshed at. A read entry carries this as a
340
+ * dependency, so it restarts when what it observes is refreshed and not when
341
+ * anything else is.
342
+ */
343
+ const refreshedAt = (refresh, requests, connections = []) => {
344
+ let at = refresh.floor;
345
+ for (const mark of fieldMarks(requests)) at = Math.max(at, refresh.requested.get(mark) ?? 0);
346
+ for (const identity of connections) at = Math.max(at, refresh.requested.get(connectionMark(identity)) ?? 0);
347
+ return at;
348
+ };
349
+ /**
350
+ * Marks every field of `requests` and every connection in `connections`
351
+ * refreshed at the next generation.
352
+ */
353
+ const withRefreshRequested = (refresh, requests, connections = []) => {
354
+ const generation = refresh.generation + 1;
355
+ const requested = new Map(refresh.requested);
356
+ for (const mark of fieldMarks(requests)) requested.set(mark, generation);
357
+ for (const identity of connections) requested.set(connectionMark(identity), generation);
358
+ return {
359
+ ...refresh,
360
+ generation,
361
+ requested
362
+ };
363
+ };
364
+ /**
365
+ * Marks connections as needing to be asked again, and restarts a read of them
366
+ * already in flight: a page requested before what invalidated it could land
367
+ * after it, saying what is no longer true. `Remote.refresh` marks the same way.
368
+ */
369
+ const invalidateConnections = (model, connections) => {
370
+ if (connections.length === 0) return model;
371
+ const marked = connections.reduce((current, connection) => updateRemote(current, {
372
+ _tag: "ConnectionInvalidated",
373
+ connection
374
+ }), model);
375
+ return {
376
+ ...marked,
377
+ refresh: withRefreshRequested(marked.refresh, [], connections)
378
+ };
379
+ };
380
+ /**
381
+ * Records that a read of `requests` began under `generation`. A read that
382
+ * carries no generation began under none, and leaves the marks as they are.
383
+ */
384
+ const withRefreshStarted = (refresh, requests, generation) => {
385
+ if (generation === void 0 || generation === 0) return refresh;
386
+ let started;
387
+ for (const mark of fieldMarks(requests)) {
388
+ if ((refresh.started.get(mark) ?? 0) >= generation) continue;
389
+ started ??= new Map(refresh.started);
390
+ started.set(mark, generation);
391
+ }
392
+ return started === void 0 ? refresh : {
393
+ ...refresh,
394
+ started
395
+ };
396
+ };
397
+ /**
398
+ * Whether one field's read already began under the generation that field was
399
+ * last refreshed at. Such a read is in flight and would otherwise outlive the
400
+ * refresh, so the refresh takes a new generation to restart it even though the
401
+ * field is already stale.
402
+ */
403
+ const refreshIsInFlight = (refresh, request, field) => {
404
+ const mark = fieldMark(request.entity, request.id, field);
405
+ return (refresh.started.get(mark) ?? 0) === (refresh.requested.get(mark) ?? 0);
406
+ };
407
+ /**
408
+ * Whether a read is fetching any of `fields` for this entity. Consulted only
409
+ * when the store lacks the value, which is what separates `Loading` from
410
+ * `Initial`.
411
+ */
412
+ const isLoading = (model, entity, id, fields) => {
413
+ if (model.loading.size === 0) return false;
414
+ return fields.some((field) => model.loading.has(fieldMark(entity, id, field)));
415
+ };
416
+ const setConnectionStale = (connections, connection, stale) => {
417
+ const current = connections[connection];
418
+ if (current?.stale === stale) return connections;
419
+ return {
420
+ ...connections,
421
+ [connection]: {
422
+ ...current ?? emptyConnection,
423
+ stale
424
+ }
425
+ };
426
+ };
427
+ /**
428
+ * How many streams `live` and `streams` keep. Which streams the active
429
+ * Surfaces hold is not in the Model (retention roots merge every Surface's
430
+ * requirements, and a stream key is one entry's), so the ledger keeps those
431
+ * most recently heard from, as `failed` keeps the latest failures. A stream
432
+ * dropped while still open meets its next event as a gap, which restarts it
433
+ * and refetches what it covers.
434
+ */
435
+ const STREAM_WINDOW = 256;
436
+ /** `record` with `stream` set as the most recent, the oldest dropped past the window. */
437
+ const heard = (record, stream, value) => {
438
+ const { [stream]: _previous, ...rest } = record;
439
+ const next = {
440
+ ...rest,
441
+ [stream]: value
442
+ };
443
+ const keys = Object.keys(next);
444
+ for (const key of keys.slice(0, Math.max(0, keys.length - STREAM_WINDOW))) delete next[key];
445
+ return next;
446
+ };
447
+ const withLiveState = (live, stream, state) => live[stream] === state ? live : heard(live, stream, state);
448
+ const noHealth = {
449
+ restarts: 0,
450
+ failures: 0,
451
+ error: Option.none(),
452
+ refused: Option.none()
453
+ };
454
+ /**
455
+ * A stream broke: it is marked a gap and its restart count moves, which
456
+ * restarts its live entry. A stream already broken and not yet restarted
457
+ * changes nothing, so the events a dying stream still delivers restart it once.
458
+ */
459
+ const broke = (model, stream, error) => {
460
+ if (model.gaps.has(stream)) return model;
461
+ const health = model.streams[stream] ?? noHealth;
462
+ const streams = heard(model.streams, stream, {
463
+ restarts: health.restarts + 1,
464
+ failures: health.failures + 1,
465
+ error,
466
+ refused: Option.none()
467
+ });
468
+ const gaps = new Set([...model.gaps, stream].filter((key) => Object.hasOwn(streams, key)));
469
+ return {
470
+ ...model,
471
+ gaps,
472
+ streams
473
+ };
474
+ };
475
+ /** The tag of the server's refusal of a live stream, which `RemoteError` carries open. */
476
+ const refusedTag = "RemoteLiveRefused";
477
+ /**
478
+ * The server refused a stream for what it asks: it is marked refused and not a
479
+ * gap, so its live entry does not restart it, and the refusal is recorded as
480
+ * a diagnostic once.
481
+ */
482
+ const refused = (model, stream, error) => {
483
+ const health = model.streams[stream] ?? noHealth;
484
+ if (Option.isSome(health.refused)) return model;
485
+ const streams = heard(model.streams, stream, {
486
+ ...health,
487
+ error: Option.some(error),
488
+ refused: Option.some(error)
489
+ });
490
+ return {
491
+ ...clearGap(model, stream),
492
+ streams,
493
+ diagnostics: withDiagnostics(model.diagnostics, [liveRefused({
494
+ stream,
495
+ error
496
+ })])
497
+ };
498
+ };
499
+ /**
500
+ * An event applied on a stream that resubscribed: it works, so its next break
501
+ * backs off from the start. A broken stream's last events prove nothing.
502
+ */
503
+ const healthy = (model, stream) => {
504
+ const health = model.streams[stream];
505
+ const settled = health !== void 0 && health.failures === 0 && Option.isNone(health.refused);
506
+ return health === void 0 || settled || model.gaps.has(stream) ? model : {
507
+ ...model,
508
+ streams: heard(model.streams, stream, {
509
+ ...health,
510
+ failures: 0,
511
+ error: Option.none(),
512
+ refused: Option.none()
513
+ })
514
+ };
515
+ };
516
+ const clearGap = (model, stream) => model.gaps.has(stream) ? {
517
+ ...model,
518
+ gaps: new Set([...model.gaps].filter((entry) => entry !== stream))
519
+ } : model;
520
+ /**
521
+ * The pure reducer all four producers share. A live event that arrives ahead of
522
+ * its cursor is a gap: it is not applied, and the stream is recorded so its
523
+ * live entry restarts it rather than silently missing facts. When every field a Message
524
+ * touches keeps its identity, `model` itself is returned, so the application's
525
+ * root keeps its identity and Foldkit does not render. The common no-ops do (a
526
+ * duplicate live event, a repeated `ReadStarted`); a repeated failure, or a
527
+ * `preserve-existing` restore of entities already held, still makes an equal
528
+ * copy.
529
+ */
530
+ const updateRemote = (model, message) => {
531
+ const next = reduceRemote(model, message);
532
+ return next === model || !sameFields(model, next) ? next : model;
533
+ };
534
+ /** Whether every field of `next` is the one `model` holds. */
535
+ const sameFields = (model, next) => {
536
+ const keys = Object.keys(next);
537
+ return keys.length === Object.keys(model).length && keys.every((key) => Object.is(model[key], next[key]));
538
+ };
539
+ /**
540
+ * A placement compared with the server's first page of the list it placed
541
+ * in, under `verifyPredictions`: the keys as far as both reach must agree, and
542
+ * no row placing took out may be one the page still lists. The record goes
543
+ * once compared; a disagreement is kept.
544
+ */
545
+ const checkedPlacement = (model, connection, page, refreshes) => {
546
+ const placed = model.placed[connection];
547
+ if (placed === void 0 || !refreshes || page.start._tag !== "Terminal") return {
548
+ placed: model.placed,
549
+ diagnostics: model.diagnostics
550
+ };
551
+ const { [connection]: _, ...rest } = model.placed;
552
+ const reach = Math.min(placed.order.length, page.edges.length);
553
+ const served = page.edges.slice(0, reach).map((edge) => edge.key);
554
+ const asPlaced = placed.order.slice(0, reach);
555
+ const listed = new Set(page.edges.map((edge) => edge.key));
556
+ return {
557
+ placed: rest,
558
+ diagnostics: served.every((key, index) => key === asPlaced[index]) && !placed.dropped.some((key) => listed.has(key)) ? model.diagnostics : withDiagnostics(model.diagnostics, [predictionMismatch(PredictionPart.Placement({
559
+ connection,
560
+ placed: asPlaced,
561
+ served
562
+ }))])
563
+ };
564
+ };
565
+ /** The queue with `queued` in place of the entry of its operation, or after the rest. */
566
+ const placedIn = (queue, queued) => {
567
+ const operation = operationOf(queued);
568
+ return queue.some((each) => operationOf(each) === operation) ? queue.map((each) => operationOf(each) === operation ? queued : each) : [...queue, queued];
569
+ };
570
+ /** A durable write queued to wait its turn: new, or a failed one retried, its revision moved on. */
571
+ const queuedWrite = (outbox, operation, write, message) => {
572
+ const prior = outbox.queue.find((queued) => operationOf(queued) === operation);
573
+ const stored = {
574
+ operation,
575
+ mutation: write.mutation,
576
+ input: write.durable?.input,
577
+ ...write.keys === void 0 ? {} : { keys: write.keys },
578
+ ...write.now === void 0 ? {} : { now: write.now },
579
+ optimistic: message.optimistic ?? [],
580
+ ...message.viewer === void 0 ? {} : { viewer: message.viewer },
581
+ shown: write.durable?.shown ?? [],
582
+ revision: prior === void 0 ? 0 : revisionOf(prior) + 1,
583
+ state: { _tag: "Waiting" }
584
+ };
585
+ return {
586
+ ...outbox,
587
+ queue: placedIn(outbox.queue, Queued.Readable({
588
+ write: stored,
589
+ stored: false,
590
+ attempt: 0,
591
+ delayed: Option.none()
592
+ }))
593
+ };
594
+ };
595
+ /** The outbox with the readable write of `operation` changed by `change`; itself when it has none. */
596
+ const withQueued = (outbox, operation, change) => {
597
+ const at = outbox.queue.findIndex((queued) => operationOf(queued) === operation);
598
+ const queued = outbox.queue[at];
599
+ if (queued?._tag !== "Readable") return outbox;
600
+ return {
601
+ ...outbox,
602
+ queue: outbox.queue.map((each, index) => index === at ? change(queued) : each)
603
+ };
604
+ };
605
+ /** The outbox with `operation` settled: out of the queue, and remembered as done. */
606
+ const withoutQueued = (outbox, operation) => outbox.queue.some((queued) => operationOf(queued) === operation) ? {
607
+ ...outbox,
608
+ queue: outbox.queue.filter((queued) => operationOf(queued) !== operation),
609
+ done: withDone(outbox.done, [operation])
610
+ } : outbox;
611
+ const storedAs = (queued, stored) => queued.stored === stored ? queued : {
612
+ ...queued,
613
+ stored
614
+ };
615
+ /** A stored entry as the queue holds it: in the store, as read. */
616
+ const queuedOf = (entry) => entry._tag === "Readable" ? Queued.Readable({
617
+ write: entry.write,
618
+ stored: true,
619
+ attempt: 0,
620
+ delayed: Option.none()
621
+ }) : Queued.Unreadable({
622
+ operation: entry.operation,
623
+ revision: entry.revision,
624
+ raw: entry.raw,
625
+ reason: entry.reason,
626
+ stored: true
627
+ });
628
+ const entryOperation = (entry) => entry._tag === "Readable" ? entry.write.operation : entry.operation;
629
+ const entryRevision = (entry) => entry._tag === "Readable" ? entry.write.revision : entry.revision;
630
+ /** Whether the queued write waits its turn: in the ledger in flight, its layer shown. */
631
+ const isWaiting = (queued) => queued._tag === "Readable" && queued.write.state._tag === "Waiting";
632
+ /** The ledger and layers without a queued write: nothing of it shown or listed. */
633
+ const releasedFrom = (model, queued) => {
634
+ const operation = operationOf(queued);
635
+ if (!model.mutations.pending.has(operation)) return {
636
+ ...model,
637
+ mutations: forgetFailure(model.mutations, operation)
638
+ };
639
+ const pending = new Set(model.mutations.pending);
640
+ pending.delete(operation);
641
+ const writes = new Map(model.mutations.writes);
642
+ writes.delete(operation);
643
+ const order = new Map(model.mutations.order);
644
+ order.delete(operation);
645
+ return {
646
+ ...model,
647
+ mutations: {
648
+ ...model.mutations,
649
+ pending,
650
+ writes,
651
+ order
652
+ },
653
+ optimistic: settleFailure(model.optimistic, operation)
654
+ };
655
+ };
656
+ /**
657
+ * The ledger and layers with a stored entry adopted in place of what the
658
+ * page held of it (`prior`): a write waiting is begun, its layer shown; one
659
+ * failed is failed, its layer gone; one this version cannot read is not in
660
+ * the ledger at all.
661
+ */
662
+ const adopted = (model, prior, entry) => {
663
+ const released = prior === void 0 ? model : releasedFrom(model, prior);
664
+ if (entry._tag === "Unreadable") return released;
665
+ const { write } = entry;
666
+ const begun = {
667
+ ...released,
668
+ mutations: beginMutation(released.mutations, write.operation, {
669
+ mutation: write.mutation,
670
+ input: entry.input,
671
+ keys: write.keys,
672
+ now: Option.fromUndefinedOr(write.now),
673
+ optimistic: write.optimistic
674
+ })
675
+ };
676
+ return write.state._tag === "Waiting" ? {
677
+ ...begun,
678
+ optimistic: beginOptimistic(begun.optimistic, write.operation, write.optimistic, Option.fromUndefinedOr(write.viewer))
679
+ } : {
680
+ ...begun,
681
+ mutations: failMutation(begun.mutations, write.operation, write.state.error, write.operation)
682
+ };
683
+ };
684
+ /**
685
+ * Writes another tab settled, whose answers this page did not see: their
686
+ * layers go, every row they showed is asked for again and every list is
687
+ * refetched, since what the server made of them is not known here.
688
+ */
689
+ const settledElsewhere = (model, settled) => {
690
+ const released = settled.reduce(releasedFrom, model);
691
+ const rows = /* @__PURE__ */ new Map();
692
+ for (const queued of settled) {
693
+ if (queued._tag !== "Readable" || queued.write.state._tag !== "Waiting") continue;
694
+ for (const operation of queued.write.optimistic) if ("entity" in operation) rows.set(entityKey(operation.entity, operation.id), operation);
695
+ }
696
+ if (!settled.some(isWaiting)) return released;
697
+ const requests = [...rows.entries()].flatMap(([key, row]) => Option.match(entry(released.entities, key), {
698
+ onNone: () => [],
699
+ onSome: (held) => held.present.size === 0 ? [] : [{
700
+ entity: row.entity,
701
+ id: row.id,
702
+ fields: [...held.present]
703
+ }]
704
+ }));
705
+ const asked = requests.length === 0 ? released : reduceRemote(released, {
706
+ _tag: "RefreshStarted",
707
+ requests
708
+ });
709
+ return invalidateConnections(asked, Object.keys(asked.connections));
710
+ };
711
+ /**
712
+ * What the store holds, met by what the page holds: an entry the store has a
713
+ * newer revision of is adopted, one it has as the page does is marked
714
+ * stored, one it lacks is written again, and one it settled goes. A read
715
+ * under another key replaces the page's queue (its writes stay in their own
716
+ * store). The Model itself when nothing changed.
717
+ */
718
+ const readOutbox = (model, message) => {
719
+ const switching = Option.isSome(model.outbox.key) && model.outbox.key.value !== message.key || Option.isSome(model.outbox.scope) && model.outbox.scope.value !== message.scope;
720
+ const start = switching ? {
721
+ ...model.outbox.queue.reduce(releasedFrom, model),
722
+ outbox: {
723
+ ...model.outbox,
724
+ queue: [],
725
+ done: []
726
+ }
727
+ } : model;
728
+ const before = start.outbox;
729
+ const done = withDone(before.done, message.done);
730
+ const gone = new Set(done);
731
+ const held = new Map(before.queue.map((queued) => [operationOf(queued), queued]));
732
+ const read = /* @__PURE__ */ new Set();
733
+ const queue = [];
734
+ const recorded = [];
735
+ let next = start;
736
+ for (const entry of message.entries) {
737
+ const operation = entryOperation(entry);
738
+ if (gone.has(operation) || read.has(operation)) continue;
739
+ read.add(operation);
740
+ const prior = held.get(operation);
741
+ const revision = entryRevision(entry);
742
+ if (prior !== void 0 && revisionOf(prior) > revision) queue.push(prior);
743
+ else if (prior !== void 0 && revisionOf(prior) === revision) queue.push(storedAs(prior, true));
744
+ else {
745
+ next = adopted(next, prior, entry);
746
+ if (entry._tag === "Unreadable" && prior === void 0) recorded.push(outboxUnreadable({
747
+ requestId: operation,
748
+ reason: entry.reason
749
+ }));
750
+ queue.push(queuedOf(entry));
751
+ }
752
+ }
753
+ const settled = [];
754
+ for (const queued of before.queue) {
755
+ const operation = operationOf(queued);
756
+ if (read.has(operation)) continue;
757
+ if (gone.has(operation)) settled.push(queued);
758
+ else queue.push(storedAs(queued, false));
759
+ }
760
+ next = settledElsewhere(next, settled);
761
+ const said = next.diagnostics.some((diagnostic) => diagnostic._tag === "OutboxUnsaved" && diagnostic.key === message.key);
762
+ if (message.unsaved !== void 0 && !said) recorded.push(outboxUnsaved({
763
+ key: message.key,
764
+ reason: message.unsaved
765
+ }));
766
+ const session = Option.orElse(before.session, () => Option.fromUndefinedOr(message.session));
767
+ if (!switching && next === start && recorded.length === 0 && done === before.done && Option.contains(before.key, message.key) && Option.contains(before.scope, message.scope) && Option.isSome(before.session) === Option.isSome(session) && queue.length === before.queue.length && queue.every((queued, index) => queued === before.queue[index])) return model;
768
+ return {
769
+ ...next,
770
+ outbox: {
771
+ session,
772
+ key: Option.some(message.key),
773
+ scope: Option.some(message.scope),
774
+ queue,
775
+ done
776
+ },
777
+ diagnostics: withDiagnostics(next.diagnostics, recorded)
778
+ };
779
+ };
780
+ /**
781
+ * The Remote Model with a failed or unreadable durable write settled by the
782
+ * reader (`Data.dismiss`): out of the queue and the ledger, and done, so every
783
+ * tab drops it. A waiting one, or one not queued, is the Model itself.
784
+ */
785
+ const dismissQueued = (model, requestId) => {
786
+ const queued = model.outbox.queue.find((each) => operationOf(each) === requestId);
787
+ if (queued === void 0 || isWaiting(queued)) return model;
788
+ return {
789
+ ...model,
790
+ mutations: forgetFailure(model.mutations, requestId),
791
+ outbox: withoutQueued(model.outbox, requestId)
792
+ };
793
+ };
794
+ const reduceRemote = (model, message) => {
795
+ switch (message._tag) {
796
+ case "Preloaded": return message.messages.reduce(reduceRemote, model);
797
+ case "CompletedReportDiagnostics": return model;
798
+ case "ReadReceived": return {
799
+ ...model,
800
+ entities: writeRead(model.entities, message.requests, message.result, message.now),
801
+ loading: withoutLoading(model.loading, message.requests),
802
+ failures: withoutFieldFailures(model.failures, [...fieldMarks(message.requests), ...patchedMarks(message.result.entities)])
803
+ };
804
+ case "ReadFailed":
805
+ if (message.stream !== void 0) return message.error._tag === refusedTag ? refused(model, message.stream, message.error) : broke(model, message.stream, Option.some(message.error));
806
+ return {
807
+ ...model,
808
+ entities: setStale(model.entities, marksOf(message.requests), false),
809
+ loading: withoutLoading(model.loading, message.requests),
810
+ failures: withFieldFailures(model.failures, message.requests, message.error)
811
+ };
812
+ case "RetentionChanged": {
813
+ const recent = recentRoots(model.recent, message.roots);
814
+ const retained = gc(model, recent);
815
+ const active = new Set(message.roots.connections.map((root) => root.identity));
816
+ const connections = recent.connections.reduce((held, { identity }) => active.has(identity) || held[identity] === void 0 ? held : setConnectionStale(held, identity, true), retained.connections);
817
+ let reachedKeys;
818
+ const reached = () => reachedKeys ??= reachable(model.entities, message.roots, model.connections, model.optimistic, model.mutations.pending);
819
+ return {
820
+ ...model,
821
+ ...retained,
822
+ connections,
823
+ recent,
824
+ grown: keptWindows(model.grown, retained.connections),
825
+ placed: keptPlacements(model.placed, retained.connections),
826
+ refresh: prunedRefresh(model.refresh, retained),
827
+ failures: prunedFailures(model.failures, message.roots, reached),
828
+ loading: prunedLoading(model.loading, message.roots, reached)
829
+ };
830
+ }
831
+ case "Hydrated": {
832
+ const restored = Object.entries(message.connections ?? {}).map(([identity, segments]) => [identity, {
833
+ segments: segments.map((edges) => ({
834
+ edges: [...edges],
835
+ start: unknown,
836
+ end: unknown
837
+ })),
838
+ stale: true
839
+ }]);
840
+ const written = Object.entries(message.entities).flatMap(([key, entry]) => message.merge === "replace" || !(key in model.entities) ? [...entry.present].map((field) => `${key}\u0000${field}`) : []);
841
+ return {
842
+ ...model,
843
+ entities: RemotePersistence.mergeStores(model.entities, message.entities, message.merge),
844
+ failures: withoutFieldFailures(model.failures, written),
845
+ connections: restored.length === 0 ? model.connections : message.merge === "replace" ? {
846
+ ...model.connections,
847
+ ...Object.fromEntries(restored)
848
+ } : {
849
+ ...Object.fromEntries(restored),
850
+ ...model.connections
851
+ },
852
+ restoredFrom: message.from === void 0 ? model.restoredFrom : Option.some(message.from)
853
+ };
854
+ }
855
+ case "RefreshStarted": {
856
+ const marks = marksOf(message.requests);
857
+ const entities = setUnavailable(setStale(askAgain(model.entities, marks), marks, true), marks, false);
858
+ const reached = reachedMarks(model.entities, message.requests);
859
+ const failures = withoutFieldFailures(model.failures, reached);
860
+ const retried = reached.filter((mark) => mark in model.failures.fields);
861
+ const loading = retried.length === 0 ? model.loading : /* @__PURE__ */ new Set([...model.loading, ...retried]);
862
+ return {
863
+ ...model,
864
+ entities,
865
+ failures,
866
+ loading
867
+ };
868
+ }
869
+ case "QueryStarted": {
870
+ const loading = new Set(model.loading);
871
+ for (const identity of message.connections) loading.add(connectionMark(identity));
872
+ return loading.size === model.loading.size ? model : {
873
+ ...model,
874
+ loading
875
+ };
876
+ }
877
+ case "ReadStarted": return {
878
+ ...model,
879
+ loading: withLoading(model.loading, message.requests),
880
+ refresh: withRefreshStarted(model.refresh, message.requests, message.refresh)
881
+ };
882
+ case "MutationStarted": return {
883
+ ...model,
884
+ outbox: message.write?.durable === void 0 ? model.outbox : queuedWrite(model.outbox, message.requestId, message.write, message),
885
+ mutations: beginMutation(model.mutations, message.requestId, message.write === void 0 ? void 0 : {
886
+ mutation: message.write.mutation,
887
+ input: message.write.input,
888
+ keys: message.write.keys,
889
+ now: Option.fromUndefinedOr(message.write.now),
890
+ optimistic: message.optimistic ?? []
891
+ }),
892
+ optimistic: beginOptimistic(model.optimistic, message.requestId, message.optimistic ?? [], Option.fromUndefinedOr(message.viewer))
893
+ };
894
+ case "MutationSucceeded": {
895
+ const settled = settleSuccess(model.entities, model.optimistic, model.mutations, message.requestId, message);
896
+ const read = message.read === void 0 || model.mutations.applied.has(message.requestId) ? void 0 : unsettledRead(message.read, settledLater(model.mutations, message.requestId));
897
+ return {
898
+ ...model,
899
+ entities: read === void 0 ? settled.store : writeRead(settled.store, read.requests, read.result, message.now),
900
+ mutations: settled.state,
901
+ outbox: withoutQueued(model.outbox, message.requestId),
902
+ optimistic: settled.optimistic,
903
+ failures: withoutFieldFailures(model.failures, [
904
+ ...read === void 0 ? [] : [...fieldMarks(read.requests), ...patchedMarks(read.result.entities)],
905
+ ...patchedMarks(message.entities),
906
+ ...(message.deleted ?? []).flatMap((ref) => entityFailureMarks(model.failures, entityKey(ref.entity, ref.id)))
907
+ ])
908
+ };
909
+ }
910
+ case "MutationQueued": return model;
911
+ case "OutboxDelayed": return {
912
+ ...model,
913
+ outbox: withQueued(model.outbox, message.requestId, (queued) => Queued.Readable({
914
+ ...queued,
915
+ attempt: message.attempt,
916
+ delayed: Option.some(message.error)
917
+ }))
918
+ };
919
+ case "OutboxRead": return readOutbox(model, message);
920
+ case "OverlayShown": return {
921
+ ...model,
922
+ optimistic: beginOptimistic(settleFailure(model.optimistic, overlayLayer(message.id)), overlayLayer(message.id), message.optimistic)
923
+ };
924
+ case "WindowGrown": return {
925
+ ...model,
926
+ grown: {
927
+ ...model.grown,
928
+ [message.window]: message.size
929
+ }
930
+ };
931
+ case "OverlayLifted": {
932
+ const optimistic = settleFailure(model.optimistic, overlayLayer(message.id));
933
+ return optimistic.layers.length === model.optimistic.layers.length && optimistic.overlays.length === model.optimistic.overlays.length ? model : {
934
+ ...model,
935
+ optimistic
936
+ };
937
+ }
938
+ case "MutationFailed": return {
939
+ ...model,
940
+ outbox: !model.mutations.pending.has(message.requestId) ? model.outbox : withQueued(model.outbox, message.requestId, (queued) => Queued.Readable({
941
+ write: {
942
+ ...queued.write,
943
+ revision: queued.write.revision + 1,
944
+ state: {
945
+ _tag: "Failed",
946
+ error: message.error
947
+ }
948
+ },
949
+ stored: false,
950
+ attempt: 0,
951
+ delayed: Option.none()
952
+ })),
953
+ mutations: failMutation(model.mutations, message.requestId, message.error, message.operation),
954
+ optimistic: settleFailure(model.optimistic, message.requestId)
955
+ };
956
+ case "LiveReceived": {
957
+ const state = model.live[message.stream] ?? emptyLiveState;
958
+ if (message.event._tag === "EntityPatched" || message.event._tag === "EntityDeleted") {
959
+ const event = message.event;
960
+ const applied = applyEntityEvent(state, model.entities, event, message.now);
961
+ if (applied.outcome === "gap") return broke(model, message.stream, Option.none());
962
+ const settled = applied.outcome !== "applied" ? [] : event._tag === "EntityPatched" ? [...patchedMarks([{
963
+ ...event.ref,
964
+ values: event.values
965
+ }])] : entityFailureMarks(model.failures, entityKey(event.ref.entity, event.ref.id));
966
+ const next = {
967
+ ...model,
968
+ entities: applied.store,
969
+ live: withLiveState(model.live, message.stream, applied.state),
970
+ failures: withoutFieldFailures(model.failures, settled)
971
+ };
972
+ return applied.outcome === "applied" ? healthy(next, message.stream) : next;
973
+ }
974
+ const applied = applyConnectionEvent(state, model.optimistic, message.event, message.policy);
975
+ if (applied.outcome === "gap") return broke(model, message.stream, Option.none());
976
+ const written = {
977
+ ...model,
978
+ optimistic: applied.optimistic,
979
+ live: withLiveState(model.live, message.stream, applied.state)
980
+ };
981
+ const next = applied.outcome === "applied" ? healthy(written, message.stream) : written;
982
+ return applied.invalidated === void 0 ? next : invalidateConnections(next, [applied.invalidated]);
983
+ }
984
+ case "GapCleared": return clearGap(model, message.stream);
985
+ case "ConnectionMerged": {
986
+ const current = model.connections[message.connection] ?? emptyConnection;
987
+ const loading = withoutQueryLoading(model.loading, message.connection);
988
+ const replaces = message.refreshes === true && current.stale;
989
+ const merged = merge(replaces ? emptyConnection : current, message.page);
990
+ const payload = message.select === void 0 ? [] : message.entities ?? [];
991
+ const settled = message.select === void 0 ? [] : message.settled ?? [];
992
+ const requests = message.select === void 0 || payload.length === 0 && settled.length === 0 ? [] : message.page.edges.filter((edge) => edge.ref.entity === message.select.entity).map((edge) => ({
993
+ ...message.select,
994
+ id: edge.ref.id
995
+ }));
996
+ const entities = requests.length === 0 ? model.entities : writeRead(model.entities, requests, {
997
+ entities: [...payload],
998
+ settled: [...settled]
999
+ }, message.now ?? 0);
1000
+ const failures = withoutFieldFailures(withoutConnectionFailure(model.failures, message.connection), [...fieldMarks(requests), ...patchedMarks(payload)]);
1001
+ return {
1002
+ ...model,
1003
+ ...checkedPlacement(model, message.connection, message.page, message.refreshes === true),
1004
+ entities,
1005
+ connections: {
1006
+ ...model.connections,
1007
+ [message.connection]: message.refreshes === true ? {
1008
+ ...merged,
1009
+ stale: false
1010
+ } : merged
1011
+ },
1012
+ optimistic: pruneOverlays(model.optimistic, message.connection, new Set(message.page.edges.map((edge) => edge.key)), model.mutations.pending),
1013
+ failures,
1014
+ loading
1015
+ };
1016
+ }
1017
+ case "ConnectionInvalidated": return {
1018
+ ...model,
1019
+ connections: setConnectionStale(model.connections, message.connection, true),
1020
+ failures: withoutConnectionFailure(model.failures, message.connection),
1021
+ loading: message.connection in model.failures.connections ? /* @__PURE__ */ new Set([...model.loading, connectionMark(message.connection)]) : model.loading
1022
+ };
1023
+ case "QueryFailed": return {
1024
+ ...model,
1025
+ failures: {
1026
+ ...model.failures,
1027
+ connections: {
1028
+ ...model.failures.connections,
1029
+ [message.connection]: message.error
1030
+ }
1031
+ },
1032
+ loading: withoutQueryLoading(model.loading, message.connection)
1033
+ };
1034
+ case "ConnectionRefreshed": return {
1035
+ ...model,
1036
+ connections: setConnectionStale(model.connections, message.connection, false)
1037
+ };
1038
+ }
1039
+ };
1040
+ /** The failures without one connection's, keeping identity when it had none. */
1041
+ const withoutConnectionFailure = (failures, connection) => {
1042
+ if (!(connection in failures.connections)) return failures;
1043
+ const { [connection]: _settled, ...connections } = failures.connections;
1044
+ return {
1045
+ ...failures,
1046
+ connections
1047
+ };
1048
+ };
1049
+ /** The failures with every field a read asked for failed with `error`. */
1050
+ const withFieldFailures = (failures, requests, error) => {
1051
+ const fields = { ...failures.fields };
1052
+ for (const mark of fieldMarks(requests)) fields[mark] = error;
1053
+ return {
1054
+ ...failures,
1055
+ fields
1056
+ };
1057
+ };
1058
+ /**
1059
+ * The failures without the fields `settled` names, keeping identity when none
1060
+ * of them had failed, which is nearly always: this runs on every write.
1061
+ */
1062
+ const withoutFieldFailures = (failures, settled) => {
1063
+ if (Object.keys(failures.fields).length === 0) return failures;
1064
+ const marks = [...settled].filter((mark) => mark in failures.fields);
1065
+ if (marks.length === 0) return failures;
1066
+ const fields = { ...failures.fields };
1067
+ for (const mark of marks) delete fields[mark];
1068
+ return {
1069
+ ...failures,
1070
+ fields
1071
+ };
1072
+ };
1073
+ /** Every failed field of one entity: what deleting the entity settles. */
1074
+ const entityFailureMarks = (failures, key) => Object.keys(failures.fields).filter((mark) => mark.startsWith(`${key}\u0000`));
1075
+ /** The field marks of written values: each patch settles the fields it carries. */
1076
+ const patchedMarks = function* (patches) {
1077
+ for (const patch of patches) for (const field of Object.keys(patch.values)) yield fieldMark(patch.entity, patch.id, field);
1078
+ };
1079
+ /**
1080
+ * Every field a read's requests reach: their own, and through each relation
1081
+ * the store already holds, the fields they ask of its targets. A request with
1082
+ * a relation riding on it names no target id; the store does, and a failure
1083
+ * recorded on a target is reached only this way.
1084
+ */
1085
+ const reachedMarks = (store, requests) => {
1086
+ const marks = [];
1087
+ const walked = /* @__PURE__ */ new Map();
1088
+ const walk = (entity, id, requirement) => {
1089
+ const key = entityKey(entity, id);
1090
+ const seen = walked.get(key) ?? /* @__PURE__ */ new Set();
1091
+ if (seen.has(requirement)) return;
1092
+ seen.add(requirement);
1093
+ walked.set(key, seen);
1094
+ for (const field of requirement.fields) {
1095
+ marks.push(fieldMark(entity, id, field));
1096
+ const nested = requirement.relations?.[field];
1097
+ if (nested === void 0) continue;
1098
+ const value = readField(store, key, field);
1099
+ if (value._tag === "None") continue;
1100
+ for (const ref of targetsOf(value.value, nested)) walk(ref.entity, ref.id, nested);
1101
+ }
1102
+ };
1103
+ for (const request of requests) walk(request.entity, request.id, request);
1104
+ return marks;
1105
+ };
1106
+ /** The entity key a field mark belongs to. */
1107
+ const markEntity = (mark) => mark.slice(0, mark.indexOf("\0"));
1108
+ /**
1109
+ * The failures of what the roots still reach. A released connection's or
1110
+ * entity's failure goes with it, so asking for it again later asks the server
1111
+ * again rather than repeating an error that may no longer hold.
1112
+ *
1113
+ * Reached is retention's own walk, taken over the Model before collection. It
1114
+ * counts a relation's target and a list's row that the store does not hold,
1115
+ * which is exactly a value whose first read failed: it is still on screen,
1116
+ * behind the parent that refers to it.
1117
+ */
1118
+ const prunedFailures = (failures, roots, reached) => {
1119
+ const named = new Set(roots.connections.map((root) => root.identity));
1120
+ const connections = Object.entries(failures.connections).filter(([identity]) => named.has(identity));
1121
+ const fields = Object.keys(failures.fields).length === 0 ? [] : Object.entries(failures.fields).filter(([mark]) => reached().has(markEntity(mark)));
1122
+ return connections.length === Object.keys(failures.connections).length && fields.length === Object.keys(failures.fields).length ? failures : {
1123
+ connections: Object.fromEntries(connections),
1124
+ fields: Object.fromEntries(fields)
1125
+ };
1126
+ };
1127
+ /** Whether this field's last read failed and nothing has settled it since. */
1128
+ const isFieldFailed = (model, entity, id, field) => fieldMark(entity, id, field) in model.failures.fields;
1129
+ /**
1130
+ * Whether a read is fetching any field `relation` reads of an entity, its own
1131
+ * or, through each relation the store holds, its targets'. A value whose owner
1132
+ * is being fetched is loading, not `Initial`: nothing is missing a fetch.
1133
+ */
1134
+ const isLoadingThrough = (model, entity, id, relation) => {
1135
+ if (model.loading.size === 0) return false;
1136
+ const walked = /* @__PURE__ */ new Map();
1137
+ const walk = (target, targetId, requirement) => {
1138
+ const key = entityKey(target, targetId);
1139
+ const seen = walked.get(key) ?? /* @__PURE__ */ new Set();
1140
+ if (seen.has(requirement)) return false;
1141
+ seen.add(requirement);
1142
+ walked.set(key, seen);
1143
+ for (const field of requirement.fields) {
1144
+ if (model.loading.has(fieldMark(target, targetId, field))) return true;
1145
+ const nested = requirement.relations?.[field];
1146
+ if (nested === void 0) continue;
1147
+ const value = readField(model.entities, key, field);
1148
+ if (value._tag === "None") continue;
1149
+ for (const ref of targetsOf(value.value, nested)) if (walk(ref.entity, ref.id, nested)) return true;
1150
+ }
1151
+ return false;
1152
+ };
1153
+ return walk(entity, id, relation);
1154
+ };
1155
+ /**
1156
+ * The first failure among the fields `relation` reads of an entity, following
1157
+ * each relation the store holds into its targets, as `assemble` does. A read
1158
+ * that consults it shows `Failed` for a related entity's failure too, rather
1159
+ * than waiting on a field nothing will fetch.
1160
+ */
1161
+ const failureOf = (model, entity, id, relation) => {
1162
+ const failed = model.failures.fields;
1163
+ if (Object.keys(failed).length === 0) return void 0;
1164
+ const walked = /* @__PURE__ */ new Map();
1165
+ const walk = (target, targetId, requirement) => {
1166
+ const key = entityKey(target, targetId);
1167
+ const seen = walked.get(key) ?? /* @__PURE__ */ new Set();
1168
+ if (seen.has(requirement)) return void 0;
1169
+ seen.add(requirement);
1170
+ walked.set(key, seen);
1171
+ for (const field of requirement.fields) {
1172
+ const error = failed[fieldMark(target, targetId, field)];
1173
+ if (error !== void 0) return error;
1174
+ const nested = requirement.relations?.[field];
1175
+ if (nested === void 0) continue;
1176
+ const value = readField(model.entities, key, field);
1177
+ if (value._tag === "None") continue;
1178
+ for (const ref of targetsOf(value.value, nested)) {
1179
+ const found = walk(ref.entity, ref.id, nested);
1180
+ if (found !== void 0) return found;
1181
+ }
1182
+ }
1183
+ };
1184
+ return walk(entity, id, relation);
1185
+ };
1186
+ /** Appends (after) or prepends (before) an incoming page onto the stored one. */
1187
+ const mergeWireRefPages = (current, next, direction) => {
1188
+ const refs = direction === "after" ? [.../* @__PURE__ */ new Set([...current.refs, ...next.refs])] : [.../* @__PURE__ */ new Set([...next.refs, ...current.refs])];
1189
+ return direction === "after" ? {
1190
+ refs,
1191
+ hasNext: next.hasNext,
1192
+ hasPrevious: current.hasPrevious
1193
+ } : {
1194
+ refs,
1195
+ hasNext: current.hasNext,
1196
+ hasPrevious: next.hasPrevious
1197
+ };
1198
+ };
1199
+ /**
1200
+ * A mutation answer's read without what a later request settled (#288): a
1201
+ * field it wrote, a row it deleted, and the absence of a row it wrote, which
1202
+ * the read would otherwise tombstone.
1203
+ */
1204
+ const unsettledRead = (read, { newer, rewritten }) => {
1205
+ const entities = read.result.entities.flatMap((entity) => {
1206
+ if (newer(entityKey(entity.entity, entity.id))) return [];
1207
+ const values = Object.fromEntries(Object.entries(entity.values).filter(([field]) => !newer(fieldMark(entity.entity, entity.id, field))));
1208
+ return Object.keys(values).length === 0 ? [] : [{
1209
+ ...entity,
1210
+ values
1211
+ }];
1212
+ });
1213
+ const settled = read.result.settled.flatMap((entry) => {
1214
+ if (newer(entityKey(entry.entity, entry.id))) return [];
1215
+ const fields = entry.fields.filter((field) => !newer(fieldMark(entry.entity, entry.id, field)));
1216
+ return fields.length === 0 ? [] : [{
1217
+ ...entry,
1218
+ fields
1219
+ }];
1220
+ });
1221
+ const answered = new Set([...entities, ...settled].map((entry) => entityKey(entry.entity, entry.id)));
1222
+ return {
1223
+ requests: read.requests.filter((request) => {
1224
+ const key = entityKey(request.entity, request.id);
1225
+ return answered.has(key) || !rewritten(key);
1226
+ }),
1227
+ result: {
1228
+ entities,
1229
+ settled
1230
+ }
1231
+ };
1232
+ };
1233
+ /**
1234
+ * Writes a read result into the store, recording each field's applied window so
1235
+ * a later request with a different window refetches. A relation page requested
1236
+ * with a cursor is merged onto the page already stored, so "load more"
1237
+ * accumulates rather than replaces. Every read path should use this rather than
1238
+ * reducing `writeEntity` by hand.
1239
+ */
1240
+ const writeRead = (store, requests, result, now = 0) => {
1241
+ const returned = /* @__PURE__ */ new Map();
1242
+ for (const entity of result.entities) {
1243
+ const key = entityKey(entity.entity, entity.id);
1244
+ returned.set(key, {
1245
+ ...returned.get(key),
1246
+ ...entity.values
1247
+ });
1248
+ }
1249
+ const byEntity = /* @__PURE__ */ new Map();
1250
+ const record = (key, windows, relations) => {
1251
+ let entry = byEntity.get(key);
1252
+ if (entry === void 0) {
1253
+ entry = {
1254
+ windows: {},
1255
+ merge: /* @__PURE__ */ new Map()
1256
+ };
1257
+ byEntity.set(key, entry);
1258
+ }
1259
+ for (const [field, window] of Object.entries(windows ?? {})) {
1260
+ put(entry.windows, field, windowKey(window));
1261
+ const direction = window.after !== void 0 ? "after" : window.before !== void 0 ? "before" : void 0;
1262
+ if (direction !== void 0) entry.merge.set(field, direction);
1263
+ }
1264
+ const values = returned.get(key);
1265
+ if (values === void 0) return;
1266
+ for (const [field, relation] of Object.entries(relations ?? {})) for (const ref of targetsOf(values[field], relation)) record(entityKey(ref.entity, ref.id), relation.windows, relation.relations);
1267
+ };
1268
+ for (const request of requests) record(entityKey(request.entity, request.id), request.windows, request.relations);
1269
+ const writes = [];
1270
+ const pending = /* @__PURE__ */ new Map();
1271
+ for (const entity of result.entities) {
1272
+ const key = entityKey(entity.entity, entity.id);
1273
+ const entry = byEntity.get(key);
1274
+ let values = entity.values;
1275
+ if (entry !== void 0 && entry.merge.size > 0) {
1276
+ const previous = pending.get(key) ?? store[key]?.values;
1277
+ if (previous !== void 0) {
1278
+ const merged = { ...values };
1279
+ for (const [field, direction] of entry.merge) {
1280
+ const incoming = values[field];
1281
+ const existing = previous[field];
1282
+ if (isRefPage(incoming) && isRefPage(existing)) merged[field] = mergeWireRefPages(existing, incoming, direction);
1283
+ }
1284
+ values = merged;
1285
+ }
1286
+ }
1287
+ pending.set(key, {
1288
+ ...pending.get(key),
1289
+ ...values
1290
+ });
1291
+ writes.push({
1292
+ key,
1293
+ values,
1294
+ windows: entry?.windows
1295
+ });
1296
+ }
1297
+ const settled = new Set(result.settled.map((entry) => entityKey(entry.entity, entry.id)));
1298
+ let answered = store;
1299
+ for (const request of requests) {
1300
+ const key = entityKey(request.entity, request.id);
1301
+ if (!returned.has(key) && !settled.has(key)) answered = tombstone(answered, key);
1302
+ else if (settled.has(key) && isTombstone(answered, key)) answered = remove(answered, key);
1303
+ }
1304
+ return setUnavailable(writeEntities(answered, writes, now), result.settled.map((entry) => [entityKey(entry.entity, entry.id), entry.fields]), true);
1305
+ };
1306
+ export { dismissQueued, emptyRefresh, failureOf, fieldMark, forgetRemote, initialRemoteModel, invalidateConnections, isFieldFailed, isLoading, isLoadingThrough, isQueryLoading, isRemoteMessage, noFailures, onlyOverlaid, overlaysOf, refreshIsInFlight, refreshedAt, remoteMessageCases, remoteMessageSchema, remoteModelSchema, retentionRootsSchema, updateRemote, windowGrowthKey, withRefreshRequested, writeRead };
1307
+
1308
+ //# sourceMappingURL=model.js.map