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/index.js ADDED
@@ -0,0 +1,2791 @@
1
+ import { PredictionPart, predictionMismatch, withDiagnostics } from "./diagnostics.js";
2
+ import { RemoteConnections, RemoteRequirements, Requirement, connectionsOf, requirementsOf } from "./requirement.js";
3
+ import { patchedFields, shownOf } from "./shown.js";
4
+ import { sameData } from "./data.js";
5
+ import { RELATION_ALIAS, RelationAnnotation, RelationEntityAnnotation, aliasedField, isRefPage, refParts, refsIn, relationAlias, relationShape, targetsOf } from "./relation.js";
6
+ import { askAgain, clearStale, emptyStore, entityKey, entry, hasField, isFieldStale, isFieldUnavailable, isTombstone, markStale, missingFields, readField, remove, setStale, setUnavailable, tombstone, writeEntities, writeEntity } from "./store.js";
7
+ import { answerGaps } from "./answerGaps.js";
8
+ import { DerivedBy, Mutation, Refusal, beginMutation, brokenByWrite, derivedOfWrite, emptyMutationState, failMutation, forbiddenByWrite, forgetFailure, mutationStatus, optimisticOfWrite, patchOfWrite, predictionOfWrite, reconcileMutation, schemaOf, settledLater } from "./mutation.js";
9
+ import { IDENTITY_SEPARATOR, Query, queryIdentity, stableStringify } from "./query.js";
10
+ import { AnswerRead, ConnectionChangeSchema, Live, LiveChange, LiveEdge, LiveRequirement, MAX_FIELDS_PER_REQUEST, MAX_LIVE_QUERIES, MAX_RELATION_DEPTH, MAX_SHOWN_ENTITIES, Mutate, MutationRequest, MutationResult, NormalizedEntity, OperationId, PageSize, QueryEdge, QueryRequest, QueryResult, QueryRpc, REMOTE_PROTOCOL_VERSION, Read, ReadBatch, ReadBatchResult, ReadRequest, RelationRequest, RemoteForbidden, RemoteLiveError, RemoteLiveRefused, RemoteMutationError, RemoteProtocolError, RemoteQueryError, RemoteReadError, RemoteRpc, RemoteUnauthenticated, RemoteUnreachable, SettledFields, ShownSlice, WindowSchema, WireBoundary } from "./wire.js";
11
+ import { coalesceQueries, coalesceReads, requirementKey } from "./coalesce.js";
12
+ import { RemoteClient, coalescedLayer, connectionChangeOf, liveEventOf, mintOperation, mutateRemote, remoteError } from "./client.js";
13
+ import { cursor, edge, emptyConnection, hasNext, hasPrevious, isGapped, items, merge, segment, terminal, unknown } from "./connection.js";
14
+ import { a as relationOf, i as pageSchema, n as Selection, o as unavailableOf, r as assemble, t as Entity } from "./entity-DuDxaa3n.js";
15
+ import { RowPlace, placeIn, withEdge } from "./placement.js";
16
+ import { belongsEncoded, matching } from "./matching.js";
17
+ import { EntityChange, Impact, changesOf, impactOn } from "./impact.js";
18
+ import { wireValues } from "./patchValues.js";
19
+ import { Denied, Freshness, RemoteData, remoteDataSchema, remoteErrorSchema } from "./remoteData.js";
20
+ import { ReadWhy, ShownBy, Why, WhyTerm, readWhy, regressed } from "./why.js";
21
+ import { ConnectionChange, addLayer, addOverlay, beginOptimistic, connectionIdentity, emptyOptimistic, goneIn, heldOnlyByRequests, plannedStore, pruneOverlays, removeLayer, removeOverlay, resolvedPatches, settleFailure, settleSuccess, visibleItems, visibleStore } from "./optimistic.js";
22
+ import { applyConnectionEvent, applyEntityEvent, classifyLive, emptyLiveState, liveHasNext, liveHasPrevious } from "./live.js";
23
+ import { OutboxItemState, StoredWrite, backoff, channelFor, headOf, mergeStored, onlineAgain, operationOf, othersWaiting, outboxStatus, readEntries, readStored, sendLock, settledIn, storeLock, storedFromText, storedOf, storedText, withLock } from "./outbox.js";
24
+ import { deadlineOf, plan, windowKey } from "./plan.js";
25
+ import { departedLimit, gc, reachable, recentRoots } from "./retain.js";
26
+ import { REMOTE_CACHE_VERSION, RemotePersistence } from "./persistence.js";
27
+ import { 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 } from "./model.js";
28
+ import { failedReasons, reasonsOf } from "./whyReasons.js";
29
+ import { inspectEntity, inspectRemote } from "./inspect.js";
30
+ import { RemotePolicy } from "./policy.js";
31
+ import { markAll, resumePart } from "./resume.js";
32
+ import { reduceReporting } from "./reporting.js";
33
+ import { RemoteJsonAnswer, RemoteJsonRequest, http, json } from "./json.js";
34
+ import { httpWithLive, liveFetch } from "./sse.js";
35
+ import { Clock, Duration, Effect, Layer, Match, Option, Queue, Record, Result, Schema, Stream } from "effect";
36
+ import { Diagnostics, ModelRef } from "foldkit-surface";
37
+ import { Derivation, Entity as Entity$1, Operators, Query as Query$1, QueryEvaluateError, ReadFilter, SelectionTypeId } from "foldkit-entity";
38
+ import { KeyValueStore } from "effect/persistence";
39
+ import { mapMessage } from "foldkit/command";
40
+ import { defineMessageUnion } from "foldkit/message";
41
+ import * as Subscription from "foldkit/subscription";
42
+ // src/index.ts
43
+ /**
44
+ * `foldkit-remote` — normalized application-facing server state.
45
+ *
46
+ * The pure core (entities, selections, the store, the planner, connections,
47
+ * live classification, optimistic layers) performs no I/O. The `Remote.*`
48
+ * helpers that read or mutate go through the `RemoteClient` Effect service.
49
+ */
50
+ /** A body's dependencies as an explanation carries them: without the owner identity, which holds a symbol. */
51
+ const explainedDependencies = (dependencies) => ({
52
+ fields: dependencies.fields.map(({ entity, key }) => ({
53
+ entity,
54
+ key
55
+ })),
56
+ inputs: dependencies.inputs,
57
+ operations: dependencies.operations,
58
+ operators: dependencies.operators.map(({ id, version }) => ({
59
+ id,
60
+ version
61
+ }))
62
+ });
63
+ const descriptorOf = (entity) => Entity$1.is(entity) ? Entity.from(entity) : entity;
64
+ const selectionOf = (selection) => SelectionTypeId in selection ? Selection.from(selection) : selection;
65
+ const readContract = (source, shape, window) => {
66
+ const ref = {
67
+ ...source,
68
+ window: pickWindow(window)
69
+ };
70
+ const relation = relationOf(shape);
71
+ return {
72
+ ref,
73
+ relation,
74
+ requirement: {
75
+ identity: ref.identity,
76
+ window: ref.window,
77
+ select: relation,
78
+ ref
79
+ }
80
+ };
81
+ };
82
+ /**
83
+ * A read contract whose connection, when a client can place rows in it, also
84
+ * reads every field its body filters and orders by: a changed row is then
85
+ * compared with neighbours that hold those fields. What the view reads, and so
86
+ * decodes, stays its Selection.
87
+ */
88
+ const placeableReadContract = (contract, body, operators) => {
89
+ if (body === void 0) return contract;
90
+ const { requirement, ref } = contract;
91
+ const encoded = Schema.encodeSync(ref.Input)(ref.input);
92
+ if (Query$1.placement(body, encoded, { operators })._tag !== "Placeable") return contract;
93
+ const missing = Query$1.dependencies(body).fields.filter((field) => !requirement.select.fields.includes(field.key));
94
+ if (missing.length === 0) return contract;
95
+ return {
96
+ ...contract,
97
+ requirement: {
98
+ ...requirement,
99
+ select: {
100
+ ...requirement.select,
101
+ fields: [...requirement.select.fields, ...missing.map((field) => field.key)]
102
+ }
103
+ }
104
+ };
105
+ };
106
+ const metaOf = (remote, asked, operators) => {
107
+ const visible = visibleStoreOf(remote.entities, remote.optimistic, operators);
108
+ let updatedAt;
109
+ let stale = false;
110
+ let loading = false;
111
+ const seen = /* @__PURE__ */ new Set();
112
+ const touch = (entity, id, requirement) => {
113
+ const key = entityKey(entity, id);
114
+ const memo = `${key}\u0000${stableStringify(requirement)}`;
115
+ if (seen.has(memo)) return;
116
+ seen.add(memo);
117
+ const entry = visible[key];
118
+ if (entry !== void 0 && !entry.tombstone) for (const field of requirement.fields) {
119
+ if (!entry.present.has(field)) continue;
120
+ if (Number.isFinite(entry.updatedAt)) updatedAt = updatedAt === void 0 ? entry.updatedAt : Math.max(updatedAt, entry.updatedAt);
121
+ if (entry.stale.has(field)) stale = true;
122
+ }
123
+ if (isLoadingThrough(remote, entity, id, requirement)) loading = true;
124
+ for (const [field, relation] of Object.entries(requirement.relations ?? {})) {
125
+ const value = readField(visible, key, field);
126
+ if (value._tag === "None" || value.value === null || value.value === void 0) continue;
127
+ for (const ref of targetsOf(value.value, relation)) touch(ref.entity, ref.id, relation);
128
+ }
129
+ };
130
+ for (const requirement of asked.requirements) touch(requirement.entity, requirement.id, requirement);
131
+ for (const connection of asked.connections) {
132
+ const known = remote.connections[connection.identity];
133
+ if (known === void 0) {
134
+ if (isQueryLoading(remote, connection.identity)) loading = true;
135
+ continue;
136
+ }
137
+ if (known.stale) stale = true;
138
+ if (isQueryLoading(remote, connection.identity)) loading = true;
139
+ for (const edge of visibleItems(known, connection.identity, remote.optimistic.overlays, goneIn(remote.entities, remote.optimistic))) {
140
+ if (edge.ref.entity !== connection.select.entity) continue;
141
+ touch(edge.ref.entity, edge.ref.id, connection.select);
142
+ }
143
+ }
144
+ return {
145
+ updatedAt,
146
+ stale,
147
+ loading
148
+ };
149
+ };
150
+ /**
151
+ * The store a read or plan sees: the base with every pending optimistic layer
152
+ * applied, so a request's patches show until it settles and a temporary id is
153
+ * not planned as a fetch.
154
+ */
155
+ const storeOf = (bound, model) => {
156
+ const remote = bound.store.get(model);
157
+ return visibleStoreOf(remote.entities, remote.optimistic, bound.operators);
158
+ };
159
+ /**
160
+ * The Model as it would be with nothing optimistic pending. Every read reaches
161
+ * the layers through the Model it is given, so dropping them here is what makes
162
+ * a read confirmed — no read has to know it is being read confirmed.
163
+ */
164
+ const withoutOptimistic = (bound, model) => {
165
+ const remote = bound.store.get(model);
166
+ return remote.optimistic.layers.length === 0 && remote.optimistic.overlays.length === 0 ? model : bound.store.set(model, {
167
+ ...remote,
168
+ optimistic: emptyOptimistic
169
+ });
170
+ };
171
+ const confirmed = (bound, projection) => ({
172
+ ...projection,
173
+ read: (root) => projection.read(withoutOptimistic(bound, root))
174
+ });
175
+ const visibleStores = /* @__PURE__ */ new WeakMap();
176
+ const visibleStoreOf = (entities, optimistic, operators) => {
177
+ let byLayers = visibleStores.get(operators);
178
+ if (byLayers === void 0) {
179
+ byLayers = /* @__PURE__ */ new WeakMap();
180
+ visibleStores.set(operators, byLayers);
181
+ }
182
+ let byStore = byLayers.get(optimistic.layers);
183
+ if (byStore === void 0) {
184
+ byStore = /* @__PURE__ */ new WeakMap();
185
+ byLayers.set(optimistic.layers, byStore);
186
+ }
187
+ let visible = byStore.get(entities);
188
+ if (visible === void 0) {
189
+ visible = visibleStore(entities, optimistic, operators);
190
+ byStore.set(entities, visible);
191
+ }
192
+ return visible;
193
+ };
194
+ const plannedStores = /* @__PURE__ */ new WeakMap();
195
+ /** `plannedStore` of the visible store, kept by it. */
196
+ const plannedStoreOf = (entities, optimistic, visible) => {
197
+ let planned = plannedStores.get(visible);
198
+ if (planned === void 0) {
199
+ planned = plannedStore(entities, visible, optimistic);
200
+ plannedStores.set(visible, planned);
201
+ }
202
+ return planned;
203
+ };
204
+ /**
205
+ * `Failed` naming a field the server settled without a value, when one is
206
+ * among what `relation` reads of the entity; otherwise nothing, and the store
207
+ * merely lacks the value. The reason stays with the server, so a view shows
208
+ * the field it cannot have, not why.
209
+ */
210
+ const unavailableFailure = (store, key, relation) => {
211
+ const field = unavailableOf(store, key, relation);
212
+ return field === void 0 ? void 0 : {
213
+ _tag: "Failed",
214
+ error: {
215
+ _tag: "Unavailable",
216
+ message: `The server answered without ${field}, and will not answer with it: the field is not available to this client.`
217
+ }
218
+ };
219
+ };
220
+ /**
221
+ * A row's decoded value, kept while the data it was assembled from is the same,
222
+ * so a refetch or live patch that brought equal data hands a view the object it
223
+ * already rendered. Keyed on the row's own values object, which the store keeps
224
+ * across equal writes; a relation's targets are covered by comparing what was
225
+ * assembled.
226
+ */
227
+ const decodedRows = /* @__PURE__ */ new WeakMap();
228
+ const decodeRow = (schema, source, values) => {
229
+ let bySchema = decodedRows.get(schema);
230
+ if (bySchema === void 0) {
231
+ bySchema = {
232
+ decode: Schema.decodeUnknownResult(schema),
233
+ rows: /* @__PURE__ */ new WeakMap()
234
+ };
235
+ decodedRows.set(schema, bySchema);
236
+ }
237
+ const cached = bySchema.rows.get(source);
238
+ if (cached !== void 0 && sameData(cached.values, values)) return cached.decoded;
239
+ const decoded = bySchema.decode(values);
240
+ bySchema.rows.set(source, {
241
+ values,
242
+ decoded
243
+ });
244
+ return decoded;
245
+ };
246
+ const readResults = {
247
+ inner: /* @__PURE__ */ new WeakMap(),
248
+ results: /* @__PURE__ */ new Map()
249
+ };
250
+ const memoRead = (scopes, key, compute) => {
251
+ let scope = readResults;
252
+ for (const by of scopes) {
253
+ let inner = scope.inner.get(by);
254
+ if (inner === void 0) {
255
+ inner = {
256
+ inner: /* @__PURE__ */ new WeakMap(),
257
+ results: /* @__PURE__ */ new Map()
258
+ };
259
+ scope.inner.set(by, inner);
260
+ }
261
+ scope = inner;
262
+ }
263
+ if (scope.results.has(key)) return scope.results.get(key);
264
+ const value = compute();
265
+ scope.results.set(key, value);
266
+ return value;
267
+ };
268
+ /**
269
+ * The default clock. It reads `Date.now` when called rather than holding the
270
+ * function, so fake timers installed after the domain was made move it.
271
+ */
272
+ const wallClock = () => Date.now();
273
+ /** The default `toMessage`: the application reduces `RemoteMessage` itself. */
274
+ const identityMessage = (message) => message;
275
+ /**
276
+ * `Data.satisfy` reached its bound with Surfaces still reading, such as a
277
+ * chain whose every read reveals one more. It names them, so a page that
278
+ * would have rendered them loading fails instead. (A read the server leaves
279
+ * unanswered settles as missing and ends the loop.)
280
+ */
281
+ var RemoteUnsatisfied = class extends Schema.TaggedError()("RemoteUnsatisfied", {
282
+ surfaces: Schema.Array(Schema.String),
283
+ passes: Schema.Number
284
+ }) {};
285
+ /** A stable key for a live subscription's rows and lists. */
286
+ const liveStreamKey = ({ requirements, queries }) => [...requirements.map((requirement) => `${entityKey(requirement.entity, requirement.id)}:${[...requirement.fields].sort().join(",")}`), ...queries.map((query) => stableStringify(query))].sort().join("|");
287
+ /** Each registered Entity's `Derived.count`s and `Derived.exists`, under the pointing Entity. */
288
+ const derivedMembersBy = (entities) => {
289
+ const byPointing = /* @__PURE__ */ new Map();
290
+ const add = (pointing, member) => {
291
+ const members = byPointing.get(pointing) ?? [];
292
+ members.push(member);
293
+ byPointing.set(pointing, members);
294
+ };
295
+ for (const entity of entities) {
296
+ if (!Entity$1.is(entity)) continue;
297
+ const derived = entity.derived;
298
+ for (const [field, member] of Object.entries(derived)) Derivation.$match(member.derives, {
299
+ Supplied: () => void 0,
300
+ Count: ({ relation }) => add(relation.owner.name, DerivedBy.Count({
301
+ relation: relation.key,
302
+ owner: entity.name,
303
+ field
304
+ })),
305
+ Exists: ({ relation, viewer }) => add(relation.owner.name, DerivedBy.Exists({
306
+ relation: relation.key,
307
+ viewer: viewer.key,
308
+ owner: entity.name,
309
+ field
310
+ }))
311
+ });
312
+ }
313
+ return byPointing;
314
+ };
315
+ /** Declares a Remote domain: its entities, queries, and mutations, plus the submodel. */
316
+ const defineRemote = (config) => {
317
+ const entities = config.entities.map(descriptorOf);
318
+ return {
319
+ entities,
320
+ queries: config.queries ?? [],
321
+ mutations: config.mutations ?? [],
322
+ Model: remoteModelSchema(),
323
+ initial: initialRemoteModel,
324
+ Message: remoteMessageSchema,
325
+ update: updateRemote,
326
+ rpc: RemoteRpc,
327
+ registry: {
328
+ entities: new Map(entities.map((entity) => [entity.name, entity])),
329
+ queries: new Map((config.queries ?? []).map((query) => [query.name, query])),
330
+ mutations: new Map((config.mutations ?? []).map((mutation) => [mutation.name, mutation])),
331
+ filtered: new Map(config.entities.flatMap((entity) => Entity$1.is(entity) ? [[entity.name, ReadFilter.members(entity.metadata)]] : [])),
332
+ derived: derivedMembersBy(config.entities)
333
+ }
334
+ };
335
+ };
336
+ /** Binds a Remote domain to its store's location in the application Model. */
337
+ const bindRemote = (definition, ref, operators) => {
338
+ const store = ModelRef.focus(ref);
339
+ return {
340
+ definition,
341
+ store,
342
+ operators,
343
+ contract: {
344
+ kind: "remote",
345
+ name: store.dependency.join(".") || "remote",
346
+ owner: store.owner,
347
+ owns: store.dependency.length === 0 ? [] : [store.dependency],
348
+ observes: store.dependency.length === 0 ? [] : [store.dependency],
349
+ messages: [],
350
+ metadata: []
351
+ }
352
+ };
353
+ };
354
+ /** The runtime half of `Registered`: a descriptor the domain never declared is an error naming both. */
355
+ const assertRegistered = (bound, kind, registered, name) => {
356
+ if (!registered.has(name)) throw new Error(`Remote: ${kind} "${name}" is not registered with domain "${bound.contract.name}"`);
357
+ };
358
+ const askedOf = (projection) => ({
359
+ requirements: requirementsOf(projection),
360
+ connections: connectionsOf(projection)
361
+ });
362
+ /**
363
+ * Whether `outer` asks for everything `inner` does of one entity: each field,
364
+ * each relation's own slice of its target, and each page window alike.
365
+ */
366
+ const sliceCovers = (outer, inner) => outer.entity === inner.entity && inner.fields.every((field) => outer.fields.includes(field)) && Object.entries(inner.relations ?? {}).every(([field, relation]) => {
367
+ const wider = outer.relations?.[field];
368
+ return wider !== void 0 && sliceCovers(wider, relation);
369
+ }) && Object.entries(inner.windows ?? {}).every(([field, window]) => outer.windows?.[field] !== void 0 && stableStringify(outer.windows[field]) === stableStringify(window));
370
+ /**
371
+ * Whether what `outer` asks includes everything `inner` asks: each list with
372
+ * the slice it selects of its rows, and each entity with the slice it reads,
373
+ * relations included. A Surface that shows a read is one whose projection
374
+ * asks for all of it; one that reads part of it cannot be why it is fetched.
375
+ * A Surface that asks for one entity in several pieces is already one
376
+ * requirement here: projection metadata unions them per entity and id.
377
+ */
378
+ const covers = (outer, inner) => {
379
+ const { connections, requirements } = outer;
380
+ return inner.connections.every((connection) => connections.some((wider) => wider.identity === connection.identity && (connection.select === void 0 || wider.select !== void 0 && sliceCovers(wider.select, connection.select)))) && inner.requirements.every((requirement) => requirements.some((wider) => wider.id === requirement.id && sliceCovers(wider, requirement)));
381
+ };
382
+ /**
383
+ * Plans what a projection asks against the Model. A connection the Model holds
384
+ * fresh contributes its visible items' selected fields to the entity plan; one
385
+ * it does not hold, or holds stale, is a query to run (its items are planned
386
+ * once the page arrives).
387
+ *
388
+ * A connection whose query failed is not run again here, short of `force`: the
389
+ * failure is what its read shows, and retrying is `Remote.refresh`'s to ask
390
+ * for. The rows it already holds are still planned, since they are on screen.
391
+ */
392
+ /**
393
+ * One requirement per connection and direction: of the sized windows reading
394
+ * a connection from its start (or end), the widest, which covers the rest.
395
+ * Windows with a cursor are pages of their own and stay as they are.
396
+ */
397
+ const widest = (remote, connections) => {
398
+ const kept = /* @__PURE__ */ new Map();
399
+ const rest = [];
400
+ for (const connection of connections) {
401
+ const size = windowSize(remote, connection);
402
+ if (size === void 0) {
403
+ rest.push(connection);
404
+ continue;
405
+ }
406
+ const key = `${connection.identity}${String.fromCharCode(0)}${connection.window.first === void 0 ? "last" : "first"}`;
407
+ const current = kept.get(key);
408
+ if (current === void 0) kept.set(key, connection);
409
+ else if (size > (windowSize(remote, current) ?? 0)) kept.set(key, {
410
+ ...connection,
411
+ select: Requirement.mergeRelation(current.select, connection.select)
412
+ });
413
+ else kept.set(key, {
414
+ ...current,
415
+ select: Requirement.mergeRelation(current.select, connection.select)
416
+ });
417
+ }
418
+ return [...kept.values(), ...rest];
419
+ };
420
+ const withWindow = (connection, window) => ({
421
+ ...connection,
422
+ window,
423
+ ref: {
424
+ ...connection.ref,
425
+ window
426
+ }
427
+ });
428
+ /**
429
+ * The page a connection lacks for a window of `size` from its start (or end):
430
+ * the rows after its first segment's end cursor (or before its last's start).
431
+ * Undefined when it holds enough, reaches the end, or does not know where it
432
+ * starts.
433
+ */
434
+ const missingRows = (known, window, size) => {
435
+ const forward = window.first !== void 0;
436
+ const segment = forward ? known.segments[0] : known.segments[known.segments.length - 1];
437
+ if (segment === void 0) return void 0;
438
+ const [from, to] = forward ? [segment.start, segment.end] : [segment.end, segment.start];
439
+ if (from._tag !== "Terminal" || to._tag !== "Cursor" || segment.edges.length >= size) return void 0;
440
+ const lacking = size - segment.edges.length;
441
+ return forward ? {
442
+ first: lacking,
443
+ after: to.cursor
444
+ } : {
445
+ last: lacking,
446
+ before: to.cursor
447
+ };
448
+ };
449
+ const planAsked = (remote, asked, options, operators) => {
450
+ const queries = [];
451
+ const items = [];
452
+ const connections = Requirement.mergeConnections(asked.connections);
453
+ for (const connection of widest(remote, connections)) {
454
+ const known = remote.connections[connection.identity];
455
+ const failed = options.force !== true && connection.identity in remote.failures.connections;
456
+ const size = windowSize(remote, connection);
457
+ if (!failed && (known === void 0 || known.stale || options.force === true)) {
458
+ queries.push(size === void 0 ? connection : withWindow(connection, connection.window.first === void 0 ? { last: size } : { first: size }));
459
+ continue;
460
+ }
461
+ if (known === void 0) continue;
462
+ const tail = failed || size === void 0 ? void 0 : missingRows(known, connection.window, size);
463
+ if (tail !== void 0) queries.push(withWindow(connection, tail));
464
+ const edges = visibleItems(known, connection.identity, remote.optimistic.overlays, goneIn(remote.entities, remote.optimistic));
465
+ items.push(...itemsOf(edges.map((edge) => edge.ref), connection.select));
466
+ }
467
+ const visible = visibleStoreOf(remote.entities, remote.optimistic, operators);
468
+ const read = [...asked.requirements, ...items];
469
+ const held = heldOnlyByRequests(remote.entities, remote.optimistic);
470
+ const sent = plan(plannedStoreOf(remote.entities, remote.optimistic, visible), read, options).filter((requirement) => !held.has(entityKey(requirement.entity, requirement.id)));
471
+ return {
472
+ requirements: options.force === true ? sent : withoutFailedFields(remote, sent),
473
+ queries,
474
+ expires: options.freshness === void 0 ? null : deadlineOf(visible, read, options.freshness) ?? null
475
+ };
476
+ };
477
+ /**
478
+ * The plan without the fields whose last read failed. Like a failed query,
479
+ * a failed field is shown rather than retried on its own, so a persistent
480
+ * error is not asked again on every unrelated restart of the read entry. A
481
+ * field leaving the request takes its window and its relation with it.
482
+ */
483
+ const withoutFailedFields = (remote, planned) => {
484
+ if (Object.keys(remote.failures.fields).length === 0) return planned;
485
+ return planned.flatMap((requirement) => {
486
+ const fields = requirement.fields.filter((field) => !isFieldFailed(remote, requirement.entity, requirement.id, field));
487
+ if (fields.length === requirement.fields.length) return [requirement];
488
+ if (fields.length === 0) return [];
489
+ const kept = new Set(fields);
490
+ const only = (record) => record === void 0 ? void 0 : Object.fromEntries(Object.entries(record).filter(([field]) => kept.has(field)));
491
+ const windows = only(requirement.windows);
492
+ const relations = only(requirement.relations);
493
+ return [{
494
+ entity: requirement.entity,
495
+ id: requirement.id,
496
+ fields,
497
+ ...windows === void 0 || Object.keys(windows).length === 0 ? {} : { windows },
498
+ ...relations === void 0 || Object.keys(relations).length === 0 ? {} : { relations },
499
+ ...requirement.live === void 0 ? {} : { live: requirement.live }
500
+ }];
501
+ });
502
+ };
503
+ /** The entity requirements a page's edges add under a selection. */
504
+ const itemsOf = (edges, select) => edges.filter((edge) => edge.entity === select.entity).map((edge) => ({
505
+ ...select,
506
+ id: edge.id
507
+ }));
508
+ /**
509
+ * The wire request of a planned query; a `QueryRef` identity carries the query
510
+ * name and the canonical encoded input. A connection requirement built by hand
511
+ * with another identity cannot be run, and fails as a query would.
512
+ */
513
+ const parsedQueryRequest = (query) => {
514
+ const separator = query.identity.indexOf("\0");
515
+ if (separator < 0) return Result.fail(new RemoteQueryError({ message: `connection "${query.identity}" is not a query's: only a QueryRef identity can be run` }));
516
+ try {
517
+ return Result.succeed({
518
+ query: query.identity.slice(0, separator),
519
+ input: JSON.parse(query.identity.slice(separator + 1)),
520
+ window: query.window,
521
+ ...query.select === void 0 ? {} : { select: query.select }
522
+ });
523
+ } catch {
524
+ return Result.fail(new RemoteQueryError({ message: `connection "${query.identity}" carries no encoded input` }));
525
+ }
526
+ };
527
+ const queryRequestOf = (query) => Effect.suspend(() => Result.match(parsedQueryRequest(query), {
528
+ onFailure: Effect.fail,
529
+ onSuccess: Effect.succeed
530
+ }));
531
+ /**
532
+ * The live rows and lists of what projections ask. A list read live is a
533
+ * `Data.query`, whose identity is always a query's, so one that is not is
534
+ * this package's bug, and throws.
535
+ */
536
+ const liveAskOf = (asked) => ({
537
+ requirements: asked.requirements.filter((requirement) => requirement.live === true),
538
+ queries: asked.connections.flatMap((connection) => connection.live === true ? [Result.getOrThrow(parsedQueryRequest(connection))] : [])
539
+ });
540
+ /**
541
+ * The `ConnectionMerged` for a query result, in the client's edge shape.
542
+ * `refreshes` marks the page as answering the connection's refresh, so one
543
+ * Message both merges it and clears `stale`.
544
+ */
545
+ /**
546
+ * The page a window asked for, or the number of edges that came back instead.
547
+ *
548
+ * A window is a request the client made and the server answered, so more edges
549
+ * than were asked for is a protocol disagreement rather than a windfall: the
550
+ * client cannot tell which of them the window meant, the connection's
551
+ * boundaries stop describing what it holds, and a `first: 25` that quietly
552
+ * becomes a thousand is a memory event with no error attached to it.
553
+ *
554
+ * `undefined` when the page is within its window, or when no size was asked
555
+ * for — `after`/`before` with no `first`/`last` bounds nothing.
556
+ */
557
+ const overrun = (window, edges) => {
558
+ const asked = window.first ?? window.last;
559
+ return asked !== void 0 && edges.length > asked ? {
560
+ asked,
561
+ got: edges.length
562
+ } : void 0;
563
+ };
564
+ const pageMessage = (connection, result, refreshes = false, window = {}, select, now) => {
565
+ const over = overrun(window, result.edges);
566
+ if (over !== void 0) return {
567
+ _tag: "QueryFailed",
568
+ connection,
569
+ error: {
570
+ _tag: "RemoteProtocolError",
571
+ message: `the server returned ${over.got} edges for a window of ${over.asked}`
572
+ }
573
+ };
574
+ const hasPayload = select !== void 0 && (result.entities !== void 0 && result.entities.length > 0 || result.settled !== void 0 && result.settled.length > 0);
575
+ return {
576
+ _tag: "ConnectionMerged",
577
+ connection,
578
+ page: {
579
+ edges: result.edges.map((edge) => ({
580
+ key: edge.key,
581
+ ref: {
582
+ entity: edge.entity,
583
+ id: edge.id
584
+ }
585
+ })),
586
+ start: result.start,
587
+ end: result.end
588
+ },
589
+ ...refreshes ? { refreshes } : {},
590
+ ...select === void 0 || !hasPayload ? {} : { select },
591
+ ...hasPayload ? {
592
+ entities: [...result.entities ?? []],
593
+ settled: [...result.settled ?? []],
594
+ now: now ?? 0
595
+ } : {}
596
+ };
597
+ };
598
+ /**
599
+ * The retention roots of some projections: their requirements, their
600
+ * connections (each with the union of what the projections select of its
601
+ * items), and the connections listed by identity alone.
602
+ */
603
+ const rootsOf = (projections, options) => {
604
+ const connections = /* @__PURE__ */ new Map();
605
+ for (const identity of (options.connections ?? []).map(connectionIdentity)) connections.set(identity, { identity });
606
+ for (const { identity, select } of projections.flatMap(connectionsOf)) {
607
+ const current = connections.get(identity)?.select;
608
+ connections.set(identity, {
609
+ identity,
610
+ select: current === void 0 ? select : Requirement.mergeRelation(current, select)
611
+ });
612
+ }
613
+ return {
614
+ requirements: Requirement.merge(projections.flatMap(requirementsOf)),
615
+ connections: [...connections.values()].sort((a, b) => a.identity < b.identity ? -1 : 1)
616
+ };
617
+ };
618
+ /** Reads requirements and reports the Message that settles them. Never fails. */
619
+ const readMessage = (requirements, now) => Effect.gen(function* () {
620
+ const client = yield* RemoteClient;
621
+ const at = now();
622
+ const result = yield* Effect.result(client.read({
623
+ version: 5,
624
+ requests: requirements
625
+ }));
626
+ return Result.isFailure(result) ? {
627
+ _tag: "ReadFailed",
628
+ requests: requirements,
629
+ error: remoteError(result.failure)
630
+ } : {
631
+ _tag: "ReadReceived",
632
+ requests: requirements,
633
+ result: result.success,
634
+ now: at
635
+ };
636
+ });
637
+ /** Runs a query and reports the page that refreshes its connection, or the failure. Never fails. */
638
+ const queryMessage = (query, now = wallClock) => Effect.gen(function* () {
639
+ const client = yield* RemoteClient;
640
+ const result = yield* Effect.result(queryRequestOf(query).pipe(Effect.flatMap((request) => client.query(request))));
641
+ return Result.isFailure(result) ? {
642
+ _tag: "QueryFailed",
643
+ connection: query.identity,
644
+ error: remoteError(result.failure)
645
+ } : pageMessage(query.identity, result.success, true, query.window, query.select, now());
646
+ });
647
+ /** A query the read entry runs, as its dependencies carry it: plain data Foldkit compares. */
648
+ const PlannedQuery = Schema.Struct({
649
+ identity: Schema.String,
650
+ window: WindowSchema,
651
+ select: RelationRequest
652
+ });
653
+ /**
654
+ * The read entry: plans what `askedOf(model)` asks against the Model and runs
655
+ * the entity read and the queries concurrently. Each Message it emits changes
656
+ * the Model, so Foldkit recomputes the dependencies and restarts the stream
657
+ * (`switchMap`): a merged page's items are planned by that next computation,
658
+ * against the store as it then is, rather than read here unplanned. Nothing
659
+ * the entry needs is spread over two Messages, since the second could be lost
660
+ * to the restart; a page and its refresh are one `ConnectionMerged`.
661
+ */
662
+ const observeEntry = (bound, askedOf, toMessage, options) => {
663
+ const { policy = RemotePolicy.cacheFirst, now = wallClock } = options;
664
+ const read = (requirements) => Effect.map(readMessage(requirements, now), toMessage);
665
+ const run = (query) => Effect.map(queryMessage(query, now), toMessage);
666
+ const planned = /* @__PURE__ */ new WeakMap();
667
+ return {
668
+ dependenciesSchema: Schema.Struct({
669
+ requirements: Schema.Array(ReadRequest),
670
+ queries: Schema.Array(PlannedQuery),
671
+ refresh: Schema.Number,
672
+ expires: Schema.NullOr(Schema.Struct({
673
+ at: Schema.Number,
674
+ due: Schema.Array(ReadRequest)
675
+ }))
676
+ }),
677
+ modelToDependencies: (model) => {
678
+ const remote = bound.store.get(model);
679
+ const asked = askedOf(model);
680
+ const askedKey = stableStringify(asked);
681
+ const at = now();
682
+ let byAsked = planned.get(remote);
683
+ const known = byAsked?.get(askedKey);
684
+ if (known !== void 0 && at < known.until) return known.dependencies;
685
+ const plan = planAsked(remote, asked, RemotePolicy.toPlan(policy, at), bound.operators);
686
+ const dependencies = {
687
+ refresh: refreshedAt(remote.refresh, plan.requirements, plan.queries.map((query) => query.identity)),
688
+ requirements: plan.requirements,
689
+ queries: plan.queries.map(({ identity, window, select }) => ({
690
+ identity,
691
+ window,
692
+ select
693
+ })),
694
+ expires: plan.expires
695
+ };
696
+ if (byAsked === void 0) {
697
+ byAsked = /* @__PURE__ */ new Map();
698
+ planned.set(remote, byAsked);
699
+ }
700
+ byAsked.set(askedKey, {
701
+ until: plan.expires?.at ?? Infinity,
702
+ dependencies
703
+ });
704
+ return dependencies;
705
+ },
706
+ dependenciesToStream: ({ requirements, queries, refresh, expires }) => Stream.concat(Stream.fromIterable([...requirements.length === 0 ? [] : [toMessage({
707
+ _tag: "ReadStarted",
708
+ requests: requirements,
709
+ refresh
710
+ }), ...RemotePolicy.refreshes(policy) ? [toMessage({
711
+ _tag: "RefreshStarted",
712
+ requests: requirements
713
+ })] : []], ...queries.length === 0 ? [] : [toMessage({
714
+ _tag: "QueryStarted",
715
+ connections: queries.map((query) => query.identity)
716
+ })]]), Stream.merge(Stream.mergeAll([...requirements.length === 0 ? [] : [Stream.fromEffect(read(requirements))], ...queries.map((query) => Stream.fromEffect(run(query)))], { concurrency: "unbounded" }), expires === null ? Stream.empty : Stream.fromEffect(Effect.as(Effect.sleep(Math.max(0, expires.at - now())), toMessage({
717
+ _tag: "RefreshStarted",
718
+ requests: expires.due
719
+ })))))
720
+ };
721
+ };
722
+ const resumeCursor = Schema.Number.pipe(Schema.overrideToEquivalence(() => () => true));
723
+ /** The wait before a stream's restart after `failures` breaks in a row: Sync's transport backoff. */
724
+ const restartDelay = (options, failures) => {
725
+ const base = Duration.toMillis(Duration.fromInputUnsafe(options.retryBase ?? "50 millis"));
726
+ const max = Duration.toMillis(Duration.fromInputUnsafe(options.maxRetryDelay ?? "5 seconds"));
727
+ const backoff = Math.min(max, base * 2 ** Math.min(Math.max(failures, 1) - 1, 30));
728
+ return Duration.millis(backoff * (.8 + Math.random() * .4));
729
+ };
730
+ /** The live entry: subscribes to `liveOf(model)` from the Model's resume cursor. */
731
+ const liveEntry = (bound, liveOf, toMessage, options) => ({
732
+ dependenciesSchema: Schema.Struct({
733
+ requirements: Schema.Array(ReadRequest),
734
+ queries: Schema.Array(QueryRequest),
735
+ cursor: resumeCursor,
736
+ floor: Schema.Number,
737
+ restarts: Schema.Number,
738
+ failures: resumeCursor
739
+ }),
740
+ modelToDependencies: (model) => {
741
+ const asked = liveOf(model);
742
+ const stream = liveStreamKey(asked);
743
+ const remote = bound.store.get(model);
744
+ const health = remote.streams[stream];
745
+ return {
746
+ requirements: asked.requirements,
747
+ queries: asked.queries,
748
+ cursor: remote.live[stream]?.cursor ?? 0,
749
+ floor: remote.refresh.floor,
750
+ restarts: health?.restarts ?? 0,
751
+ failures: health?.failures ?? 0
752
+ };
753
+ },
754
+ dependenciesToStream: ({ requirements, queries, cursor, restarts, failures }) => {
755
+ if (requirements.length === 0 && queries.length === 0) return Stream.empty;
756
+ const stream = liveStreamKey({
757
+ requirements,
758
+ queries
759
+ });
760
+ const live = Stream.unwrap(Effect.gen(function* () {
761
+ return (yield* RemoteClient).live({
762
+ requirements,
763
+ queries,
764
+ after: cursor
765
+ });
766
+ })).pipe(Stream.map((event) => toMessage({
767
+ _tag: "LiveReceived",
768
+ stream,
769
+ event,
770
+ now: (options.now ?? wallClock)()
771
+ })), Stream.catchIf((_error) => true, (error) => Stream.succeed(toMessage({
772
+ _tag: "ReadFailed",
773
+ requests: requirements,
774
+ error: remoteError(error),
775
+ stream
776
+ }))));
777
+ if (restarts === 0) return live;
778
+ return Stream.fromEffect(Effect.sleep(restartDelay(options, failures))).pipe(Stream.drain, Stream.concat(Stream.make(toMessage({
779
+ _tag: "RefreshStarted",
780
+ requests: requirements
781
+ }), toMessage({
782
+ _tag: "GapCleared",
783
+ stream
784
+ }))), Stream.concat(live));
785
+ }
786
+ });
787
+ /** The projection an active entry has for this Model; none while it is inactive. */
788
+ const projectionOf = (entry, model) => {
789
+ if ("projectionOf" in entry) return entry.projectionOf(model);
790
+ if ("projection" in entry) return Option.some(entry.projection());
791
+ return Option.none();
792
+ };
793
+ /**
794
+ * Every projection an entry resolves to for a Model: the lone one of a
795
+ * `Surface.at`/`Surface.when` value or a bare Surface, or a family's parent
796
+ * first and then each instance. The parent joins so an unloaded parent is
797
+ * fetched before what it reveals; `Data.satisfy` reaches the instances in a
798
+ * later pass. A caller that also passes the parent separately double-covers
799
+ * its requirements, as overlapping Surfaces already do.
800
+ */
801
+ const resolvedOf = (entry, model) => {
802
+ const source = entry;
803
+ if (typeof source.instancesOf === "function") {
804
+ const instances = source.instancesOf(model);
805
+ const from = entry.from;
806
+ if (from === void 0) return instances.map((instance) => ({
807
+ name: entry.name,
808
+ projection: instance.projection
809
+ }));
810
+ return [...Option.toArray(from.projectionOf(model)).map((projection) => ({
811
+ name: from.name,
812
+ projection
813
+ })), ...instances.map((instance) => ({
814
+ name: `${entry.name}[${instance.key}]`,
815
+ projection: instance.projection
816
+ }))];
817
+ }
818
+ return Option.toArray(projectionOf(entry, model)).map((projection) => ({
819
+ name: entry.name,
820
+ projection
821
+ }));
822
+ };
823
+ /** What some projections ask for together: their requirements and connections unioned. */
824
+ const askedUnion = (projections) => ({
825
+ requirements: projections.flatMap(requirementsOf),
826
+ connections: projections.flatMap(connectionsOf)
827
+ });
828
+ /**
829
+ * `resolvedOf` computed once per Model: the read, live, and retain entries
830
+ * all derive their dependencies from the same projections on the same Model
831
+ * change, and a Surface's `model` callback (which lifts and builds schemas)
832
+ * need not run three times for it. Models are objects, so the memo is weak.
833
+ */
834
+ const memoizedResolved = (entry) => {
835
+ const cache = /* @__PURE__ */ new WeakMap();
836
+ return (model) => {
837
+ const key = model;
838
+ if (typeof key !== "object" || key === null) return resolvedOf(entry, model);
839
+ const known = cache.get(key);
840
+ if (known !== void 0) return known;
841
+ const resolved = resolvedOf(entry, model);
842
+ cache.set(key, resolved);
843
+ return resolved;
844
+ };
845
+ };
846
+ /** Why the server refused a write, as `mutation` declares it: none for any other failure, or one that does not decode. */
847
+ const refusalOf = (error, mutation) => error.refusal === void 0 ? Option.none() : Schema.decodeUnknownOption(mutation.Refusal)(error.refusal);
848
+ /** Refuses an entry of another application: two can share a Model type, and the owner token tells them apart. */
849
+ const assertOwnedBy = (contract, entry) => {
850
+ if (contract.owner === void 0) return;
851
+ const from = entry.from;
852
+ for (const surface of from === void 0 ? [entry] : [entry, from]) if (surface.owner !== contract.owner) throw new Error(`Remote: Surface "${surface.name}" belongs to another application than domain "${contract.name}"`);
853
+ };
854
+ /** Whether a refresh target is one list by its query and input. */
855
+ const isQueryRef = (target) => typeof target.identity === "string";
856
+ /** Whether a refresh target is a projection, the one kind that is not a Surface. */
857
+ const isProjection = (target) => typeof target.read === "function";
858
+ /** Whether a refresh target is one entry of an active record rather than the record. */
859
+ const isActiveEntry = (target) => {
860
+ const members = target;
861
+ return typeof members.projectionOf === "function" || typeof members.instancesOf === "function" || typeof members.projection === "function";
862
+ };
863
+ /** Every projection a refresh target reads for this Model, each entry resolved as the read entries resolve it. */
864
+ const targetProjections = (contract, target, model) => {
865
+ if (isProjection(target)) return [target];
866
+ const entries = isActiveEntry(target) ? [target] : Object.values(target);
867
+ for (const entry of entries) assertOwnedBy(contract, entry);
868
+ return entries.flatMap((entry) => resolvedOf(entry, model).map(({ projection }) => projection));
869
+ };
870
+ const Remote = {
871
+ /** The submodel's schema, the same for every domain; embed it in the application Model. */
872
+ Model: remoteModelSchema(),
873
+ /** The submodel's initial value. */
874
+ initial: initialRemoteModel,
875
+ /**
876
+ * Remote's Message cases for `defineMessageUnion`: spread them into the
877
+ * application's union, and reduce the ones `Remote.reduces` recognizes with
878
+ * `RemoteDomain.reduce`.
879
+ */
880
+ messages: remoteMessageCases,
881
+ /** Whether a Message is one of Remote's, by tag. */
882
+ reduces: isRemoteMessage,
883
+ /**
884
+ * Remote's Messages as one Schema, for a wrapper variant in the
885
+ * application's union: `GotRemoteMessage: { message: Remote.Message }`. With
886
+ * a wrapper, `update` matches the application's union exhaustively and
887
+ * `Remote.fold` reduces what arrives inside it.
888
+ */
889
+ Message: defineMessageUnion(remoteMessageCases),
890
+ /**
891
+ * A bound domain under one wrapper variant of the application's union, the
892
+ * same shape Foldkit gives a Submodel: `toParentMessage` wraps Remote's
893
+ * Messages, the fold reduces them, and its `fetch`, `mutate`, and
894
+ * `subscriptions` yield the wrapper. Spreading `Remote.messages` and
895
+ * narrowing with `Remote.reduces` remains the shorter path for an
896
+ * application whose `update` only reduces.
897
+ *
898
+ * @example
899
+ * ```ts
900
+ * const Message = defineMessageUnion({ GotRemoteMessage: { message: Remote.Message } })
901
+ * const foldData = Remote.fold(Data, message => Message.GotRemoteMessage({ message }))
902
+ *
903
+ * const update = (model: Model, message: Message): Return =>
904
+ * Message.match(message, {
905
+ * GotRemoteMessage: ({ message }) => foldData(model, message),
906
+ * })
907
+ * const subscriptions = Subscription.make<Model, Message, RemoteClient>()(() =>
908
+ * foldData.subscriptions({ page: Surface.at(ProjectPage, …) }),
909
+ * )
910
+ * ```
911
+ */
912
+ fold: (domain, toParentMessage) => {
913
+ const fold = (model, message) => {
914
+ const reduced = reduceReporting(domain.store, domain.reduce, model, message, true);
915
+ return {
916
+ model: reduced.model,
917
+ commands: (reduced.commands ?? []).map((command) => mapMessage(command, toParentMessage))
918
+ };
919
+ };
920
+ return Object.assign(fold, {
921
+ mutate: (...args) => {
922
+ const started = domain.mutate(...args);
923
+ return {
924
+ ...started,
925
+ command: mapMessage(started.command, toParentMessage)
926
+ };
927
+ },
928
+ retry: (...args) => Option.map(domain.retry(...args), (started) => ({
929
+ ...started,
930
+ command: mapMessage(started.command, toParentMessage)
931
+ })),
932
+ preload: (...args) => mapMessage(domain.preload(...args), toParentMessage),
933
+ subscriptions: (active, options) => markAll(messagesAs(brandEntries(domain.subscriptions(active, options)), toParentMessage))
934
+ });
935
+ },
936
+ /**
937
+ * A patch of one entity's values, for a mutation's `optimistic` list or an
938
+ * overlay. Takes a `foldkit-entity` Entity or a Remote descriptor alike, so
939
+ * the domain you declared once is the one you patch:
940
+ * `Remote.patch(Project, 'p1', { name: 'Apollo II' })`. Values are the
941
+ * fields' wire shape, as the server writes them, except a relation, which is
942
+ * a ref (`owner: Remote.ref(User, 'u1')`), an array of refs, or `null` where
943
+ * it is optional. A ref key typed by hand, or a ref to another Entity, is
944
+ * refused, in the type and when the patch is made.
945
+ */
946
+ patch: (entity, id, values) => {
947
+ const descriptor = descriptorOf(entity);
948
+ return descriptor.patch(id, wireValues(descriptor, values));
949
+ },
950
+ /**
951
+ * A deletion of one entity, for a mutation's `optimistic` list or an
952
+ * overlay: `Remote.tombstone(Post, 'p1')`. Until it settles the row reads
953
+ * `NotFound` and leaves every list and relation, as an answer's `deleted`
954
+ * does; a failure brings it back. Keeping it gone is the answer's to say.
955
+ */
956
+ tombstone: (entity, id) => ({
957
+ _tag: "Delete",
958
+ ...descriptorOf(entity).ref(id)
959
+ }),
960
+ /**
961
+ * A reference to one entity, for `ConnectionChange` and anything else that
962
+ * names a row: `Remote.ref(Project, 'p3')`. Either kind of entity, as with
963
+ * `Remote.patch`.
964
+ */
965
+ ref: (entity, id) => descriptorOf(entity).ref(id),
966
+ /**
967
+ * Declares a Remote domain and binds it to its place in the application Model
968
+ * in one step; the result carries the application-facing operations. The
969
+ * descriptor alone is `Remote.define`, the binding alone `Remote.at`.
970
+ */
971
+ make: (config) => bindDomain(defineRemote(config), config.model, {
972
+ verifyPredictions: config.verifyPredictions,
973
+ operators: config.operators,
974
+ viewer: config.viewer
975
+ }),
976
+ /**
977
+ * Declares a Remote domain without binding it: its entities, queries, and
978
+ * mutations, plus the submodel. For a domain reused across applications or
979
+ * bound in a test; `Remote.make` is the one-step form.
980
+ */
981
+ define: defineRemote,
982
+ /**
983
+ * Binds a Remote domain to its store's location in the application Model. The
984
+ * registered entity names are carried on the returned value, so `Remote.select`
985
+ * rejects a selection for an entity this domain never declared. `operators`
986
+ * is the client's table, which a pending computed assignment is predicted by
987
+ * (`Operators.kernel` when not given), as `Remote.make`'s is.
988
+ */
989
+ at: (definition, store, options = {}) => bindRemote(definition, store, options.operators ?? Operators.kernel),
990
+ /**
991
+ * The resume part for this domain, for `foldkit-ssr`'s `SSR.plan({ parts })`:
992
+ * a server render sends what the plan's active Surfaces read from the store,
993
+ * each connection with its boundaries and each live cursor with it, and
994
+ * nothing else. `id` tells two domains in one application apart.
995
+ */
996
+ resume: resumePart,
997
+ /**
998
+ * A Projection node that reads a `RemoteData` value out of the store. The id
999
+ * is supplied by the caller, usually from a Surface's params. The assembled
1000
+ * value is decoded against the Selection, so malformed server data surfaces
1001
+ * as `Failed` instead of being asserted into `Value`. A present value with a
1002
+ * stale field reads as `Refreshing`: an observer is refetching it.
1003
+ */
1004
+ select: (bound, given) => {
1005
+ const selection = selectionOf(given);
1006
+ assertRegistered(bound, "Entity", bound.definition.registry.entities, selection.entity);
1007
+ const relation = relationOf(selection);
1008
+ const relationKey = stableStringify(relation);
1009
+ return (id) => ({
1010
+ Model: remoteDataSchema(selection.schema),
1011
+ dependencies: [],
1012
+ metadata: RemoteRequirements.of({
1013
+ ...relation,
1014
+ id
1015
+ }),
1016
+ read: (root) => {
1017
+ const store = storeOf(bound, root);
1018
+ const key = entityKey(selection.entity, id);
1019
+ const present = memoRead([store], `${key}\u0000${relationKey}`, () => {
1020
+ if (isTombstone(store, key)) return { _tag: "NotFound" };
1021
+ const assembled = assemble(store, key, relation);
1022
+ if (assembled === void 0) return unavailableFailure(store, key, relation);
1023
+ const decoded = decodeRow(selection.schema, store[key].values, assembled.values);
1024
+ return Result.isFailure(decoded) ? {
1025
+ _tag: "Failed",
1026
+ error: {
1027
+ _tag: "DecodeError",
1028
+ message: decoded.failure.message
1029
+ }
1030
+ } : assembled.refreshing ? {
1031
+ _tag: "Refreshing",
1032
+ value: decoded.success
1033
+ } : {
1034
+ _tag: "Ready",
1035
+ value: decoded.success
1036
+ };
1037
+ });
1038
+ const remote = bound.store.get(root);
1039
+ const failure = failureOf(remote, selection.entity, id, relation);
1040
+ if (present !== void 0) return failure !== void 0 && (present._tag === "Ready" || present._tag === "Refreshing") ? {
1041
+ _tag: "Failed",
1042
+ error: failure,
1043
+ previous: present.value
1044
+ } : present;
1045
+ if (isLoadingThrough(remote, selection.entity, id, relation)) return { _tag: "Loading" };
1046
+ if (failure !== void 0) return {
1047
+ _tag: "Failed",
1048
+ error: failure
1049
+ };
1050
+ if (onlyOverlaid(remote, selection.entity, id)) {
1051
+ const lacking = missingFields(store, key, relation.fields);
1052
+ return {
1053
+ _tag: "Failed",
1054
+ error: {
1055
+ _tag: "Overlaid",
1056
+ message: `${selection.entity} ${id} is shown only by an overlay, which does not hold ${lacking.length === 0 ? "what its relations select" : lacking.join(", ")}; the server has not seen it, so nothing will fetch them.`
1057
+ }
1058
+ };
1059
+ }
1060
+ return { _tag: "Initial" };
1061
+ }
1062
+ });
1063
+ },
1064
+ /**
1065
+ * When what a projection shows was last received, and whether it is stale
1066
+ * or loading. Pure, so a view can show "updated 5s ago" without I/O.
1067
+ *
1068
+ * `updatedAt` is the newest server write among what is shown, as the
1069
+ * store dates it; `undefined` when nothing shown was received (nothing
1070
+ * loaded, or only an optimistic preview). `stale` is any shown field or
1071
+ * connection marked stale; `loading` is any of them in flight. Optimistic
1072
+ * previews do not date: showing only one reads `updatedAt: undefined`.
1073
+ */
1074
+ meta: (bound, model, projection) => metaOf(bound.store.get(model), askedOf(projection), bound.operators),
1075
+ /**
1076
+ * The pure plan for a projection against a Model: the requirements its
1077
+ * remote store does not satisfy, under `options` (freshness, force). A
1078
+ * Surface's projection is `surface.projection(params)`.
1079
+ */
1080
+ plan: (bound, model, projection, options) => planAsked(bound.store.get(model), askedOf(projection), options ?? {}, bound.operators).requirements,
1081
+ /**
1082
+ * The queries a projection needs run before its connections read: those the
1083
+ * Model does not hold, or holds stale (every one under `force`).
1084
+ */
1085
+ planQueries: (bound, model, projection, options) => planAsked(bound.store.get(model), askedOf(projection), options ?? {}, bound.operators).queries.flatMap((query) => query.ref === void 0 ? [] : [query.ref]),
1086
+ /**
1087
+ * The store reads see: the base store under the pending optimistic layers.
1088
+ * One store is shared by every read and plan of a Model whose remote state
1089
+ * has not changed.
1090
+ */
1091
+ storeOf: (bound, model) => storeOf(bound, model),
1092
+ /**
1093
+ * The projection read against the server-derived store alone: the same
1094
+ * requirements, planned the same way, with the pending optimistic layers and
1095
+ * connection overlays left off. See the bound `Remote.confirmed`.
1096
+ */
1097
+ confirmed: (bound, projection) => confirmed(bound, projection),
1098
+ /**
1099
+ * Executes the plan against the `RemoteClient` and returns a new store. Used
1100
+ * for SSR route prefetch, hover prefetch, and tests. Never called during render.
1101
+ * `policy` decides what a present field means (default cache-first); `now`
1102
+ * is the clock it reads, so the planner itself stays pure and time-injected.
1103
+ */
1104
+ prefetch: Effect.fn("Remote.prefetch")(function* (bound, model, projection, options = {}) {
1105
+ const store = storeOf(bound, model);
1106
+ const { policy = RemotePolicy.cacheFirst, now = wallClock } = options;
1107
+ const at = now();
1108
+ const missing = plan(store, requirementsOf(projection), RemotePolicy.toPlan(policy, at));
1109
+ if (missing.length === 0) return store;
1110
+ yield* Effect.annotateCurrentSpan("requirementCount", missing.length);
1111
+ const result = yield* (yield* RemoteClient).read({
1112
+ version: 5,
1113
+ requests: missing
1114
+ });
1115
+ return writeRead(store, missing, result, at);
1116
+ }),
1117
+ /**
1118
+ * Marks what a read requires as due again: a projection, a Surface without
1119
+ * params, one entry of the active record, or the whole record, each entry
1120
+ * resolved for this Model as `subscriptions` resolves it. Called from
1121
+ * `update`: every selected field the store holds reads `Refreshing`, and
1122
+ * every loaded query connection is invalidated. Nothing is fetched here.
1123
+ * The read entries `Data.subscriptions` derives refetch it, because a stale
1124
+ * field or connection is planned again under every policy, so a refresh never
1125
+ * sends a request beside the entry already observing the same data.
1126
+ *
1127
+ * A refreshed connection's page replaces its pages, so items the server removed
1128
+ * or reordered follow it; pages loaded past the first are dropped and paged
1129
+ * again with `next`/`previous`. The read entries restart, so a read or query
1130
+ * sent before the refresh is not applied after it.
1131
+ *
1132
+ * It is also how a failed read is retried: nothing that failed is asked
1133
+ * again on its own, so its read stays `Failed` until this asks. A failed
1134
+ * read with nothing to show reads `Loading` from here, and `Failed` again if
1135
+ * the retry fails. `{ failed: true }` asks again only what failed, which is
1136
+ * what a "Try again" means: what loaded keeps its value and its pages.
1137
+ *
1138
+ * The projection must be observed, which it is while it is on screen. Live
1139
+ * subscriptions are left as they are. For data nothing observes (SSR, tests),
1140
+ * use `Remote.prefetch` with `RemotePolicy.networkOnly`. Refreshing what is
1141
+ * already being refreshed returns the same Model.
1142
+ */
1143
+ refresh: (bound, model, target, options = {}) => {
1144
+ const remote = bound.store.get(model);
1145
+ if (isQueryRef(target)) {
1146
+ assertRegistered(bound, "Query", bound.definition.registry.queries, target.query);
1147
+ return Object.hasOwn(remote.failures.connections, target.identity) || options.failed !== true && remote.connections[target.identity] !== void 0 ? bound.store.set(model, invalidateConnections(remote, [target.identity])) : model;
1148
+ }
1149
+ const asked = askedUnion(targetProjections(bound.contract, target, model));
1150
+ const merged = Requirement.mergeConnections(asked.connections);
1151
+ const items = merged.flatMap((connection) => {
1152
+ const known = remote.connections[connection.identity];
1153
+ return known === void 0 ? [] : itemsOf(visibleItems(known, connection.identity, remote.optimistic.overlays, goneIn(remote.entities, remote.optimistic)).map((edge) => edge.ref), connection.select);
1154
+ });
1155
+ const asking = [...asked.requirements, ...items];
1156
+ const connections = options.failed === true ? merged.filter((connection) => connection.identity in remote.failures.connections) : merged;
1157
+ const requirements = plan(visibleStoreOf(remote.entities, remote.optimistic, bound.operators), options.failed === true ? asking.filter((requirement) => failureOf(remote, requirement.entity, requirement.id, requirement) !== void 0) : asking, { force: true });
1158
+ const invalidated = connections.filter((connection) => remote.connections[connection.identity] !== void 0 || connection.identity in remote.failures.connections).map((connection) => connection.identity);
1159
+ const marked = [...requirements.length === 0 ? [] : [{
1160
+ _tag: "RefreshStarted",
1161
+ requests: requirements
1162
+ }], ...invalidated.map((connection) => ({
1163
+ _tag: "ConnectionInvalidated",
1164
+ connection
1165
+ }))].reduce(updateRemote, remote);
1166
+ const inFlight = requirements.some((requirement) => requirement.fields.some((field) => remote.entities[entityKey(requirement.entity, requirement.id)]?.stale.has(field) === true && refreshIsInFlight(remote.refresh, requirement, field)));
1167
+ if (marked === remote && !inFlight) return model;
1168
+ const refresh = withRefreshRequested(marked.refresh, requirements, invalidated);
1169
+ return bound.store.set(model, {
1170
+ ...marked,
1171
+ refresh
1172
+ });
1173
+ },
1174
+ /**
1175
+ * Forgets everything the server told this Model: values, tombstones,
1176
+ * unavailable fields, connections, live cursors, failures. What is known is
1177
+ * known for a principal, and this is the one boundary for a login, a logout
1178
+ * or a switch of organization: called from `update`, it performs no I/O, and
1179
+ * every active Surface's read and live entries restart, so the screen asks
1180
+ * again as whoever the client now is. A read or stream begun before is
1181
+ * interrupted rather than landing after. A mutation in flight is treated as
1182
+ * applied: its answer writes nothing here.
1183
+ */
1184
+ forget: (bound, model) => bound.store.set(model, forgetRemote(bound.store.get(model))),
1185
+ /**
1186
+ * Writes a read result into the store, recording each field's applied window.
1187
+ * `requests` are the planned requirements the result answers.
1188
+ */
1189
+ writeRead,
1190
+ /**
1191
+ * Adapts an Effect RPC client for `RemoteRpc` to `RemoteClient`, so an
1192
+ * application provides the transport's RPC layer instead of writing the
1193
+ * `LiveChange`-to-`LiveEvent` mapping by hand. The client `RpcClient.make`
1194
+ * builds is accepted as it is: when its transport fails (`RpcClientError`),
1195
+ * the call fails with its own Remote error, which the application shows and
1196
+ * retries like any failed read, query, mutation or live stream.
1197
+ */
1198
+ /**
1199
+ * Remote's client over any way of sending one JSON request and receiving its
1200
+ * answer, for `clientLayer`: a worker, a server in the page. `RemoteServer.answer`
1201
+ * is the other end.
1202
+ */
1203
+ json,
1204
+ /**
1205
+ * Remote's client over HTTP, for `clientLayer`: each call a `POST` of the JSON
1206
+ * request to `url`. `headers` is read per request (a session token, say).
1207
+ */
1208
+ http,
1209
+ /**
1210
+ * Remote's client over HTTP with live, for `clientLayer`: reads, queries,
1211
+ * and mutations as `http` sends them, live requirements as one `POST`
1212
+ * answered with a `text/event-stream` of changes.
1213
+ * `foldkit-remote-server/fetch` is the other end.
1214
+ */
1215
+ httpWithLive,
1216
+ clientLayer: (client, options = {}) => coalescedLayer(Layer.effect(RemoteClient, Effect.gen(function* () {
1217
+ const context = yield* Effect.context();
1218
+ return {
1219
+ read: (batch) => client.FoldkitRemoteRead(batch).pipe(Effect.catchTag(["RpcClientError", "Unreachable"], (error) => Effect.fail(new RemoteReadError({ message: error.message }))), Effect.provideContext(context)),
1220
+ query: (request) => client.FoldkitRemoteQuery(request).pipe(Effect.catchTag(["RpcClientError", "Unreachable"], (error) => Effect.fail(new RemoteQueryError({ message: error.message }))), Effect.provideContext(context)),
1221
+ mutate: (request) => client.FoldkitRemoteMutate(request).pipe(Effect.catchTag("RpcClientError", (error) => Effect.fail(new RemoteUnreachable({ message: error.message }))), Effect.provideContext(context)),
1222
+ live: ({ requirements, queries, after }) => client.FoldkitRemoteLive({
1223
+ version: 5,
1224
+ requirements,
1225
+ ...queries.length === 0 ? {} : { queries },
1226
+ after
1227
+ }).pipe(Stream.catchTag(["RpcClientError", "Unreachable"], (error) => Stream.fail(new RemoteLiveError({ message: error.message }))), Stream.map(liveEventOf), Stream.provideContext(context))
1228
+ };
1229
+ })), options),
1230
+ /**
1231
+ * Wraps a `RemoteClient` layer so its reads coalesce: requirements issued
1232
+ * together become one batch, and a requirement already in flight is joined.
1233
+ * `Remote.clientLayer` applies this; use it on a hand-written client.
1234
+ */
1235
+ coalesced: coalescedLayer,
1236
+ /**
1237
+ * A Foldkit Subscription entry that keeps the cache to what the active
1238
+ * Surfaces reach. `projections` are the ones the application observes (the
1239
+ * same it passes to `Remote.observe`), `connections` the query connections
1240
+ * it shows; anything else is collected once the roots have been stable for
1241
+ * `grace`, so a route transition that comes straight back does not thrash.
1242
+ */
1243
+ retain: (projections, toMessage = identityMessage, options = {}) => {
1244
+ const roots = rootsOf(projections, options);
1245
+ return {
1246
+ dependenciesSchema: retentionRootsSchema,
1247
+ modelToDependencies: () => roots,
1248
+ dependenciesToStream: (current) => Stream.fromEffect(Effect.succeed(toMessage({
1249
+ _tag: "RetentionChanged",
1250
+ roots: current
1251
+ })).pipe(Effect.delay(options.grace ?? 0)))
1252
+ };
1253
+ },
1254
+ /**
1255
+ * Runs a `Query` through `RemoteClient`, encoding its input from the ref.
1256
+ * Pair the result with `Remote.queryMessage` to merge the page into the Model.
1257
+ * With `select`, the server also returns the selected fields of the page's
1258
+ * items, so one response populates both the connection and the store.
1259
+ */
1260
+ query: Effect.fn("Remote.query")(function* (ref, select) {
1261
+ const client = yield* RemoteClient;
1262
+ const input = yield* Schema.encodeUnknownEffect(ref.Input)(ref.input).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new RemoteQueryError({ message: error.message }))));
1263
+ return yield* client.query({
1264
+ query: ref.query,
1265
+ input,
1266
+ window: ref.window,
1267
+ ...select === void 0 ? {} : { select }
1268
+ });
1269
+ }),
1270
+ /** The `RemoteMessage` that merges a query page into its connection. */
1271
+ queryMessage: (ref, result, select, now) => pageMessage(ref.identity, result, false, ref.window, select, now),
1272
+ /** A pure, serializable view of the whole cache. */
1273
+ inspect: inspectRemote,
1274
+ /** A pure, serializable view of one entity, or `undefined` if unknown. */
1275
+ inspectEntity,
1276
+ /**
1277
+ * Runs a mutation through `RemoteClient`, decoding its typed Output and
1278
+ * returning the result's normalized patches so the caller can reconcile them
1279
+ * through `updateRemote`'s `MutationSucceeded` (or `Remote.mutateInto`).
1280
+ */
1281
+ mutate: mutateRemote,
1282
+ /**
1283
+ * Reads the clock where the reader acts, for `Data.mutate`'s `now`: a
1284
+ * Command that yields `toMessage(now)`, the time in milliseconds since the
1285
+ * epoch. It reads Effect's `Clock`, so a test pins it, and `Trace.run`
1286
+ * runs it at the trace's time. Chirp's compose box answers a submit with
1287
+ * `Remote.now(now => Message.ClockedPost({ post, now }))`, and
1288
+ * `ClockedPost` starts the write.
1289
+ */
1290
+ now: (toMessage) => ({
1291
+ name: "Remote.now",
1292
+ effect: Effect.map(Clock.currentTimeMillis, toMessage)
1293
+ }),
1294
+ /**
1295
+ * Runs a mutation and reconciles its patches into a `RemoteModel` in one step,
1296
+ * returning the new model alongside the typed Output. The model-level form of
1297
+ * `MutationStarted` → `RemoteClient.mutate` → `MutationSucceeded`; in an
1298
+ * application the three are `update` (with `optimistic`), a Command, and the
1299
+ * Message the Command returns, so the optimistic operations show meanwhile.
1300
+ */
1301
+ mutateInto: Effect.fn("Remote.mutateInto")(function* (bound, model, mutation, input, requestId, options = {}) {
1302
+ const started = updateRemote(bound.store.get(model), {
1303
+ _tag: "MutationStarted",
1304
+ requestId,
1305
+ ...options.optimistic === void 0 ? {} : { optimistic: options.optimistic }
1306
+ });
1307
+ const outcome = yield* mutateRemote(mutation, input, requestId, { now: options.now });
1308
+ const settled = updateRemote(started, {
1309
+ _tag: "MutationSucceeded",
1310
+ requestId,
1311
+ entities: outcome.entities,
1312
+ connections: outcome.connections,
1313
+ deleted: outcome.deleted,
1314
+ now: yield* Clock.currentTimeMillis
1315
+ });
1316
+ return {
1317
+ output: outcome.output,
1318
+ model: bound.store.set(model, settled)
1319
+ };
1320
+ }),
1321
+ /**
1322
+ * A Foldkit Subscription entry that plans a Surface's missing fields from the
1323
+ * Model and fetches them through `RemoteClient`, emitting a `RemoteMessage`
1324
+ * per outcome. Wrap it in an application Message (`toMessage`) and reduce it
1325
+ * with the domain's `update`. Under a refreshing `policy` the entry first
1326
+ * emits `RefreshStarted`, so the fields being refetched read as `Refreshing`
1327
+ * while the request is pending.
1328
+ */
1329
+ observe: (bound, surface, params, toMessage = identityMessage, options = {}) => observeEntry(bound, () => askedOf(surface.projection(params)), toMessage, options),
1330
+ /**
1331
+ * A Foldkit Subscription entry that consumes the live stream for a Surface's
1332
+ * requirements, emitting a `LiveReceived` per event and a `ReadFailed`
1333
+ * carrying its `stream` when the stream breaks (including
1334
+ * `ResumeUnavailable`). That records a gap on the stream rather than a failed
1335
+ * read, since nothing was being read, and restarts the entry after a
1336
+ * backoff; the restart refetches what it covers. The resume cursor is read
1337
+ * from `RemoteModel.live`, so the application tracks no cursor of its own.
1338
+ */
1339
+ live: (bound, surface, params, toMessage = identityMessage, options = {}) => liveEntry(bound, () => {
1340
+ const projection = surface.projection(params);
1341
+ return {
1342
+ ...liveAskOf(askedOf(projection)),
1343
+ requirements: requirementsOf(projection)
1344
+ };
1345
+ }, toMessage, options)
1346
+ };
1347
+ /**
1348
+ * Each entry with its Messages as the parent's and its dependencies as they
1349
+ * were. The Model is the parent's already, and `Subscription.lift` would wrap
1350
+ * every entry's dependencies in `maybeDependencies` (since Foldkit 0.166).
1351
+ */
1352
+ const messagesAs = (entries, toParentMessage) => Record.map(entries, (entry) => ({
1353
+ ...entry,
1354
+ dependenciesToStream: (dependencies, read) => Stream.map(entry.dependenciesToStream(dependencies, read), toParentMessage)
1355
+ }));
1356
+ /**
1357
+ * An answer with what it read folded into its patches, one per row, so a list
1358
+ * is placed by every value the answer wrote, a derived field it read included.
1359
+ */
1360
+ const withAnswerRead = (answer) => {
1361
+ if (answer.read === void 0) return answer;
1362
+ const rows = /* @__PURE__ */ new Map();
1363
+ for (const patch of [...answer.entities, ...answer.read.result.entities]) {
1364
+ const key = entityKey(patch.entity, patch.id);
1365
+ const held = rows.get(key);
1366
+ rows.set(key, held === void 0 ? patch : {
1367
+ ...held,
1368
+ values: {
1369
+ ...held.values,
1370
+ ...patch.values
1371
+ }
1372
+ });
1373
+ }
1374
+ return {
1375
+ ...answer,
1376
+ entities: [...rows.values()]
1377
+ };
1378
+ };
1379
+ const brandEntries = (entries) => Subscription.make()(() => entries);
1380
+ /** The bound domain: the descriptor, the binding, and the operations over them. */
1381
+ const bindDomain = (definition, ref, options = {}) => {
1382
+ const operators = options.operators ?? Operators.kernel;
1383
+ const bound = bindRemote(definition, ref, operators);
1384
+ const store = bound.store;
1385
+ const viewerOf = options.viewer ?? (() => Option.none());
1386
+ const contextOf = (model) => ({
1387
+ viewer: viewerOf(model),
1388
+ now: Option.none()
1389
+ });
1390
+ const readers = /* @__PURE__ */ new Map();
1391
+ const keptConnections = /* @__PURE__ */ new Set();
1392
+ /**
1393
+ * What to do with a row a live event says was inserted into a connection.
1394
+ *
1395
+ * Resolved here rather than in the pure reducer because this is the only
1396
+ * place with both the Model and the registry, and two separate things needed
1397
+ * one or the other.
1398
+ *
1399
+ * **The declared policy reaches the decision.** `Query.connection(E, { live })`
1400
+ * has always been typed, documented, carried on the descriptor and encoded in
1401
+ * the Message schema — and nothing ever put it on a Message, so every live
1402
+ * insert took the default whatever an application asked for. It does now.
1403
+ *
1404
+ * **A row the client can judge does not need a policy.** The policy exists
1405
+ * because nothing could tell whether an inserted row belonged to the query.
1406
+ * Where the body says it does not, `ignore` is not a guess. Where the body
1407
+ * says it does, or the client cannot tell — a row it never fetched, or holds
1408
+ * without a field the body reads — the declared policy is the answer, exactly
1409
+ * as it was meant to be.
1410
+ *
1411
+ * Only membership is decided here. *Where* a row sorts needs text collation,
1412
+ * which is the backend's, so the position the event carries stands.
1413
+ */
1414
+ const livePolicyFor = (model, event) => {
1415
+ if (event._tag !== "ConnectionInsert") return void 0;
1416
+ const identity = event.connection;
1417
+ const descriptor = definition.registry.queries.get(identity.slice(0, identity.indexOf("\0")));
1418
+ const declared = (descriptor?.Result)?.live;
1419
+ if (descriptor?.body === void 0) return declared;
1420
+ const encoded = JSON.parse(identity.slice(identity.indexOf("\0") + 1));
1421
+ return belongsEncoded(storeOf(bound, model), descriptor, encoded, entityKey(event.edge.ref.entity, event.edge.ref.id), operators, contextOf(model)) === "no" ? {
1422
+ prepend: "ignore",
1423
+ append: "ignore"
1424
+ } : declared;
1425
+ };
1426
+ const reduce = (model, message) => {
1427
+ const live = message._tag === "LiveReceived" ? message : void 0;
1428
+ const resolved = live === void 0 || live.policy !== void 0 ? message : {
1429
+ ...live,
1430
+ policy: livePolicyFor(model, live.event)
1431
+ };
1432
+ const remote = store.get(model);
1433
+ const reduced = updateRemote(remote, resolved);
1434
+ const answer = message._tag === "MutationSucceeded" ? message : void 0;
1435
+ const next = answer === void 0 ? placedNews(reduced, newsOf(message), remote.entities, contextOf(model)) : impactOf(reduced, remote.mutations.applied.has(answer.requestId) ? answer : withAnswerRead(answer), remote.entities, contextOf(model));
1436
+ const checked = answer === void 0 || options.verifyPredictions !== true ? next : withAnswerChecked(model, remote, next, answer);
1437
+ return checked === remote ? model : store.set(model, checked);
1438
+ };
1439
+ /**
1440
+ * `after` with each part of the request's prediction the answer disagreed
1441
+ * with (`verifyPredictions`): a field it showed that the answer holds
1442
+ * otherwise (`Value`), and a row it left without fields the client showed
1443
+ * (`Gap`).
1444
+ */
1445
+ const withAnswerChecked = (model, before, after, answer) => {
1446
+ const { requestId } = answer;
1447
+ if (before.mutations.applied.has(requestId)) return after;
1448
+ const mutation = Option.map(Option.fromUndefinedOr(before.mutations.writes.get(requestId)), (write) => write.mutation);
1449
+ const settled = store.set(model, after);
1450
+ const projections = [...readers.values()].flatMap((resolvedAt) => resolvedAt(settled).map(({ projection }) => projection));
1451
+ const found = [...valueMismatches(requestId, mutation, before, after, answer), ...answerGaps(requestId, mutation, before.optimistic, after.optimistic, visibleStoreOf(after.entities, after.optimistic, operators), planAsked(unverified(after), askedUnion(projections), {}, operators).requirements)];
1452
+ return found.length === 0 ? after : {
1453
+ ...after,
1454
+ diagnostics: withDiagnostics(after.diagnostics, found)
1455
+ };
1456
+ };
1457
+ /**
1458
+ * `remote` with each list placed only to be checked (`placed`) read as it
1459
+ * would be without the check: such a list is fetched for its page, and a
1460
+ * stale list plans no reads of its rows, so a gap in them would go unseen.
1461
+ */
1462
+ const unverified = (remote) => {
1463
+ const checked = Object.keys(remote.placed).filter((identity) => remote.connections[identity] !== void 0);
1464
+ if (checked.length === 0) return remote;
1465
+ const connections = { ...remote.connections };
1466
+ for (const identity of checked) connections[identity] = {
1467
+ ...connections[identity],
1468
+ stale: false
1469
+ };
1470
+ return {
1471
+ ...remote,
1472
+ connections
1473
+ };
1474
+ };
1475
+ /**
1476
+ * Each field the request's layer showed that its answer carries another
1477
+ * value of. A field the answer does not carry is the `Gap`'s; one a request
1478
+ * still pending also patches shows that request's value before and after,
1479
+ * so it is not judged.
1480
+ */
1481
+ const valueMismatches = (requestId, mutation, before, after, answer) => {
1482
+ const layer = before.optimistic.layers.find((each) => each.id === requestId);
1483
+ if (layer === void 0) return [];
1484
+ const shown = visibleStoreOf(before.entities, before.optimistic, operators);
1485
+ const answered = /* @__PURE__ */ new Map();
1486
+ for (const row of [...answer.entities, ...answer.read?.result.entities ?? []]) {
1487
+ const key = entityKey(row.entity, row.id);
1488
+ answered.set(key, {
1489
+ ...answered.get(key),
1490
+ ...row.values
1491
+ });
1492
+ }
1493
+ const stillPatched = new Set(after.optimistic.layers.flatMap((each) => each.patches.flatMap((patch) => patchedFields(patch).map((field) => `${entityKey(patch.entity, patch.id)}.${field}`))));
1494
+ return layer.patches.flatMap((patch) => {
1495
+ const key = entityKey(patch.entity, patch.id);
1496
+ const values = answered.get(key);
1497
+ const entry = shown[key];
1498
+ if (values === void 0 || entry === void 0 || entry.tombstone) return [];
1499
+ return patchedFields(patch).flatMap((field) => stillPatched.has(`${key}.${field}`) || !entry.present.has(field) || !Object.hasOwn(values, field) || sameData(entry.values[field], values[field]) ? [] : [predictionMismatch(PredictionPart.Value({
1500
+ requestId,
1501
+ mutation,
1502
+ entity: patch.entity,
1503
+ id: patch.id,
1504
+ field,
1505
+ shown: entry.values[field],
1506
+ answered: values[field]
1507
+ }))]);
1508
+ });
1509
+ };
1510
+ /** The registered query a connection identity names, and its encoded input. */
1511
+ const queryOf = (identity) => {
1512
+ const separator = identity.indexOf("\0");
1513
+ const descriptor = definition.registry.queries.get(identity.slice(0, separator));
1514
+ if (descriptor === void 0) return Option.none();
1515
+ const encoded = JSON.parse(identity.slice(separator + 1));
1516
+ const over = descriptor.body?.entity.name;
1517
+ const filtered = (over === void 0 ? void 0 : definition.registry.filtered.get(over)) ?? [];
1518
+ return Option.some({
1519
+ descriptor,
1520
+ encoded,
1521
+ filtered
1522
+ });
1523
+ };
1524
+ /**
1525
+ * `pages` with each change to a row they hold placed, judged against
1526
+ * `store`: a row that moved goes where the body's order puts it, and one
1527
+ * that stopped matching leaves. A row a pending write creates joins where
1528
+ * the body puts it, since the write holds all it is. Another row a change
1529
+ * brings in is not added, since what the list selects of it may not be
1530
+ * loaded, and a change the client cannot place leaves the row where it is.
1531
+ */
1532
+ const placedHeld = (identity, pages, changes, store, context) => {
1533
+ const query = queryOf(identity);
1534
+ if (Option.isNone(query)) return pages;
1535
+ const holds = new Set(items(pages).map((edge) => entityKey(edge.ref.entity, edge.ref.id)));
1536
+ let placed = pages;
1537
+ for (const change of changes) {
1538
+ const created = EntityChange.match(change, {
1539
+ Changed: ({ created }) => created,
1540
+ Deleted: () => false
1541
+ });
1542
+ if (!holds.has(entityKey(change.entity, change.id)) && !created) continue;
1543
+ placed = Impact.$match(impactOn({
1544
+ ...query.value,
1545
+ connection: placed,
1546
+ holds
1547
+ }, store, change, operators, context), {
1548
+ Kept: () => placed,
1549
+ Placed: ({ connection }) => connection,
1550
+ Invalidated: () => placed
1551
+ });
1552
+ }
1553
+ return placed;
1554
+ };
1555
+ /**
1556
+ * What a list body's context reads while pending layers are placed: the
1557
+ * viewer the newest operation was predicted for. The viewer is the
1558
+ * reader's, so every operation since the last `forget` names the same one.
1559
+ */
1560
+ const layersContext = (layers) => {
1561
+ const viewer = [...layers].reverse().find((layer) => Option.isSome(layer.viewer))?.viewer;
1562
+ return viewer === void 0 ? void 0 : {
1563
+ viewer,
1564
+ now: Option.none()
1565
+ };
1566
+ };
1567
+ /**
1568
+ * A loaded list with the pending layers placed in its pages, so a row a
1569
+ * pending edit moves, or takes out of the list, shows where its answer
1570
+ * will be placed until the edit settles.
1571
+ */
1572
+ const pendingPlaced = (remote, identity, pages) => {
1573
+ const { layers } = remote.optimistic;
1574
+ if (layers.length === 0) return pages;
1575
+ return memoRead([
1576
+ pages,
1577
+ layers,
1578
+ remote.entities
1579
+ ], `placed:${identity}`, () => placedHeld(identity, pages, changesOf({ entities: resolvedPatches(remote.entities, remote.optimistic, operators) }, remote.entities), visibleStoreOf(remote.entities, remote.optimistic, operators), layersContext(layers)));
1580
+ };
1581
+ /**
1582
+ * The edges a loaded list shows: its pages with pending edits placed, and
1583
+ * each row an insert overlay shows (a pending create, a confirmed or a live
1584
+ * insert) where the body's order puts it, or left out when it sorts past
1585
+ * what is loaded. An overlay says a row is in the list; in a list the
1586
+ * client can place, the order says where. A row it cannot place stays at
1587
+ * the end its overlay names.
1588
+ */
1589
+ const shownEdges = (remote, identity, connection) => {
1590
+ const pages = pendingPlaced(remote, identity, connection);
1591
+ const shown = remote.optimistic.overlays.some((overlay) => overlay.connection === identity && overlay.position !== "remove") ? insertsPlaced(remote, identity, pages) : {
1592
+ pages,
1593
+ overlays: remote.optimistic.overlays
1594
+ };
1595
+ return visibleItems(shown.pages, identity, shown.overlays, goneIn(remote.entities, remote.optimistic));
1596
+ };
1597
+ const insertsPlaced = (remote, identity, pages) => {
1598
+ const { overlays } = remote.optimistic;
1599
+ const store = visibleStoreOf(remote.entities, remote.optimistic, operators);
1600
+ return memoRead([
1601
+ pages,
1602
+ overlays,
1603
+ store
1604
+ ], `inserted:${identity}`, () => {
1605
+ const query = queryOf(identity);
1606
+ const body = Option.isSome(query) ? query.value.descriptor.body : void 0;
1607
+ if (Option.isNone(query) || body === void 0) return {
1608
+ pages,
1609
+ overlays
1610
+ };
1611
+ const inserted = /* @__PURE__ */ new Map();
1612
+ for (const overlay of overlays) {
1613
+ if (overlay.connection !== identity || overlay.position === "remove") continue;
1614
+ for (const edge of overlay.edges) inserted.set(edge.key, edge);
1615
+ }
1616
+ const held = new Set(items(pages).map((edge) => edge.key));
1617
+ let placed = pages;
1618
+ const outside = [];
1619
+ for (const edge of inserted.values()) {
1620
+ if (held.has(edge.key)) continue;
1621
+ const key = entityKey(edge.ref.entity, edge.ref.id);
1622
+ RowPlace.$match(placeIn(store, placed, body, query.value.encoded, key, operators), {
1623
+ Before: (place) => void (placed = withEdge(placed, edge, place)),
1624
+ End: (place) => void (placed = withEdge(placed, edge, place)),
1625
+ Outside: () => void outside.push(edge),
1626
+ Unknown: () => void 0
1627
+ });
1628
+ }
1629
+ return {
1630
+ pages: placed,
1631
+ overlays: outside.length === 0 ? overlays : [...overlays, {
1632
+ id: "placed:outside",
1633
+ connection: identity,
1634
+ edges: outside,
1635
+ position: "remove"
1636
+ }]
1637
+ };
1638
+ });
1639
+ };
1640
+ /**
1641
+ * The loaded lists with what the server said of rows they hold placed: a
1642
+ * read or a live patch that changes a held row's ordering or filtered field
1643
+ * moves it, or takes it out, as a mutation's answer does. A list it cannot
1644
+ * place stands as it is, as a read is no write: its own refetch or live
1645
+ * stream says otherwise. Only a value the client held before counts, so a
1646
+ * row's first read judges nothing.
1647
+ */
1648
+ const placedNews = (remote, news, before, context) => {
1649
+ if (news.length === 0) return remote;
1650
+ const held = news.map((patch) => {
1651
+ const values = before[entityKey(patch.entity, patch.id)]?.values ?? {};
1652
+ return {
1653
+ ...patch,
1654
+ values: Object.fromEntries(Object.entries(patch.values).filter(([field]) => Object.hasOwn(values, field)))
1655
+ };
1656
+ });
1657
+ const changes = changesOf({ entities: held }, before);
1658
+ if (changes.length === 0) return remote;
1659
+ const placed = {};
1660
+ for (const [identity, pages] of Object.entries(remote.connections)) {
1661
+ const after = placedHeld(identity, pages, changes, remote.entities, context);
1662
+ if (after !== pages) placed[identity] = after;
1663
+ }
1664
+ return withPlacements(remote, placed, []);
1665
+ };
1666
+ /** The rows a read or a live patch says the server holds now. */
1667
+ const newsOf = (message) => {
1668
+ if (message._tag === "ReadReceived") return message.result.entities;
1669
+ if (message._tag !== "LiveReceived") return [];
1670
+ const event = message.event;
1671
+ if (event._tag !== "EntityPatched") return [];
1672
+ return [{
1673
+ entity: event.ref.entity,
1674
+ id: event.ref.id,
1675
+ values: event.values
1676
+ }];
1677
+ };
1678
+ /**
1679
+ * The loaded connections as a write left them: each judged by its body
1680
+ * against the store as the write left it, a change it can place placed in
1681
+ * its pages, and one it cannot invalidated for its read entry to fetch.
1682
+ */
1683
+ const impactOf = (remote, answer, before, context) => {
1684
+ const changes = changesOf(answer, before);
1685
+ if (changes.length === 0) return remote;
1686
+ const named = /* @__PURE__ */ new Map();
1687
+ for (const change of answer.connections ?? []) {
1688
+ const keys = named.get(change.connection) ?? /* @__PURE__ */ new Set();
1689
+ keys.add(change.edge.key);
1690
+ named.set(change.connection, keys);
1691
+ }
1692
+ const visible = visibleStoreOf(remote.entities, remote.optimistic, operators);
1693
+ const placed = {};
1694
+ const invalidated = [];
1695
+ for (const identity of Object.keys(remote.connections)) {
1696
+ const answered = named.get(identity);
1697
+ const judged = answered === void 0 ? changes : changes.filter((change) => !answered.has(entityKey(change.entity, change.id)));
1698
+ if (judged.length === 0) continue;
1699
+ const query = queryOf(identity);
1700
+ if (Option.isNone(query)) continue;
1701
+ const original = remote.connections[identity];
1702
+ const holds = new Set(visibleItems(original, identity, remote.optimistic.overlays, goneIn(remote.entities, remote.optimistic)).map((edge) => entityKey(edge.ref.entity, edge.ref.id)));
1703
+ let pages = original;
1704
+ let invalid = false;
1705
+ for (const change of judged) {
1706
+ const held = {
1707
+ ...query.value,
1708
+ connection: pages,
1709
+ holds
1710
+ };
1711
+ const after = Impact.$match(impactOn(held, visible, change, operators, context), {
1712
+ Kept: () => Option.some(pages),
1713
+ Placed: ({ connection }) => Option.some(connection),
1714
+ Invalidated: () => Option.none()
1715
+ });
1716
+ if (Option.isNone(after)) {
1717
+ invalid = true;
1718
+ break;
1719
+ }
1720
+ pages = after.value;
1721
+ }
1722
+ if (invalid) invalidated.push(identity);
1723
+ else if (pages !== original) placed[identity] = pages;
1724
+ }
1725
+ return withPlacements(remote, placed, invalidated);
1726
+ };
1727
+ /**
1728
+ * The lists placed and invalidated. Under `verifyPredictions`, each placed
1729
+ * list is fetched too, and the order it was placed in kept to compare with
1730
+ * the server's page.
1731
+ */
1732
+ const withPlacements = (remote, placed, invalidated) => {
1733
+ const identities = Object.keys(placed);
1734
+ if (identities.length === 0) return invalidateConnections(remote, invalidated);
1735
+ const withPlaced = {
1736
+ ...remote,
1737
+ connections: {
1738
+ ...remote.connections,
1739
+ ...placed
1740
+ }
1741
+ };
1742
+ if (options.verifyPredictions !== true) return invalidateConnections(withPlaced, invalidated);
1743
+ const firstPage = (connection) => (connection?.segments[0]?.edges ?? []).map((edge) => edge.key);
1744
+ const orders = Object.fromEntries(identities.map((identity) => {
1745
+ const order = firstPage(placed[identity]);
1746
+ const kept = new Set(order);
1747
+ return [identity, {
1748
+ order,
1749
+ dropped: firstPage(remote.connections[identity]).filter((key) => !kept.has(key))
1750
+ }];
1751
+ }));
1752
+ return invalidateConnections({
1753
+ ...withPlaced,
1754
+ placed: {
1755
+ ...remote.placed,
1756
+ ...orders
1757
+ }
1758
+ }, [...invalidated, ...identities]);
1759
+ };
1760
+ const assertOwned = (entry) => assertOwnedBy(bound.contract, entry);
1761
+ /**
1762
+ * What the reader is shown of each entity in `model`, and what the write
1763
+ * itself shows (`predicted`), for its answer to carry (#245, #265).
1764
+ */
1765
+ const shownAt = (model, predicted) => shownOf(askedUnion([...readers.values()].flatMap((resolvedAt) => resolvedAt(model).map(({ projection }) => projection))), predicted);
1766
+ /**
1767
+ * A durable write's request id, which is its operation id: this page's
1768
+ * session and the next in its sequence, unique across tabs and loads. Only
1769
+ * a retry names one, and only one the outbox holds.
1770
+ */
1771
+ const durableId = (remote, mutation, given) => {
1772
+ if (given !== void 0) {
1773
+ if (remote.outbox.queue.some((queued) => operationOf(queued) === given)) return given;
1774
+ throw new Error(`[foldkit-remote] Data.mutate(${mutation}, { durable: true, requestId: "${given}" }): a durable write is named by the outbox; pass no requestId`);
1775
+ }
1776
+ return Option.match(remote.outbox.session, {
1777
+ onNone: () => {
1778
+ throw new Error(`[foldkit-remote] Data.mutate(${mutation}, { durable: true }) before the outbox's restore has run: start init from Data.outbox(…).restore(model), as the runtime's flags, so a durable write has an id no other tab or page load uses`);
1779
+ },
1780
+ onSome: (session) => `${session}-${remote.mutations.sequence + 1}`
1781
+ });
1782
+ };
1783
+ /** The descriptor a recorded write names; only a hand-built `MutationStarted` names another. */
1784
+ const registeredMutation = (name) => {
1785
+ const descriptor = definition.registry.mutations.get(name);
1786
+ if (descriptor === void 0) throw new Error(`Remote: a failed write names mutation "${name}", which domain "${bound.contract.name}" does not register`);
1787
+ return descriptor;
1788
+ };
1789
+ /**
1790
+ * `prefetch` and `satisfy`, each Message they reduce also kept in `heard`,
1791
+ * so `preload` can hand them to the Model as it is by the time they land.
1792
+ */
1793
+ const prefetchInto = (model, projection, options, heard) => {
1794
+ const hear = (current, message) => {
1795
+ heard.push(message);
1796
+ return reduce(current, message);
1797
+ };
1798
+ return Effect.gen(function* () {
1799
+ const { policy = RemotePolicy.cacheFirst, now = wallClock } = options;
1800
+ const client = yield* RemoteClient;
1801
+ const planOptions = RemotePolicy.toPlan(policy, now());
1802
+ let current = model;
1803
+ for (const query of planAsked(store.get(current), askedOf(projection), planOptions, operators).queries) {
1804
+ const page = yield* queryRequestOf(query).pipe(Effect.flatMap((request) => client.query(request)));
1805
+ current = hear(current, pageMessage(query.identity, page, true, query.window, query.select, now()));
1806
+ }
1807
+ const requirements = planAsked(store.get(current), askedOf(projection), planOptions, operators).requirements;
1808
+ if (requirements.length === 0) return current;
1809
+ const at = now();
1810
+ const result = yield* client.read({
1811
+ version: 5,
1812
+ requests: requirements
1813
+ });
1814
+ return hear(current, {
1815
+ _tag: "ReadReceived",
1816
+ requests: requirements,
1817
+ result,
1818
+ now: at
1819
+ });
1820
+ });
1821
+ };
1822
+ const satisfyInto = (model, active, options, heard) => Effect.gen(function* () {
1823
+ const { passes = 8, now = wallClock } = options;
1824
+ const entries = Object.values(active);
1825
+ entries.forEach(assertOwned);
1826
+ const planOptions = RemotePolicy.toPlan(RemotePolicy.cacheFirst, now());
1827
+ const plans = (current, projections) => {
1828
+ const planned = planAsked(store.get(current), askedUnion(projections), planOptions, operators);
1829
+ return planned.queries.length > 0 || planned.requirements.length > 0;
1830
+ };
1831
+ let current = model;
1832
+ for (let pass = 0; pass < passes; pass++) {
1833
+ if (!entries.some((entry) => plans(current, resolvedOf(entry, current).map(({ projection }) => projection)))) return current;
1834
+ for (const entry of entries) for (const { projection } of resolvedOf(entry, current)) current = yield* prefetchInto(current, projection, { now }, heard);
1835
+ }
1836
+ const reading = entries.flatMap((entry) => resolvedOf(entry, current).filter(({ projection }) => plans(current, [projection])));
1837
+ if (reading.length === 0) return current;
1838
+ return yield* new RemoteUnsatisfied({
1839
+ surfaces: reading.map(({ name }) => name),
1840
+ passes
1841
+ });
1842
+ });
1843
+ const domain = {
1844
+ ...definition,
1845
+ ...bound,
1846
+ get: (selection, id) => Remote.select(bound, selection)(id),
1847
+ live: (selection, id) => {
1848
+ const projection = Remote.select(bound, selection)(id);
1849
+ return {
1850
+ ...projection,
1851
+ metadata: RemoteRequirements.of(...requirementsOf(projection).map((requirement) => ({
1852
+ ...requirement,
1853
+ live: true
1854
+ })))
1855
+ };
1856
+ },
1857
+ active: (name, projectionOf) => {
1858
+ const { owner } = bound.contract;
1859
+ if (owner === void 0) throw new Error(`Remote: "${name}" reads domain "${bound.contract.name}", whose Model field is a raw optic that names no application; make the domain from an application's field (App.model.remote) to give it reads`);
1860
+ return {
1861
+ name,
1862
+ owner,
1863
+ messages: [],
1864
+ projectionOf
1865
+ };
1866
+ },
1867
+ subscriptions: (active, options = {}) => {
1868
+ const entries = {};
1869
+ for (const entry of Object.values(active)) assertOwned(entry);
1870
+ for (const [key, entry] of Object.entries(active)) {
1871
+ const resolvedAt = readers.get(entry) ?? memoizedResolved(entry);
1872
+ readers.set(entry, resolvedAt);
1873
+ const asked = (model) => askedUnion(resolvedAt(model).map(({ projection }) => projection));
1874
+ entries[`${key}.read`] = observeEntry(bound, asked, identityMessage, options);
1875
+ entries[`${key}.live`] = liveEntry(bound, (model) => liveAskOf(asked(model)), identityMessage, options);
1876
+ }
1877
+ for (const identity of options.connections ?? []) keptConnections.add(connectionIdentity(identity));
1878
+ entries.retain = {
1879
+ dependenciesSchema: retentionRootsSchema,
1880
+ modelToDependencies: (model) => rootsOf([...readers.values()].flatMap((resolvedAt) => resolvedAt(model).map(({ projection }) => projection)), { connections: [...keptConnections] }),
1881
+ dependenciesToStream: (current) => Stream.fromEffect(Effect.succeed({
1882
+ _tag: "RetentionChanged",
1883
+ roots: current
1884
+ }).pipe(Effect.delay(options.grace ?? 0)))
1885
+ };
1886
+ markAll(entries);
1887
+ return entries;
1888
+ },
1889
+ plan: (model, projection, options) => Remote.plan(bound, model, projection, options),
1890
+ meta: (model, projection) => Remote.meta(bound, model, projection),
1891
+ storeOf: (model) => storeOf(bound, model),
1892
+ confirmed: (projection) => confirmed(bound, projection),
1893
+ prefetch: (model, projection, options = {}) => prefetchInto(model, projection, options, []),
1894
+ satisfy: (model, active, options = {}) => satisfyInto(model, active, options, []),
1895
+ preload: (model, active, options = {}) => {
1896
+ Object.values(active).forEach(assertOwned);
1897
+ return {
1898
+ name: "Remote.preload",
1899
+ args: { active: Object.keys(active).join(", ") },
1900
+ effect: Effect.suspend(() => {
1901
+ const heard = [];
1902
+ return satisfyInto(model, active, options, heard).pipe(Effect.exit, Effect.map(() => ({
1903
+ _tag: "Preloaded",
1904
+ messages: heard
1905
+ })));
1906
+ })
1907
+ };
1908
+ },
1909
+ query: (query, input, options) => {
1910
+ assertRegistered(bound, "Query", definition.registry.queries, query.name);
1911
+ const { select: given, live, ...window } = options;
1912
+ const select = selectionOf(given);
1913
+ for (const [side, size] of [["first", window.first], ["last", window.last]]) if (size !== void 0 && !(Number.isInteger(size) && size >= 0)) throw new Error(`Remote: query "${query.name}" asks for ${side}: ${String(size)}; a page size is a non-negative integer`);
1914
+ if (live === true && window.first === void 0) throw new Error(`Remote: query "${query.name}" is read live without first; a list watched live is read again every tick, so it needs a size`);
1915
+ const listed = query.Result.entity;
1916
+ if (listed === void 0) throw new Error(`Remote: query "${query.name}" is not a connection over an entity, so it has no page to select`);
1917
+ if (select.entity !== listed) throw new Error(`Remote: the selection is of "${select.entity}", but query "${query.name}" lists "${listed}"`);
1918
+ const contract = placeableReadContract(readContract(query.ref(input), select, window), query.body, operators);
1919
+ const { ref, relation } = contract;
1920
+ const requirement = live === true ? {
1921
+ ...contract.requirement,
1922
+ live
1923
+ } : contract.requirement;
1924
+ const relationKey = stableStringify(relation);
1925
+ const sizeOf = (remote) => windowSize(remote, ref);
1926
+ const shown = (remote, connection) => cutToWindow(shownEdges(remote, ref.identity, connection), ref.window, sizeOf(remote));
1927
+ const failedItem = (remote, connection) => {
1928
+ if (Object.keys(remote.failures.fields).length === 0) return void 0;
1929
+ for (const edge of shown(remote, connection).edges) {
1930
+ if (edge.ref.entity !== relation.entity) continue;
1931
+ const error = failureOf(remote, edge.ref.entity, edge.ref.id, relation);
1932
+ if (error !== void 0) return {
1933
+ _tag: "Failed",
1934
+ error
1935
+ };
1936
+ }
1937
+ };
1938
+ const loadingItem = (remote, connection) => {
1939
+ if (remote.loading.size === 0) return void 0;
1940
+ for (const edge of shown(remote, connection).edges) {
1941
+ if (edge.ref.entity !== relation.entity) continue;
1942
+ if (isLoadingThrough(remote, edge.ref.entity, edge.ref.id, relation)) return { _tag: "Loading" };
1943
+ }
1944
+ };
1945
+ const readPage = (remote, connection) => {
1946
+ const visible = visibleStoreOf(remote.entities, remote.optimistic, operators);
1947
+ return memoRead([
1948
+ visible,
1949
+ connection,
1950
+ remote.optimistic.overlays
1951
+ ], `${ref.identity}\u0000${relationKey}\u0000${String(sizeOf(remote))}`, () => {
1952
+ const rows = [];
1953
+ let refreshing = connection.stale;
1954
+ let missing = false;
1955
+ const { edges, cutBefore, cutAfter } = shown(remote, connection);
1956
+ for (const edge of edges) {
1957
+ const key = entityKey(edge.ref.entity, edge.ref.id);
1958
+ const assembled = assemble(visible, key, relation);
1959
+ if (assembled === void 0) {
1960
+ const unavailable = unavailableFailure(visible, key, relation);
1961
+ if (unavailable !== void 0) return unavailable;
1962
+ missing = true;
1963
+ continue;
1964
+ }
1965
+ const decoded = decodeRow(select.schema, visible[key].values, assembled.values);
1966
+ if (Result.isFailure(decoded)) return {
1967
+ _tag: "Failed",
1968
+ error: {
1969
+ _tag: "DecodeError",
1970
+ message: decoded.failure.message
1971
+ }
1972
+ };
1973
+ refreshing ||= assembled.refreshing;
1974
+ rows.push(decoded.success);
1975
+ }
1976
+ if (missing && rows.length === 0 && !deletedInWindow(items(connection), new Set(shownEdges(remote, ref.identity, connection).map((edge) => edge.key)), ref.window, sizeOf(remote), goneIn(remote.entities, remote.optimistic))) return;
1977
+ const page = {
1978
+ items: rows,
1979
+ hasNext: cutAfter || hasNext(connection),
1980
+ hasPrevious: cutBefore || hasPrevious(connection)
1981
+ };
1982
+ return refreshing || missing ? {
1983
+ _tag: "Refreshing",
1984
+ value: page
1985
+ } : {
1986
+ _tag: "Ready",
1987
+ value: page
1988
+ };
1989
+ });
1990
+ };
1991
+ return {
1992
+ Model: remoteDataSchema(pageSchema(select.schema)),
1993
+ dependencies: [],
1994
+ metadata: RemoteConnections.of(requirement),
1995
+ ref,
1996
+ selection: select,
1997
+ read: (root) => {
1998
+ const remote = store.get(root);
1999
+ const connection = remote.connections[ref.identity];
2000
+ const failure = remote.failures.connections[ref.identity];
2001
+ if (connection === void 0 || connection.stale && connection.segments.length === 0) {
2002
+ if (isQueryLoading(remote, ref.identity)) return { _tag: "Loading" };
2003
+ return failure === void 0 ? { _tag: "Initial" } : {
2004
+ _tag: "Failed",
2005
+ error: failure
2006
+ };
2007
+ }
2008
+ const read = readPage(remote, connection) ?? failedItem(remote, connection) ?? loadingItem(remote, connection) ?? { _tag: "Initial" };
2009
+ if (failure === void 0) {
2010
+ if (read._tag !== "Ready" && read._tag !== "Refreshing") return read;
2011
+ const item = failedItem(remote, connection);
2012
+ return item === void 0 ? read : {
2013
+ _tag: "Failed",
2014
+ error: item.error,
2015
+ previous: read.value
2016
+ };
2017
+ }
2018
+ switch (read._tag) {
2019
+ case "Ready":
2020
+ case "Refreshing": return {
2021
+ _tag: "Failed",
2022
+ error: failure,
2023
+ previous: read.value
2024
+ };
2025
+ case "Failed": return read;
2026
+ case "Initial":
2027
+ case "Loading":
2028
+ case "NotFound": return {
2029
+ _tag: "Failed",
2030
+ error: failure
2031
+ };
2032
+ }
2033
+ }
2034
+ };
2035
+ },
2036
+ more: (model, projection) => {
2037
+ const remote = store.get(model);
2038
+ const { ref } = projection;
2039
+ const size = windowSize(remote, ref);
2040
+ const page = ref.window.first ?? ref.window.last;
2041
+ if (size === void 0 || page === void 0 || page === 0) return Option.none();
2042
+ const read = projection.read(model);
2043
+ return (read._tag === "Ready" || read._tag === "Refreshing") && (ref.window.first === void 0 ? read.value.hasPrevious : read.value.hasNext) ? Option.some(bound.store.set(model, updateRemote(remote, {
2044
+ _tag: "WindowGrown",
2045
+ window: windowGrowthKey(ref.identity, stableStringify(ref.window)),
2046
+ size: size + page
2047
+ }))) : Option.none();
2048
+ },
2049
+ explain: (model, projection, options) => {
2050
+ const { ref } = projection;
2051
+ const reading = Object.values(options?.surfaces ?? {}).flatMap((source) => {
2052
+ const matched = resolvedOf(source, model).filter(({ projection: shown }) => connectionsOf(shown).some((connection) => connection.identity === ref.identity));
2053
+ if (matched.length === 0) return [];
2054
+ return [{
2055
+ source,
2056
+ names: matched.map(({ name }) => name)
2057
+ }];
2058
+ });
2059
+ const body = definition.registry.queries.get(ref.query)?.body;
2060
+ const [connection] = RemoteConnections.get(projection.metadata);
2061
+ return {
2062
+ domain: bound.contract.name,
2063
+ query: ref.query,
2064
+ input: Schema.encodeSync(ref.Input)(ref.input),
2065
+ identity: ref.identity,
2066
+ window: ref.window,
2067
+ select: connection.select,
2068
+ ...body === void 0 ? {} : {
2069
+ body: Query$1.show(body),
2070
+ dependencies: explainedDependencies(Query$1.dependencies(body))
2071
+ },
2072
+ state: projection.read(model)._tag,
2073
+ ...options?.surfaces === void 0 ? {} : {
2074
+ surfaces: reading.flatMap((read) => read.names),
2075
+ activation: reading.flatMap((read) => {
2076
+ const activation = "activation" in read.source ? read.source.activation : void 0;
2077
+ return activation === void 0 ? [] : [{
2078
+ surface: read.source.name,
2079
+ ...activation
2080
+ }];
2081
+ })
2082
+ }
2083
+ };
2084
+ },
2085
+ why: (model, projection, options) => {
2086
+ const data = projection.read(model);
2087
+ const asked = askedOf(projection);
2088
+ const reading = (options?.surfaces === void 0 ? [] : Object.values(options.surfaces)).flatMap((entry) => {
2089
+ const names = resolvedOf(entry, model).filter(({ projection: shown }) => covers(askedOf(shown), asked)).map(({ name }) => name);
2090
+ return names.length === 0 ? [] : [{
2091
+ entry,
2092
+ names
2093
+ }];
2094
+ });
2095
+ const surfaces = options?.surfaces === void 0 ? Option.none() : Option.some(reading.flatMap((read) => read.names));
2096
+ const initial = data._tag !== "Initial" ? [] : [Option.match(surfaces, {
2097
+ onNone: () => Why.NoActiveRecord(),
2098
+ onSome: (names) => names.length === 0 ? Why.NotObserved() : Why.NotFetching({ surfaces: names })
2099
+ })];
2100
+ const remote = store.get(model);
2101
+ const streams = asked.requirements.some((requirement) => requirement.live === true) || asked.connections.some((connection) => connection.live === true) ? reading.flatMap(({ entry }) => {
2102
+ return Match.valueTags(domain.liveStatus(model, entry), {
2103
+ Idle: () => [],
2104
+ Live: () => [],
2105
+ Reconnecting: ({ attempt, error }) => [Why.LiveReconnecting({
2106
+ surface: entry.name,
2107
+ attempt,
2108
+ error
2109
+ })],
2110
+ Refused: ({ error }) => [Why.LiveRefused({
2111
+ surface: entry.name,
2112
+ error
2113
+ })]
2114
+ });
2115
+ }) : [];
2116
+ const reader = {
2117
+ remote,
2118
+ visible: visibleStoreOf(remote.entities, remote.optimistic, operators),
2119
+ listOf: (identity) => ({
2120
+ query: Option.match(queryOf(identity), {
2121
+ onNone: () => identity,
2122
+ onSome: (query) => query.descriptor.name
2123
+ }),
2124
+ identity
2125
+ }),
2126
+ bodyOf: (identity) => Option.flatMap(queryOf(identity), (query) => Option.map(Option.fromUndefinedOr(query.descriptor.body), (body) => ({
2127
+ body,
2128
+ encoded: query.encoded
2129
+ }))),
2130
+ edgesOf: ({ identity, window }) => {
2131
+ const connection = remote.connections[identity] ?? emptyConnection;
2132
+ const placed = shownEdges(remote, identity, connection);
2133
+ return {
2134
+ shown: cutToWindow(placed, window, windowSize(remote, {
2135
+ identity,
2136
+ window
2137
+ })).edges,
2138
+ placed,
2139
+ pages: items(connection)
2140
+ };
2141
+ },
2142
+ operators,
2143
+ context: layersContext(remote.optimistic.layers)
2144
+ };
2145
+ return readWhy(data._tag, [
2146
+ ...initial,
2147
+ ...failedReasons(data),
2148
+ ...reasonsOf(reader, asked),
2149
+ ...streams
2150
+ ], surfaces);
2151
+ },
2152
+ timeline: (start, history, reads, options) => {
2153
+ let before = start;
2154
+ return history.flatMap((entry, index) => {
2155
+ const from = before;
2156
+ before = entry.model;
2157
+ return Object.entries(reads).flatMap(([read, projection]) => {
2158
+ const was = projection.read(from);
2159
+ const is = projection.read(entry.model);
2160
+ if (was._tag === is._tag) return [];
2161
+ return [{
2162
+ read,
2163
+ index,
2164
+ message: entry.message,
2165
+ from: was._tag,
2166
+ to: is._tag,
2167
+ why: domain.why(entry.model, projection, options),
2168
+ regression: regressed(was, is)
2169
+ }];
2170
+ });
2171
+ });
2172
+ },
2173
+ filtered: (model, over, by, input) => {
2174
+ assertRegistered(bound, "Query", definition.registry.queries, by.name);
2175
+ const filters = by.body?.entity.name;
2176
+ if (filters !== void 0 && filters !== over.selection.entity) throw new Error(`Remote: query "${by.name}" is over "${filters}", but the list is of "${over.selection.entity}", so it cannot filter it`);
2177
+ const remote = store.get(model);
2178
+ const connection = remote.connections[over.ref.identity];
2179
+ const visible = visibleStoreOf(remote.entities, remote.optimistic, operators);
2180
+ const among = (connection === void 0 ? [] : shownEdges(remote, over.ref.identity, connection)).map((edge) => entityKey(edge.ref.entity, edge.ref.id));
2181
+ let judged;
2182
+ try {
2183
+ judged = matching(visible, by, input, {
2184
+ among,
2185
+ operators,
2186
+ context: contextOf(model)
2187
+ });
2188
+ } catch (error) {
2189
+ if (error instanceof QueryEvaluateError) return {
2190
+ items: [],
2191
+ complete: false
2192
+ };
2193
+ throw error;
2194
+ }
2195
+ const relation = relationOf(over.selection);
2196
+ const items = [];
2197
+ let assembledAll = true;
2198
+ for (const key of judged.matched) {
2199
+ const assembled = assemble(visible, key, relation);
2200
+ if (assembled === void 0) {
2201
+ assembledAll = false;
2202
+ continue;
2203
+ }
2204
+ const decoded = decodeRow(over.selection.schema, visible[key].values, assembled.values);
2205
+ if (Result.isFailure(decoded)) {
2206
+ assembledAll = false;
2207
+ continue;
2208
+ }
2209
+ items.push(decoded.success);
2210
+ }
2211
+ return {
2212
+ items,
2213
+ complete: judged.skipped.length === 0 && assembledAll && connection !== void 0 && !isGapped(connection) && !hasNext(connection) && !hasPrevious(connection)
2214
+ };
2215
+ },
2216
+ refresh: (model, target, options) => Remote.refresh(bound, model, target, options),
2217
+ forget: (model) => Remote.forget(bound, model),
2218
+ overlay: (model, id, optimistic) => reduce(model, {
2219
+ _tag: "OverlayShown",
2220
+ id,
2221
+ optimistic
2222
+ }),
2223
+ lift: (model, id) => reduce(model, {
2224
+ _tag: "OverlayLifted",
2225
+ id
2226
+ }),
2227
+ overlays: (model) => overlaysOf(store.get(model)),
2228
+ visibleItems: (remote, connection) => {
2229
+ const identity = connectionIdentity(connection);
2230
+ return shownEdges(remote, identity, remote.connections[identity] ?? emptyConnection);
2231
+ },
2232
+ mutate: (model, mutation, input, options = {}) => {
2233
+ assertRegistered(bound, "Mutation", definition.registry.mutations, mutation.name);
2234
+ const remote = store.get(model);
2235
+ const durable = options.durable === true;
2236
+ const requestId = durable ? durableId(remote, mutation.name, options.requestId) : options.requestId ?? `${bound.contract.name}-${remote.mutations.sequence + 1}`;
2237
+ const tempId = `${requestId}.tmp`;
2238
+ const known = Option.fromUndefinedOr(remote.mutations.operations.get(requestId));
2239
+ const viewer = viewerOf(model);
2240
+ const visible = visibleStoreOf(remote.entities, remote.optimistic, bound.operators);
2241
+ const predicted = mutation.write === void 0 ? void 0 : predictionOfWrite(mutation.write, input, options.keys, {
2242
+ derived: definition.registry.derived,
2243
+ store: visible,
2244
+ viewer,
2245
+ now: Option.fromUndefinedOr(options.now)
2246
+ });
2247
+ const write = mutation.write;
2248
+ const refused = write === void 0 || options.optimistic !== void 0 ? Option.none() : Option.orElse(Option.map(forbiddenByWrite(write, input, options.keys, visible), ({ reason }) => remoteError(new RemoteForbidden({
2249
+ message: Option.getOrElse(reason, () => "Forbidden"),
2250
+ ...Option.match(reason, {
2251
+ onNone: () => ({}),
2252
+ onSome: (said) => ({ reason: said })
2253
+ })
2254
+ }))), () => Option.map(brokenByWrite(write, input, options.keys, {
2255
+ viewer,
2256
+ now: Option.fromUndefinedOr(options.now)
2257
+ }, visible, bound.operators), (held) => remoteError({
2258
+ _tag: "RemoteMutationError",
2259
+ message: held.message,
2260
+ refusal: Schema.encodeUnknownSync(mutation.Refusal)({
2261
+ _tag: "Invalid",
2262
+ entity: Option.some(held.entity),
2263
+ rule: Option.some(held.id),
2264
+ message: held.message,
2265
+ path: held.path
2266
+ })
2267
+ })));
2268
+ const optimistic = Option.isSome(refused) ? void 0 : typeof options.optimistic === "function" ? options.optimistic({
2269
+ requestId,
2270
+ tempId
2271
+ }) : options.optimistic ?? predicted;
2272
+ const queued = durable && Option.isNone(refused);
2273
+ const started = updateRemote(remote, {
2274
+ _tag: "MutationStarted",
2275
+ requestId,
2276
+ ...optimistic === void 0 ? {} : { optimistic },
2277
+ ...Option.isSome(viewer) ? { viewer: viewer.value } : {},
2278
+ write: {
2279
+ mutation: mutation.name,
2280
+ input,
2281
+ ...options.keys === void 0 ? {} : { keys: options.keys },
2282
+ ...options.now === void 0 ? {} : { now: options.now },
2283
+ ...queued ? { durable: {
2284
+ input: Schema.encodeUnknownSync(mutation.Input)(input),
2285
+ shown: shownAt(model, optimistic ?? [])
2286
+ } } : {}
2287
+ }
2288
+ });
2289
+ const startedModel = store.set(model, started);
2290
+ return {
2291
+ model: startedModel,
2292
+ requestId,
2293
+ tempId,
2294
+ command: {
2295
+ name: `Remote.mutate(${mutation.name})`,
2296
+ args: { requestId },
2297
+ effect: Effect.flatMap(Effect.sync(() => Option.getOrElse(known, mintOperation)), (operation) => queued ? Effect.succeed({
2298
+ _tag: "MutationQueued",
2299
+ requestId
2300
+ }) : Option.isSome(refused) ? Effect.succeed({
2301
+ _tag: "MutationFailed",
2302
+ requestId,
2303
+ error: refused.value,
2304
+ operation
2305
+ }) : mutateRemote(mutation, input, requestId, {
2306
+ keys: options.keys,
2307
+ operation,
2308
+ shown: shownAt(startedModel, optimistic ?? []),
2309
+ now: options.now
2310
+ }).pipe(Effect.matchEffect({
2311
+ onFailure: (error) => Effect.succeed({
2312
+ _tag: "MutationFailed",
2313
+ requestId,
2314
+ error: remoteError(error),
2315
+ operation
2316
+ }),
2317
+ onSuccess: (outcome) => Effect.map(Clock.currentTimeMillis, (at) => ({
2318
+ _tag: "MutationSucceeded",
2319
+ requestId,
2320
+ entities: outcome.entities,
2321
+ connections: outcome.connections,
2322
+ deleted: outcome.deleted,
2323
+ ...outcome.read === void 0 ? {} : { read: outcome.read },
2324
+ now: at
2325
+ }))
2326
+ })))
2327
+ }
2328
+ };
2329
+ },
2330
+ mutation: (model, requestId) => mutationStatus(store.get(model).mutations, requestId),
2331
+ refusal: (model, requestId, mutation) => {
2332
+ const status = mutationStatus(store.get(model).mutations, requestId);
2333
+ return status._tag === "Failed" ? refusalOf(status.error, mutation) : Option.none();
2334
+ },
2335
+ failedWrites: (model, mutation) => {
2336
+ if (mutation !== void 0) assertRegistered(bound, "Mutation", definition.registry.mutations, mutation.name);
2337
+ const { mutations } = store.get(model);
2338
+ return [...mutations.failed].flatMap((requestId) => {
2339
+ const write = mutations.writes.get(requestId);
2340
+ const error = mutations.errors.get(requestId);
2341
+ if (write === void 0 || error === void 0 || mutations.pending.has(requestId)) return [];
2342
+ if (mutation !== void 0 && write.mutation !== mutation.name) return [];
2343
+ const descriptor = registeredMutation(write.mutation);
2344
+ return [{
2345
+ requestId,
2346
+ mutation: descriptor,
2347
+ input: write.input,
2348
+ error,
2349
+ refusal: refusalOf(error, descriptor)
2350
+ }];
2351
+ });
2352
+ },
2353
+ retry: (model, requestId) => {
2354
+ const { mutations, outbox } = store.get(model);
2355
+ const write = mutations.writes.get(requestId);
2356
+ if (write === void 0 || mutationStatus(mutations, requestId)._tag !== "Failed") return Option.none();
2357
+ return Option.some(domain.mutate(model, registeredMutation(write.mutation), write.input, {
2358
+ requestId,
2359
+ optimistic: write.optimistic,
2360
+ keys: write.keys,
2361
+ now: Option.getOrUndefined(write.now),
2362
+ durable: outbox.queue.some((queued) => operationOf(queued) === requestId)
2363
+ }));
2364
+ },
2365
+ dismiss: (model, requestId) => {
2366
+ const remote = store.get(model);
2367
+ const dismissed = dismissQueued(remote, requestId);
2368
+ if (dismissed !== remote) return store.set(model, dismissed);
2369
+ if (mutationStatus(remote.mutations, requestId)._tag !== "Failed") return model;
2370
+ return store.set(model, {
2371
+ ...remote,
2372
+ mutations: forgetFailure(remote.mutations, requestId)
2373
+ });
2374
+ },
2375
+ liveStatus: (model, active) => {
2376
+ assertOwned(active);
2377
+ const resolvedAt = readers.get(active) ?? memoizedResolved(active);
2378
+ const live = liveAskOf(askedUnion(resolvedAt(model).map(({ projection }) => projection)));
2379
+ if (live.requirements.length === 0 && live.queries.length === 0) return { _tag: "Idle" };
2380
+ const remote = store.get(model);
2381
+ const stream = liveStreamKey(live);
2382
+ const health = remote.streams[stream];
2383
+ if (health === void 0) return { _tag: "Live" };
2384
+ return Option.match(health.refused, {
2385
+ onSome: (error) => ({
2386
+ _tag: "Refused",
2387
+ error
2388
+ }),
2389
+ onNone: () => remote.gaps.has(stream) ? {
2390
+ _tag: "Reconnecting",
2391
+ attempt: health.failures,
2392
+ error: health.error
2393
+ } : { _tag: "Live" }
2394
+ });
2395
+ },
2396
+ outboxStatus: (model) => outboxStatus(store.get(model).outbox),
2397
+ reduce,
2398
+ inspect: (model) => inspectRemote(store.get(model)),
2399
+ persistence: (options) => {
2400
+ const connections = (options.connections ?? []).map(connectionIdentity);
2401
+ const snapshotOf = options.snapshot ?? ((remote) => RemotePersistence.snapshotOf(remote, { connections }));
2402
+ const debounce = options.debounce ?? "250 millis";
2403
+ let last = {
2404
+ entities: emptyStore,
2405
+ connections: {},
2406
+ scope: "",
2407
+ text: Option.none()
2408
+ };
2409
+ const textOf = (remote, scope) => {
2410
+ if (last.entities !== remote.entities || last.connections !== remote.connections || last.scope !== scope) last = {
2411
+ entities: remote.entities,
2412
+ connections: remote.connections,
2413
+ scope,
2414
+ text: Option.fromUndefinedOr(RemotePersistence.dehydrate(snapshotOf(remote), {
2415
+ scope,
2416
+ maxBytes: options.maxBytes
2417
+ }))
2418
+ };
2419
+ return last.text;
2420
+ };
2421
+ const hydratedFrom = (key, scope) => RemotePersistence.restore({
2422
+ key,
2423
+ scope,
2424
+ maxBytes: options.maxBytes
2425
+ }).pipe(Effect.map((snapshot) => ({
2426
+ _tag: "Hydrated",
2427
+ entities: snapshot.entities,
2428
+ connections: snapshot.connections,
2429
+ merge: "preserve-existing",
2430
+ from: key
2431
+ })), Effect.option);
2432
+ const restoreEntry = {
2433
+ dependenciesSchema: Schema.Struct({
2434
+ key: Schema.String,
2435
+ scope: Schema.String,
2436
+ restored: Schema.Boolean
2437
+ }),
2438
+ modelToDependencies: (model) => {
2439
+ const key = options.key(model);
2440
+ return {
2441
+ key,
2442
+ scope: options.scope(model),
2443
+ restored: Option.contains(store.get(model).restoredFrom, key)
2444
+ };
2445
+ },
2446
+ dependenciesToStream: ({ key, scope, restored }) => restored ? Stream.empty : Stream.fromEffect(hydratedFrom(key, scope)).pipe(Stream.flatMap(Option.match({
2447
+ onNone: () => Stream.empty,
2448
+ onSome: Stream.make
2449
+ })))
2450
+ };
2451
+ const save = {
2452
+ dependenciesSchema: Schema.Struct({
2453
+ key: Schema.String,
2454
+ restored: Schema.Boolean,
2455
+ text: Schema.OptionFromNullOr(Schema.String)
2456
+ }),
2457
+ modelToDependencies: (model) => {
2458
+ const remote = store.get(model);
2459
+ const key = options.key(model);
2460
+ const restored = Option.contains(remote.restoredFrom, key);
2461
+ return {
2462
+ key,
2463
+ restored,
2464
+ text: restored ? textOf(remote, options.scope(model)) : Option.none()
2465
+ };
2466
+ },
2467
+ dependenciesToStream: ({ key, restored, text }) => restored ? Stream.fromEffect(Effect.gen(function* () {
2468
+ yield* Effect.sleep(debounce);
2469
+ const kv = yield* KeyValueStore.KeyValueStore;
2470
+ yield* Option.match(text, {
2471
+ onNone: () => kv.remove(key),
2472
+ onSome: (written) => kv.set(key, written)
2473
+ });
2474
+ }).pipe(Effect.ignore)).pipe(Stream.drain) : Stream.empty
2475
+ };
2476
+ return {
2477
+ key: `persistence:${bound.contract.name}`,
2478
+ handles: [],
2479
+ subscriptions: Subscription.make()(() => ({
2480
+ "persistence.restore": restoreEntry,
2481
+ "persistence.save": save
2482
+ })),
2483
+ restore: (model) => Effect.map(hydratedFrom(options.key(model), options.scope(model)), Option.match({
2484
+ onNone: () => model,
2485
+ onSome: (message) => reduce(model, message)
2486
+ }))
2487
+ };
2488
+ },
2489
+ outbox: (options) => {
2490
+ /** How this version reads a stored input of `mutation`; none for one it does not register. */
2491
+ const inputOf = (mutation) => Option.map(Option.fromUndefinedOr(definition.registry.mutations.get(mutation)), (descriptor) => (input) => Option.map(Schema.decodeUnknownOption(descriptor.Input)(input), (decoded) => ({ decoded })));
2492
+ const readOf = (key, scope, stored, extra = {}) => ({
2493
+ _tag: "OutboxRead",
2494
+ key,
2495
+ scope,
2496
+ entries: readEntries(stored, inputOf),
2497
+ done: stored.done,
2498
+ ...extra
2499
+ });
2500
+ /** Tells the other tabs on `key` the store changed, with the answer that changed it. */
2501
+ const announce = (key, outcome) => Effect.sync(() => Option.map(channelFor(key), (channel) => {
2502
+ channel.postMessage({ outcome: Option.getOrUndefined(outcome) });
2503
+ channel.close();
2504
+ }));
2505
+ /** The store with `change` merged in, under its lock, so two tabs never write it at once. */
2506
+ const writeStored = (key, scope, change) => withLock(storeLock(key), Effect.gen(function* () {
2507
+ if (yield* othersWaiting(key, scope)) return yield* Effect.fail(/* @__PURE__ */ new Error(`the store under ${key} holds another scope's waiting writes; give each principal a key of its own`));
2508
+ const merged = mergeStored(yield* readStored(key, scope), change);
2509
+ yield* (yield* KeyValueStore.KeyValueStore).set(key, storedText(merged, scope));
2510
+ return merged;
2511
+ }));
2512
+ /** What the store holds, as a page reads it; this page's own queue, and why, where it cannot be read. */
2513
+ const restored = (key, scope, session) => readStored(key, scope).pipe(Effect.map((stored) => readOf(key, scope, stored, Option.match(session, {
2514
+ onNone: () => ({}),
2515
+ onSome: (minted) => ({ session: minted })
2516
+ }))), Effect.catch((error) => Effect.succeed(readOf(key, scope, {
2517
+ entries: [],
2518
+ done: []
2519
+ }, {
2520
+ ...Option.match(session, {
2521
+ onNone: () => ({}),
2522
+ onSome: (minted) => ({ session: minted })
2523
+ }),
2524
+ unsaved: error.message
2525
+ }))));
2526
+ /** Whether the page's queue is the one stored under `key` for `scope`. */
2527
+ const holding = (outbox, key, scope) => Option.contains(outbox.key, key) && Option.contains(outbox.scope, scope);
2528
+ const minted = (has) => has ? Effect.succeed(Option.none()) : Effect.sync(() => Option.some(mintOperation()));
2529
+ let lastText = {
2530
+ outbox: void 0,
2531
+ scope: "",
2532
+ text: ""
2533
+ };
2534
+ const textOf = (outbox, scope) => {
2535
+ if (lastText.outbox !== outbox || lastText.scope !== scope) lastText = {
2536
+ outbox,
2537
+ scope,
2538
+ text: storedText(storedOf(outbox), scope)
2539
+ };
2540
+ return lastText.text;
2541
+ };
2542
+ const restoreEntry = {
2543
+ dependenciesSchema: Schema.Struct({
2544
+ key: Schema.String,
2545
+ scope: Schema.String,
2546
+ restored: Schema.Boolean,
2547
+ session: Schema.Boolean
2548
+ }),
2549
+ modelToDependencies: (model) => {
2550
+ const { outbox } = store.get(model);
2551
+ const key = options.key(model);
2552
+ const scope = options.scope(model);
2553
+ return {
2554
+ key,
2555
+ scope,
2556
+ restored: holding(outbox, key, scope),
2557
+ session: Option.isSome(outbox.session)
2558
+ };
2559
+ },
2560
+ dependenciesToStream: ({ key, scope, restored: done, session }) => done ? Stream.empty : Stream.fromEffect(Effect.flatMap(minted(session), (mint) => restored(key, scope, mint)))
2561
+ };
2562
+ const save = {
2563
+ dependenciesSchema: Schema.Struct({
2564
+ key: Schema.String,
2565
+ scope: Schema.String,
2566
+ text: Schema.OptionFromNullOr(Schema.String)
2567
+ }),
2568
+ modelToDependencies: (model) => {
2569
+ const { outbox } = store.get(model);
2570
+ const key = options.key(model);
2571
+ const scope = options.scope(model);
2572
+ return {
2573
+ key,
2574
+ scope,
2575
+ text: holding(outbox, key, scope) ? Option.some(textOf(outbox, scope)) : Option.none()
2576
+ };
2577
+ },
2578
+ dependenciesToStream: ({ key, scope, text }) => Option.match(text, {
2579
+ onNone: () => Stream.empty,
2580
+ onSome: (written) => {
2581
+ const mine = storedFromText(written);
2582
+ return Stream.fromEffect(writeStored(key, scope, mine).pipe(Effect.tap(() => announce(key, Option.none())), Effect.map((merged) => readOf(key, scope, merged)), Effect.catch((error) => Effect.succeed(readOf(key, scope, mine, { unsaved: error.message })))));
2583
+ }
2584
+ })
2585
+ };
2586
+ /** Sends `write`, as the one tab on `key` that sends, until the server answers it. */
2587
+ const sent = (key, scope, write) => Stream.callback((queue) => {
2588
+ const emit = (message) => Queue.offer(queue, message);
2589
+ /**
2590
+ * The outcome told this tab and the others first, so each settles
2591
+ * with the server's answer, then written to the store before the
2592
+ * lock goes, so the next sender reads it.
2593
+ */
2594
+ const settle = (message, change) => emit(message).pipe(Effect.andThen(announce(key, Option.some(message))), Effect.andThen(writeStored(key, scope, change)), Effect.andThen(announce(key, Option.none())), Effect.ignore);
2595
+ const operation = write.operation;
2596
+ return withLock(sendLock(key), Effect.gen(function* () {
2597
+ const there = yield* Effect.option(readStored(key, scope));
2598
+ if (Option.isSome(there) && settledIn(there.value, operation)) {
2599
+ yield* emit(readOf(key, scope, there.value));
2600
+ return;
2601
+ }
2602
+ const mutation = registeredMutation(write.mutation);
2603
+ const input = Schema.decodeUnknownSync(mutation.Input)(write.input);
2604
+ for (let attempt = 1;; attempt++) {
2605
+ const result = yield* Effect.result(mutateRemote(mutation, input, operation, {
2606
+ keys: write.keys,
2607
+ operation,
2608
+ shown: write.shown,
2609
+ now: write.now
2610
+ }));
2611
+ if (Result.isSuccess(result)) {
2612
+ const outcome = result.success;
2613
+ const at = yield* Clock.currentTimeMillis;
2614
+ return yield* settle({
2615
+ _tag: "MutationSucceeded",
2616
+ requestId: operation,
2617
+ entities: outcome.entities,
2618
+ connections: outcome.connections,
2619
+ deleted: outcome.deleted,
2620
+ ...outcome.read === void 0 ? {} : { read: outcome.read },
2621
+ now: at
2622
+ }, {
2623
+ entries: [],
2624
+ done: [operation]
2625
+ });
2626
+ }
2627
+ const error = remoteError(result.failure);
2628
+ if (result.failure._tag === "Unreachable") {
2629
+ yield* emit({
2630
+ _tag: "OutboxDelayed",
2631
+ requestId: operation,
2632
+ attempt,
2633
+ error
2634
+ });
2635
+ yield* Effect.race(Effect.flatMap(backoff(attempt, options.retry), Effect.sleep), onlineAgain);
2636
+ continue;
2637
+ }
2638
+ return yield* settle({
2639
+ _tag: "MutationFailed",
2640
+ requestId: operation,
2641
+ error,
2642
+ operation
2643
+ }, {
2644
+ entries: [{
2645
+ ...write,
2646
+ revision: write.revision + 1,
2647
+ state: {
2648
+ _tag: "Failed",
2649
+ error
2650
+ }
2651
+ }],
2652
+ done: []
2653
+ });
2654
+ }
2655
+ })).pipe(Effect.ensuring(Queue.end(queue)));
2656
+ });
2657
+ let lastHead = {
2658
+ outbox: void 0,
2659
+ head: Option.none()
2660
+ };
2661
+ const headTextOf = (outbox) => {
2662
+ if (lastHead.outbox !== outbox) lastHead = {
2663
+ outbox,
2664
+ head: Option.map(headOf(outbox), (write) => JSON.stringify(write))
2665
+ };
2666
+ return lastHead.head;
2667
+ };
2668
+ const send = {
2669
+ dependenciesSchema: Schema.Struct({
2670
+ key: Schema.String,
2671
+ scope: Schema.String,
2672
+ head: Schema.OptionFromNullOr(Schema.String)
2673
+ }),
2674
+ modelToDependencies: (model) => {
2675
+ const { outbox } = store.get(model);
2676
+ const key = options.key(model);
2677
+ const scope = options.scope(model);
2678
+ return {
2679
+ key,
2680
+ scope,
2681
+ head: holding(outbox, key, scope) ? headTextOf(outbox) : Option.none()
2682
+ };
2683
+ },
2684
+ dependenciesToStream: ({ key, scope, head }) => Option.match(head, {
2685
+ onNone: () => Stream.empty,
2686
+ onSome: (text) => sent(key, scope, Schema.decodeUnknownSync(StoredWrite)(JSON.parse(text)))
2687
+ })
2688
+ };
2689
+ /** What another tab announced: the answer it heard first, then the store as it now is. */
2690
+ const heard = (key, scope, data) => Effect.map(restored(key, scope, Option.none()), (read) => [...Option.toArray(outcomeOf(data)), read]);
2691
+ const watch = {
2692
+ dependenciesSchema: Schema.Struct({
2693
+ key: Schema.String,
2694
+ scope: Schema.String,
2695
+ restored: Schema.Boolean
2696
+ }),
2697
+ modelToDependencies: (model) => {
2698
+ const key = options.key(model);
2699
+ const scope = options.scope(model);
2700
+ return {
2701
+ key,
2702
+ scope,
2703
+ restored: holding(store.get(model).outbox, key, scope)
2704
+ };
2705
+ },
2706
+ dependenciesToStream: ({ key, scope, restored: read }) => read ? Stream.callback((queue) => Effect.acquireRelease(Effect.sync(() => Option.map(channelFor(key), (channel) => {
2707
+ channel.onmessage = (event) => {
2708
+ Queue.offerUnsafe(queue, event.data);
2709
+ };
2710
+ return channel;
2711
+ })), (channel) => Effect.sync(() => Option.map(channel, (open) => open.close())))).pipe(Stream.mapEffect((data) => heard(key, scope, data)), Stream.flatMap((messages) => Stream.fromIterable(messages))) : Stream.empty
2712
+ };
2713
+ return {
2714
+ key: `outbox:${bound.contract.name}`,
2715
+ handles: [],
2716
+ subscriptions: Subscription.make()(() => ({
2717
+ "outbox.restore": restoreEntry,
2718
+ "outbox.save": save,
2719
+ "outbox.send": send,
2720
+ "outbox.watch": watch
2721
+ })),
2722
+ restore: (model) => Effect.map(Effect.flatMap(minted(Option.isSome(store.get(model).outbox.session)), (mint) => restored(options.key(model), options.scope(model), mint)), (message) => reduce(model, message))
2723
+ };
2724
+ },
2725
+ wiring: (active, options = {}) => ({
2726
+ key: `remote:${bound.contract.name}`,
2727
+ shared: options.shared ?? [],
2728
+ handles: Record.keys(remoteMessageCases),
2729
+ route: (model, message) => isRemoteMessage(message) ? Option.some(reduceReporting(bound.store, reduce, model, message, (options.shared ?? []).some((tag) => tag === "MutationFailed"))) : Option.none(),
2730
+ subscriptions: brandEntries(domain.subscriptions(active, options)),
2731
+ contract: bound.contract
2732
+ })
2733
+ };
2734
+ return domain;
2735
+ };
2736
+ /** The outbox's own answers, as another tab announced them: a settled write, and nothing else. */
2737
+ const announcedOutcome = Schema.Struct({ outcome: Schema.optional(remoteMessageSchema) });
2738
+ const outcomeOf = (data) => Option.filter(Option.flatMap(Schema.decodeUnknownOption(announcedOutcome)(data), ({ outcome }) => Option.fromUndefinedOr(outcome)), (outcome) => outcome._tag === "MutationSucceeded" || outcome._tag === "MutationFailed");
2739
+ /** The window keys of a `QueryOptions`, and only those, without the undefined ones. */
2740
+ /**
2741
+ * How many rows a read of `ref` shows: its window's `first` or `last`, as
2742
+ * `Data.more` has grown it. `undefined` for a window with a cursor or no size,
2743
+ * which shows whatever the connection holds.
2744
+ */
2745
+ const windowSize = (remote, ref) => {
2746
+ const { first, last, after, before } = ref.window;
2747
+ if (after !== void 0 || before !== void 0) return void 0;
2748
+ const size = first ?? last;
2749
+ return size === void 0 ? void 0 : remote.grown[windowGrowthKey(ref.identity, stableStringify(ref.window))] ?? size;
2750
+ };
2751
+ /** The edges a window of `size` shows, from the start (`first`) or the end (`last`), and which end it cut. */
2752
+ const cutToWindow = (edges, window, size) => {
2753
+ if (size === void 0 || edges.length <= size) return {
2754
+ edges,
2755
+ cutBefore: false,
2756
+ cutAfter: false
2757
+ };
2758
+ return window.first === void 0 ? {
2759
+ edges: edges.slice(edges.length - size),
2760
+ cutBefore: true,
2761
+ cutAfter: false
2762
+ } : {
2763
+ edges: edges.slice(0, size),
2764
+ cutBefore: false,
2765
+ cutAfter: true
2766
+ };
2767
+ };
2768
+ /**
2769
+ * Whether a row a window of `size` would show was deleted: one of `edges`
2770
+ * gone before the window, read from its end, fills with rows still `shown`.
2771
+ * A delete past the window is another window's (#287).
2772
+ */
2773
+ const deletedInWindow = (edges, shown, window, size, gone) => {
2774
+ const ordered = window.first === void 0 ? [...edges].reverse() : edges;
2775
+ let filled = 0;
2776
+ for (const edge of ordered) {
2777
+ if (size !== void 0 && filled >= size) return false;
2778
+ if (gone(entityKey(edge.ref.entity, edge.ref.id))) return true;
2779
+ if (shown.has(edge.key)) filled++;
2780
+ }
2781
+ return false;
2782
+ };
2783
+ const pickWindow = (window) => ({
2784
+ ...window.first === void 0 ? {} : { first: window.first },
2785
+ ...window.last === void 0 ? {} : { last: window.last },
2786
+ ...window.after === void 0 ? {} : { after: window.after },
2787
+ ...window.before === void 0 ? {} : { before: window.before }
2788
+ });
2789
+ export { AnswerRead, ConnectionChange, ConnectionChangeSchema, Denied, DerivedBy, Diagnostics, Entity, Freshness, IDENTITY_SEPARATOR, Live, LiveChange, LiveEdge, LiveRequirement, MAX_FIELDS_PER_REQUEST, MAX_LIVE_QUERIES, MAX_RELATION_DEPTH, MAX_SHOWN_ENTITIES, Mutate, Mutation, MutationRequest, MutationResult, NormalizedEntity, OperationId, OutboxItemState, PageSize, PredictionPart, Query, QueryEdge, QueryRequest, QueryResult, QueryRpc, RELATION_ALIAS, REMOTE_CACHE_VERSION, REMOTE_PROTOCOL_VERSION, Read, ReadBatch, ReadBatchResult, ReadRequest, ReadWhy, Refusal, RelationAnnotation, RelationEntityAnnotation, RelationRequest, Remote, RemoteClient, RemoteConnections, RemoteData, RemoteForbidden, RemoteJsonAnswer, RemoteJsonRequest, RemoteLiveError, RemoteLiveRefused, RemoteMutationError, RemotePersistence, RemotePolicy, RemoteProtocolError, RemoteQueryError, RemoteReadError, RemoteRequirements, RemoteRpc, RemoteUnauthenticated, RemoteUnreachable, RemoteUnsatisfied, Requirement, RowPlace, Selection, SettledFields, ShownBy, ShownSlice, Why, WhyTerm, WindowSchema, WireBoundary, addLayer, addOverlay, aliasedField, applyConnectionEvent, applyEntityEvent, askAgain, assemble, beginMutation, beginOptimistic, belongsEncoded, brokenByWrite, classifyLive, clearStale, coalesceQueries, coalesceReads, coalescedLayer, connectionChangeOf, connectionIdentity, connectionsOf, cursor, deadlineOf, departedLimit, derivedOfWrite, dismissQueued, edge, emptyConnection, emptyLiveState, emptyMutationState, emptyOptimistic, emptyRefresh, emptyStore, entityKey, entry, failMutation, failureOf, fieldMark, forbiddenByWrite, forgetFailure, forgetRemote, gc, goneIn, hasField, hasNext, hasPrevious, heldOnlyByRequests, httpWithLive, initialRemoteModel, inspectEntity, inspectRemote, invalidateConnections, isFieldFailed, isFieldStale, isFieldUnavailable, isGapped, isLoading, isLoadingThrough, isQueryLoading, isRefPage, isRemoteMessage, isTombstone, items, liveEventOf, liveFetch, liveHasNext, liveHasPrevious, markStale, matching, merge, mintOperation, missingFields, mutateRemote, mutationStatus, noFailures, onlyOverlaid, optimisticOfWrite, overlaysOf, pageSchema, patchOfWrite, placeIn, plan, plannedStore, predictionOfWrite, pruneOverlays, queryIdentity, reachable, readField, recentRoots, reconcileMutation, refParts, refreshIsInFlight, refreshedAt, refsIn, relationAlias, relationOf, relationShape, remoteDataSchema, remoteError, remoteErrorSchema, remoteMessageCases, remoteMessageSchema, remoteModelSchema, remove, removeLayer, removeOverlay, requirementKey, requirementsOf, resolvedPatches, retentionRootsSchema, schemaOf, segment, setStale, setUnavailable, settleFailure, settleSuccess, settledLater, stableStringify, targetsOf, terminal, tombstone, unavailableOf, unknown, updateRemote, visibleItems, visibleStore, windowGrowthKey, windowKey, withRefreshRequested, writeEntities, writeEntity, writeRead };
2790
+
2791
+ //# sourceMappingURL=index.js.map