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
@@ -0,0 +1,612 @@
1
+ import { own, put } from "./record.js";
2
+ import { refParts } from "./relation.js";
3
+ import { emptyStore, entityKey, tombstone, writeEntities } from "./store.js";
4
+ import { holderOf } from "./held.js";
5
+ import { Data, Effect, Match, Option, Record, Schema } from "effect";
6
+ import { Derivation, Entity, Held, Write } from "foldkit-entity";
7
+ // src/mutation.ts
8
+ /**
9
+ * Mutation status and idempotent reconciliation.
10
+ *
11
+ * A mutation result is reconciled into the store **at most once per
12
+ * `requestId`**, so a transport retry cannot apply the same change twice. An
13
+ * unknown or already-applied result is a no-op.
14
+ */
15
+ const emptyMutationState = {
16
+ pending: /* @__PURE__ */ new Set(),
17
+ applied: /* @__PURE__ */ new Set(),
18
+ failed: /* @__PURE__ */ new Set(),
19
+ errors: /* @__PURE__ */ new Map(),
20
+ operations: /* @__PURE__ */ new Map(),
21
+ writes: /* @__PURE__ */ new Map(),
22
+ sequence: 0,
23
+ order: /* @__PURE__ */ new Map(),
24
+ settled: /* @__PURE__ */ new Map()
25
+ };
26
+ /**
27
+ * How many settled request ids a mutation state retains. A retry settles well
28
+ * inside this window, so bounding the ledger stops a long-lived application
29
+ * from growing it without limit; a retry older than the window would re-apply
30
+ * its entities.
31
+ */
32
+ const MUTATION_ID_WINDOW = 1024;
33
+ /** Records `id` as most recent, evicting the oldest once the window is full. */
34
+ const remember = (ids, id) => {
35
+ const next = new Set(ids);
36
+ next.delete(id);
37
+ next.add(id);
38
+ if (next.size <= MUTATION_ID_WINDOW) return next;
39
+ return new Set([...next].slice(next.size - MUTATION_ID_WINDOW));
40
+ };
41
+ const beginMutation = (state, requestId, write) => ({
42
+ ...state,
43
+ pending: /* @__PURE__ */ new Set([...state.pending, requestId]),
44
+ writes: write === void 0 ? state.writes : new Map([...state.writes, [requestId, write]]),
45
+ sequence: state.sequence + 1,
46
+ order: new Map([...state.order, [requestId, state.sequence + 1]])
47
+ });
48
+ const failMutation = (state, requestId, error, operation) => {
49
+ if (!state.pending.has(requestId)) return state;
50
+ const pending = new Set(state.pending);
51
+ pending.delete(requestId);
52
+ const failed = remember(state.failed, requestId);
53
+ const errors = new Map([...state.errors, [requestId, error]]);
54
+ const operations = new Map([...state.operations, [requestId, operation]]);
55
+ const writes = new Map(state.writes);
56
+ for (const id of errors.keys()) if (!failed.has(id)) errors.delete(id);
57
+ for (const id of operations.keys()) if (!failed.has(id)) operations.delete(id);
58
+ for (const id of writes.keys()) if (!failed.has(id) && !pending.has(id)) writes.delete(id);
59
+ return {
60
+ ...state,
61
+ pending,
62
+ failed,
63
+ errors,
64
+ operations,
65
+ writes,
66
+ ...ordered(state, requestId, [])
67
+ };
68
+ };
69
+ /**
70
+ * `order` without a request that settled, and `settled` with what its answer
71
+ * wrote (`marks`), each kept only while a request started before it is pending.
72
+ */
73
+ const ordered = (state, requestId, marks) => {
74
+ const at = state.order.get(requestId);
75
+ const order = new Map(state.order);
76
+ order.delete(requestId);
77
+ const earliest = Math.min(...order.values());
78
+ const settled = /* @__PURE__ */ new Map();
79
+ for (const [mark, by] of state.settled) if (by > earliest) settled.set(mark, by);
80
+ if (at !== void 0 && at > earliest) for (const mark of marks) settled.set(mark, at);
81
+ return {
82
+ order,
83
+ settled
84
+ };
85
+ };
86
+ /** The ledger with a request's failure forgotten: its id, error, operation and write. */
87
+ const forgetFailure = (state, requestId) => {
88
+ const without = (map) => {
89
+ if (!map.has(requestId)) return map;
90
+ const next = new Map(map);
91
+ next.delete(requestId);
92
+ return next;
93
+ };
94
+ return {
95
+ ...state,
96
+ failed: state.failed.has(requestId) ? new Set([...state.failed].filter((id) => id !== requestId)) : state.failed,
97
+ errors: without(state.errors),
98
+ operations: without(state.operations),
99
+ writes: without(state.writes)
100
+ };
101
+ };
102
+ const mutationStatus = (state, requestId) => {
103
+ if (state.pending.has(requestId)) return { _tag: "Pending" };
104
+ if (state.applied.has(requestId)) return { _tag: "Applied" };
105
+ const error = state.errors.get(requestId);
106
+ return error === void 0 ? { _tag: "Unknown" } : {
107
+ _tag: "Failed",
108
+ error
109
+ };
110
+ };
111
+ /**
112
+ * What requests started after `requestId` already settled, which its answer
113
+ * leaves alone. The server applied requests in the order they started, so a
114
+ * later one's answer is newer: `newer` holds of a field it wrote (its mark)
115
+ * and of a row it deleted (its key), `rewritten` of a row it wrote any field
116
+ * of. An answer of a request no longer pending (after `forget`, or a failure)
117
+ * has no order, and nothing is newer than it.
118
+ */
119
+ const settledLater = (state, requestId) => {
120
+ const at = state.order.get(requestId) ?? Infinity;
121
+ return {
122
+ newer: (mark) => (state.settled.get(mark) ?? -Infinity) > at,
123
+ rewritten: (key) => [...state.settled].some(([mark, by]) => by > at && mark.startsWith(`${key}\u0000`))
124
+ };
125
+ };
126
+ /**
127
+ * Applies a mutation result once and marks the request settled. A second call
128
+ * with the same `requestId` (a retry, or a live event describing the same change)
129
+ * does not re-apply the entities, but still clears `pending`.
130
+ */
131
+ const reconcileMutation = (store, state, requestId, answer) => {
132
+ const repeated = state.applied.has(requestId);
133
+ if (repeated && !state.pending.has(requestId) && !state.failed.has(requestId)) return {
134
+ store,
135
+ state
136
+ };
137
+ const { newer, rewritten } = settledLater(state, requestId);
138
+ const writes = repeated ? [] : answer.entities.flatMap((entity) => {
139
+ const key = entityKey(entity.entity, entity.id);
140
+ if (newer(key)) return [];
141
+ const values = Object.fromEntries(Object.entries(entity.values).filter(([field]) => !newer(`${key}\u0000${field}`)));
142
+ return Object.keys(values).length === 0 ? [] : [{
143
+ key,
144
+ values
145
+ }];
146
+ });
147
+ const deleted = repeated ? [] : (answer.deleted ?? []).map((gone) => entityKey(gone.entity, gone.id)).filter((key) => !rewritten(key));
148
+ const next = deleted.reduce(tombstone, writeEntities(store, writes, answer.now));
149
+ const applied = remember(state.applied, requestId);
150
+ const pending = new Set(state.pending);
151
+ pending.delete(requestId);
152
+ const marks = [...writes.flatMap((write) => Object.keys(write.values).map((field) => `${write.key}\u0000${field}`)), ...deleted];
153
+ return {
154
+ store: next,
155
+ state: {
156
+ ...forgetFailure(state, requestId),
157
+ applied,
158
+ pending,
159
+ ...ordered(state, requestId, marks)
160
+ }
161
+ };
162
+ };
163
+ const conflict = Schema.TaggedStruct("Conflict", {});
164
+ const unmet = Schema.TaggedStruct("Unmet", {});
165
+ /**
166
+ * Another row already holds the id an insert names: its Entity and that id,
167
+ * both the caller's own, and nothing of the row, which may be hidden from them.
168
+ */
169
+ const taken = Schema.TaggedStruct("Taken", {
170
+ entity: Schema.String,
171
+ id: Schema.String
172
+ });
173
+ /**
174
+ * The row the write would leave breaks one of its Entity's invariants: which
175
+ * one (its Entity and its id, since an id is unique only within one), what to
176
+ * tell the author, and the fields to show it beside. None and empty when the
177
+ * invariant reads a member hidden from whoever asked.
178
+ */
179
+ const invalid = Schema.TaggedStruct("Invalid", {
180
+ entity: Schema.OptionFromOptionalNullOr(Schema.String),
181
+ rule: Schema.OptionFromNullOr(Schema.String),
182
+ message: Schema.String,
183
+ path: Schema.Array(Schema.String)
184
+ });
185
+ const conflictOrInvalid = Schema.Union([conflict, invalid]);
186
+ const conflictUnmetOrInvalid = Schema.Union([
187
+ conflict,
188
+ unmet,
189
+ invalid
190
+ ]);
191
+ const takenOrInvalid = Schema.Union([taken, invalid]);
192
+ /** What the constructors typed a write by: its kind, and whether it holds a `when`. */
193
+ const refusalOfWrite = (write) => {
194
+ const moved = () => Option.isSome(write.when) ? conflictUnmetOrInvalid : conflictOrInvalid;
195
+ return Match.value(write).pipe(Match.tagsExhaustive({
196
+ Insert: () => takenOrInvalid,
197
+ Update: moved,
198
+ Delete: moved
199
+ }));
200
+ };
201
+ const anyField = Schema.TaggedStruct("Field", {
202
+ key: Schema.String,
203
+ reason: Schema.Unknown
204
+ });
205
+ /** Refusals a form or an editor knows how to show. */
206
+ const Refusal = {
207
+ /** One input key was refused, for `reason`, which a form shows beside that key. */
208
+ field: (key, reason) => Schema.TaggedStruct("Field", {
209
+ key: Schema.Literal(key),
210
+ reason
211
+ }),
212
+ /** The row moved on since the client read it; the author decides what wins. */
213
+ conflict,
214
+ /** The row does not hold what the write's `when` asks of it, where it would commit. */
215
+ unmet,
216
+ /** The id an insert names is taken: another create made it, or a retry the server could not tell. */
217
+ taken,
218
+ /** The row the write would leave breaks an invariant of its Entity. */
219
+ invalid,
220
+ /** Whether a refusal, of whatever mutation, is a `Refusal.field`. */
221
+ isField: Schema.is(anyField),
222
+ /** Whether a refusal, of whatever mutation, is `Refusal.conflict`. */
223
+ isConflict: Schema.is(conflict),
224
+ /** Whether a refusal, of whatever mutation, is `Refusal.unmet`. */
225
+ isUnmet: Schema.is(unmet),
226
+ /** Whether a refusal, of whatever mutation, is `Refusal.taken`. */
227
+ isTaken: Schema.is(taken),
228
+ /** Whether a refusal, of whatever mutation, is `Refusal.invalid`. */
229
+ isInvalid: Schema.is(invalid),
230
+ /**
231
+ * Whether a refusal is `Refusal.invalid` naming this invariant, by its
232
+ * Entity and its id: the declaration `Entity.invariant` returned, once
233
+ * attached, or one `Entity.invariants` reads. An invariant over a member
234
+ * hidden from the asker names none, so it matches nothing.
235
+ */
236
+ isInvalidBy: (refusal, invariant) => Schema.is(invalid)(refusal) && Option.contains(refusal.entity, invariant.entity) && Option.contains(refusal.rule, invariant.id)
237
+ };
238
+ /** The codec itself, or a `Schema.Struct` over the fields. */
239
+ const schemaOf = (shape) => Schema.isSchema(shape) ? shape : Schema.Struct(shape);
240
+ /**
241
+ * A bound write as the store patches it: its fields as they are, and each
242
+ * relation it points as the ref key the store holds (`'User:u1'`), or null.
243
+ */
244
+ const patchOfWrite = (bound) => ({
245
+ entity: bound.entity,
246
+ id: bound.id,
247
+ values: {
248
+ ...bound.values,
249
+ ...Record.map(bound.links, (link) => Option.match(link, {
250
+ onNone: () => null,
251
+ onSome: (ref) => entityKey(ref.entity, ref.id)
252
+ }))
253
+ }
254
+ });
255
+ /**
256
+ * One member of a row as `store` holds it, for a path from the viewer: a row
257
+ * known gone reaches no row (null), and a row or a field never read is unheld.
258
+ */
259
+ const heldIn = (store) => (row, key) => {
260
+ const entry = store[entityKey(row.entity, row.id)];
261
+ if (entry === void 0) return Effect.succeed(Held.unheld(`${row.entity} ${row.id} is not held`));
262
+ if (entry.tombstone) return Effect.succeed(Held.of(null));
263
+ return Effect.succeed(entry.present.has(key) ? Held.of(entry.values[key]) : Held.unheld(`${row.entity}.${key} of ${row.id} is not held`));
264
+ };
265
+ /**
266
+ * A write bound to its input, with what its Entity sets from the context
267
+ * (`Entity.sets`) as `context` gives it: who asks (`Remote.make`'s viewer),
268
+ * what their row holds (read from `store`), and when the reader acted
269
+ * (`Data.mutate`'s `now`). A member with no value to give is left out of the
270
+ * prediction, and the answer brings it.
271
+ */
272
+ const contextBound = (write, input, keys, context, store) => {
273
+ const bound = Write.bind(write, input, keys);
274
+ const set = Effect.runSync(Write.contextValues(write, context, heldIn(store)));
275
+ return {
276
+ ...bound,
277
+ values: {
278
+ ...bound.values,
279
+ ...set.values
280
+ },
281
+ links: {
282
+ ...bound.links,
283
+ ...set.links
284
+ }
285
+ };
286
+ };
287
+ const nobody = {
288
+ viewer: Option.none(),
289
+ now: Option.none()
290
+ };
291
+ /**
292
+ * What a declared write shows before the server answers: an update's or an
293
+ * insert's row as a patch (an update's only for the keys named, when some
294
+ * are), and an update's increments and computed fields, worked out over the
295
+ * row the store holds when it is read. A delete shows its row deleted. What
296
+ * the context sets is filled from `context` and `store`, where it can be.
297
+ */
298
+ const optimisticOfWrite = (write, input, keys, context = nobody, store = emptyStore) => Match.value(write).pipe(Match.tagsExhaustive({
299
+ Update: () => {
300
+ const bound = contextBound(write, input, keys, context, store);
301
+ return [{
302
+ ...patchOfWrite(bound),
303
+ ...Object.keys(bound.increments).length === 0 ? {} : { adds: bound.increments },
304
+ ...Object.keys(bound.assignments).length === 0 ? {} : { computes: bound.assignments }
305
+ }];
306
+ },
307
+ Insert: () => {
308
+ const patch = patchOfWrite(contextBound(write, input, void 0, context, store));
309
+ return [{
310
+ ...patch,
311
+ values: {
312
+ id: patch.id,
313
+ ...patch.values
314
+ }
315
+ }];
316
+ },
317
+ Delete: () => {
318
+ const { entity, id } = Write.bind(write, input);
319
+ return [{
320
+ _tag: "Delete",
321
+ entity,
322
+ id
323
+ }];
324
+ }
325
+ }));
326
+ const DerivedBy = Data.taggedEnum();
327
+ /**
328
+ * The derived members a declared write moves, as pending patches of the rows
329
+ * its row points at. A count: an insert adds one where it points, a delete
330
+ * takes one away where the row pointed, and a relink does both. An exists:
331
+ * a row now pointing here and at `viewer` is found (its id), and a row the
332
+ * store shows as found by this one is no longer, once the write deletes it or
333
+ * points it elsewhere.
334
+ *
335
+ * Where the row pointed before is read from `store`: the row as held, or,
336
+ * when the store holds an exists found by this row, the row it is found on
337
+ * and the viewer. When neither says, nothing is predicted and the answer
338
+ * decides. A member is moved only on a row that `store` holds it on.
339
+ */
340
+ const derivedOfWrite = (write, input, keys, derived, store, context) => {
341
+ if (derived.length === 0) return [];
342
+ const bound = contextBound(write, input, write._tag === "Update" ? keys : void 0, context, store);
343
+ const row = store[entityKey(bound.entity, bound.id)];
344
+ const held = row === void 0 || row.tombstone ? void 0 : row;
345
+ const viewerKey = Option.map(context.viewer, (ref) => entityKey(ref.entity, ref.id));
346
+ /** What a live row holds in `field`, as the store has it; none for a row gone or not held. */
347
+ const holding = (key, field) => Option.flatMap(Option.fromUndefinedOr(store[key]), (entry) => entry.tombstone ? Option.none() : Option.some(entry.values[field]));
348
+ const found = /* @__PURE__ */ new Map();
349
+ if (write._tag !== "Insert") for (const member of derived.filter(DerivedBy.$is("Exists"))) found.set(member, Object.keys(store).filter((key) => key.startsWith(`${member.owner}:`) && Option.contains(holding(key, member.field), bound.id)));
350
+ const before = (relation) => {
351
+ if (held !== void 0 && held.present.has(relation)) return Option.some(held.values[relation]);
352
+ for (const [member, rows] of found) {
353
+ if (rows.length === 0) continue;
354
+ if (member.relation === relation) return Option.some(rows[0]);
355
+ if (member.viewer === relation && Option.isSome(viewerKey)) return viewerKey;
356
+ }
357
+ return write._tag === "Insert" ? Option.some(null) : Option.none();
358
+ };
359
+ const patched = patchOfWrite(bound).values;
360
+ const after = (relation) => Match.value(write).pipe(Match.tagsExhaustive({
361
+ Delete: () => Option.some(null),
362
+ Insert: () => Option.some(Object.hasOwn(bound.links, relation) ? patched[relation] : null),
363
+ Update: () => Object.hasOwn(bound.links, relation) ? Option.some(patched[relation]) : before(relation)
364
+ }));
365
+ const moves = /* @__PURE__ */ new Map();
366
+ const at = (ref, owner) => {
367
+ const move = moves.get(ref) ?? {
368
+ owner,
369
+ values: {},
370
+ adds: {}
371
+ };
372
+ moves.set(ref, move);
373
+ return move;
374
+ };
375
+ const count = (ref, owner, field, by) => {
376
+ if (typeof ref !== "string") return;
377
+ if (!Option.exists(holding(ref, field), (value) => typeof value === "number")) return;
378
+ const move = at(ref, owner);
379
+ put(move.adds, field, (own(move.adds, field) ?? 0) + by);
380
+ };
381
+ for (const member of derived) DerivedBy.$match(member, {
382
+ Count: ({ relation, owner, field }) => {
383
+ const from = before(relation);
384
+ const to = after(relation);
385
+ if (Option.isNone(from) || Option.isNone(to) || from.value === to.value) return;
386
+ count(from.value, owner, field, -1);
387
+ count(to.value, owner, field, 1);
388
+ },
389
+ Exists: (exists) => {
390
+ const { relation, viewer: by, owner, field } = exists;
391
+ const pointed = after(relation);
392
+ const viewing = Option.flatMap(after(by), (ref) => Option.map(viewerKey, (key) => ref === key));
393
+ for (const ref of found.get(exists) ?? []) if (Option.exists(pointed, (to) => to !== ref) || Option.contains(viewing, false)) put(at(ref, owner).values, field, null);
394
+ if (!Option.contains(viewing, true) || Option.isNone(pointed)) return;
395
+ const to = pointed.value;
396
+ if (typeof to === "string" && Option.contains(holding(to, field), null)) put(at(to, owner).values, field, bound.id);
397
+ }
398
+ });
399
+ return [...moves].flatMap(([ref, { owner, values, adds }]) => {
400
+ const moved = Object.fromEntries(Object.entries(adds).filter(([, by]) => by !== 0));
401
+ return Object.keys(values).length === 0 && Object.keys(moved).length === 0 ? [] : [{
402
+ entity: owner,
403
+ id: refParts(ref).id,
404
+ values,
405
+ ...Object.keys(moved).length === 0 ? {} : { adds: moved }
406
+ }];
407
+ });
408
+ };
409
+ /**
410
+ * A new row's derived members over relations: each count 0 and each exists
411
+ * none, since no row points at a row not yet made. A later pending write that
412
+ * points at it moves them as it would any held row's.
413
+ */
414
+ const freshDerived = (write, input, derived) => {
415
+ const entity = write.input.entity.name;
416
+ const values = {};
417
+ for (const members of derived.values()) for (const member of members) if (member.owner === entity) put(values, member.field, DerivedBy.$match(member, {
418
+ Count: () => 0,
419
+ Exists: () => null
420
+ }));
421
+ if (Object.keys(values).length === 0) return [];
422
+ const { id } = Write.bind(write, input);
423
+ return [{
424
+ entity,
425
+ id,
426
+ values
427
+ }];
428
+ };
429
+ /** Each supplied member of `entity` that says how a client foresees it, by key. */
430
+ const predicted = /* @__PURE__ */ new WeakMap();
431
+ const predictorsOf = (entity) => {
432
+ const known = predicted.get(entity);
433
+ if (known !== void 0) return known;
434
+ const derived = entity.derived;
435
+ const found = Object.entries(derived).flatMap(([key, member]) => Derivation.$match(member.derives, {
436
+ Supplied: ({ predict }) => Option.match(predict, {
437
+ onNone: () => [],
438
+ onSome: (run) => [[key, run]]
439
+ }),
440
+ Count: () => [],
441
+ Exists: () => []
442
+ }));
443
+ predicted.set(entity, found);
444
+ return found;
445
+ };
446
+ /**
447
+ * The supplied members a write's row foresees (`Derived.make`'s `predict`),
448
+ * judged over the row it leaves, held over the store with the write's values:
449
+ * a member foreseen as none is left out, so the row waits for the answer.
450
+ */
451
+ const suppliedOfWrite = (write, own, by, store) => {
452
+ const entity = write.input.entity;
453
+ const predictors = predictorsOf(entity);
454
+ if (predictors.length === 0) return [];
455
+ const holder = holderOf(store);
456
+ const row = holder.written(entity, own.id, own.values, [...Object.keys(own.adds ?? {}), ...Object.keys(own.computes ?? {})]);
457
+ const values = {};
458
+ for (const [key, predict] of predictors) Option.map(predict({
459
+ entity,
460
+ row,
461
+ rows: holder.rows,
462
+ context: by
463
+ }), (value) => {
464
+ put(values, key, value);
465
+ });
466
+ return Object.keys(values).length === 0 ? [] : [{
467
+ entity: entity.name,
468
+ id: own.id,
469
+ values
470
+ }];
471
+ };
472
+ /**
473
+ * Everything a declared write shows before the server answers, from its
474
+ * declarations alone: its own row (`optimisticOfWrite`), with what the
475
+ * context sets filled from `viewer` and `now`, a new row's
476
+ * derived members (`freshDerived`), what its supplied members foresee
477
+ * (`suppliedOfWrite`, Guard's `may`), and the derived members it moves on
478
+ * the rows it points at (`derivedOfWrite`). `derived` is the registry's, by
479
+ * the pointing Entity; `store` is the store as shown, layers and all.
480
+ */
481
+ const predictionOfWrite = (write, input, keys, context) => {
482
+ const by = {
483
+ viewer: context.viewer,
484
+ now: context.now
485
+ };
486
+ const own = optimisticOfWrite(write, input, keys, by, context.store);
487
+ return [
488
+ ...Match.value(write).pipe(Match.tagsExhaustive({
489
+ Insert: () => freshDerived(write, input, context.derived),
490
+ Update: () => [],
491
+ Delete: () => []
492
+ })),
493
+ ...own,
494
+ ...own.flatMap((patch) => "_tag" in patch ? [] : suppliedOfWrite(write, patch, by, context.store)),
495
+ ...derivedOfWrite(write, input, keys, context.derived.get(write.input.entity.name) ?? [], context.store, by)
496
+ ];
497
+ };
498
+ /** The held fields of `row`, plain and encoded, as an invariant is judged over them. */
499
+ const plainRow = (entity, row) => {
500
+ const plain = {};
501
+ for (const key of Object.keys(entity.fields)) Held.match(row.field(key), {
502
+ onHeld: (value) => {
503
+ put(plain, key, value);
504
+ },
505
+ onUnheld: () => void 0
506
+ });
507
+ return plain;
508
+ };
509
+ /**
510
+ * The invariant a declared write's row would break, as the client holds it:
511
+ * the store's row with the write's values over it, a field the write moves
512
+ * (an increment, a computed value) not held, and for an update the row
513
+ * before. Only a definite failure counts: an invariant over a field the
514
+ * client lacks is undecided, and refuses nothing. A delete leaves no row.
515
+ */
516
+ const brokenByWrite = (write, input, keys, context, store, operators) => {
517
+ const entity = write.input.entity;
518
+ const patch = optimisticOfWrite(write, input, keys, context, store).find((each) => !("_tag" in each));
519
+ if (patch === void 0) return Option.none();
520
+ const holder = holderOf(store);
521
+ const after = plainRow(entity, holder.written(entity, patch.id, patch.values, [...Object.keys(patch.adds ?? {}), ...Object.keys(patch.computes ?? {})]));
522
+ const before = Option.getOrUndefined(Option.map(holder.rows.row(entity, patch.id), (row) => plainRow(entity, row)));
523
+ const judged = Entity.judge(entity, write._tag === "Insert" || before === void 0 ? { after } : {
524
+ after,
525
+ before
526
+ }, operators);
527
+ return judged._tag === "Invalid" ? Option.fromUndefinedOr(judged.failures[0]) : Option.none();
528
+ };
529
+ /** Each supplied member of `entity` that says which write its value refuses, by key. */
530
+ const forbidding = /* @__PURE__ */ new WeakMap();
531
+ const forbiddersOf = (entity) => {
532
+ const known = forbidding.get(entity);
533
+ if (known !== void 0) return known;
534
+ const derived = entity.derived;
535
+ const found = Object.entries(derived).flatMap(([key, member]) => Derivation.$match(member.derives, {
536
+ Supplied: ({ forbids }) => Option.match(forbids, {
537
+ onNone: () => [],
538
+ onSome: (run) => [[key, run]]
539
+ }),
540
+ Count: () => [],
541
+ Exists: () => []
542
+ }));
543
+ forbidding.set(entity, found);
544
+ return found;
545
+ };
546
+ /**
547
+ * Why a declared write is refused by what the client holds of its row: a
548
+ * supplied member (Guard's `may`) whose value, as the store shows it, forbids
549
+ * what the write does to the row. None where nothing held says so: an
550
+ * insert, a row not held, or a member not held or not refusing.
551
+ */
552
+ const forbiddenByWrite = (write, input, keys, store) => {
553
+ const entity = write.input.entity;
554
+ const forbidders = forbiddersOf(entity);
555
+ if (forbidders.length === 0) return Option.none();
556
+ const bound = Write.bind(write, input, keys);
557
+ const held = store[entityKey(entity.name, bound.id)];
558
+ if (held === void 0) return Option.none();
559
+ const surface = Match.valueTags(write, {
560
+ Insert: () => Option.none(),
561
+ Delete: () => Option.some({
562
+ kind: "Delete",
563
+ members: [],
564
+ relations: []
565
+ }),
566
+ Update: () => Option.some({
567
+ kind: "Update",
568
+ members: [
569
+ ...Object.keys(bound.values),
570
+ ...Object.keys(bound.increments),
571
+ ...Object.keys(bound.assignments)
572
+ ],
573
+ relations: Object.entries(bound.links).map(([relation, link]) => ({
574
+ relation,
575
+ interaction: Option.isSome(link) ? "Link" : "Unlink"
576
+ }))
577
+ })
578
+ });
579
+ return Option.flatMap(surface, (asked) => Option.firstSomeOf(forbidders.map(([key, forbids]) => held.present.has(key) ? forbids(held.values[key], asked) : Option.none())));
580
+ };
581
+ const Mutation = {
582
+ /**
583
+ * Declares a mutation. `Input` and `Output` are codecs, or the fields of the
584
+ * `Schema.Struct` they would be (`{ id: ProjectId, name: Schema.String }`).
585
+ * `Refusal`, when given, is what the server may refuse with: a codec, usually
586
+ * a union of `Refusal.field(...)` and `Refusal.conflict`.
587
+ */
588
+ /**
589
+ * A mutation that is a declared `Write` (`update`, `insert` or `delete`): its
590
+ * input is the write's, and it answers nothing but the row it changed, as a
591
+ * patch, or its deletion. An insert may be refused as taken, when a row
592
+ * already holds its id; an update or a delete as a conflict, when it
593
+ * `expect`s a revision the row has moved past, and as unmet, when it has a
594
+ * `when`; and any as invalid, when the row it would leave breaks an invariant.
595
+ */
596
+ write: (name, write) => ({
597
+ name,
598
+ Input: write.input.schema,
599
+ Output: Schema.Struct({}),
600
+ Refusal: refusalOfWrite(write),
601
+ write
602
+ }),
603
+ make: (name, config) => ({
604
+ name,
605
+ Input: schemaOf(config.Input),
606
+ Output: schemaOf(config.Output),
607
+ Refusal: config.Refusal ?? Schema.Never
608
+ })
609
+ };
610
+ export { DerivedBy, Mutation, Refusal, beginMutation, brokenByWrite, derivedOfWrite, emptyMutationState, failMutation, forbiddenByWrite, forgetFailure, mutationStatus, optimisticOfWrite, patchOfWrite, predictionOfWrite, reconcileMutation, schemaOf, settledLater };
611
+
612
+ //# sourceMappingURL=mutation.js.map