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/law.js ADDED
@@ -0,0 +1,201 @@
1
+ import { patchedFields } from "./shown.js";
2
+ import { entityKey } from "./store.js";
3
+ import { stableStringify } from "./query.js";
4
+ import "./wire.js";
5
+ import { RemoteClient, remoteError } from "./client.js";
6
+ import { belongsEncoded } from "./matching.js";
7
+ import { Effect, Option, Result, Schema } from "effect";
8
+ // src/law.ts
9
+ /**
10
+ * `Law.check`: one operation run through both interpreters, the client's and
11
+ * the server's, and every part the client predicted compared with what the
12
+ * server then holds (operations-phase6-DESIGN §1). Where the client's answer
13
+ * is not unknown, it must equal the server's, while the write is pending and
14
+ * again once its answer has landed and the prediction is released.
15
+ */
16
+ /** How many rows a list may hold for its placement to be compared: all of them, at once. */
17
+ const LIST_WINDOW = 500;
18
+ /** Every member of `entity` a read can ask for: its fields, its links, its derived members. */
19
+ const membersOf = (entity) => [
20
+ ...Object.keys(entity.fields),
21
+ ...Object.entries(entity.relations).filter(([, relation]) => relation.cardinality === "one").map(([key]) => key),
22
+ ...Object.keys(entity.derived)
23
+ ];
24
+ const same = (left, right) => stableStringify(left) === stableStringify(right);
25
+ const ids = (edges) => edges.map((edge) => edge.ref.id);
26
+ /** Two sides that both said nothing agree, and that agreement is no evidence. */
27
+ const nothingCompared = (mutation) => {
28
+ throw new Error(`[foldkit-remote] Law.check: ${mutation} compared nothing, so it proves nothing. Hold the rows it writes and reads.`);
29
+ };
30
+ /**
31
+ * Runs `config.mutation` as the client would, through `Data.mutate` over a
32
+ * Model holding what the server sends this principal, and through the server
33
+ * (`RemoteClient`: `Remote.clientLayer(RemoteServer.handlers(server, principal))`),
34
+ * then compares each predicted field, each predicted deletion and each list's
35
+ * order with what the server answers after the write.
36
+ *
37
+ * Fails when nothing was compared: two sides that both said nothing agree,
38
+ * and that agreement is no evidence.
39
+ */
40
+ const check = (Data, config) => Effect.gen(function* () {
41
+ const client = yield* RemoteClient;
42
+ const lists = config.lists ?? [];
43
+ const requests = config.holds.flatMap(({ entity, ids }) => ids.map((id) => ({
44
+ entity: entity.name,
45
+ id,
46
+ fields: [...membersOf(entity)]
47
+ })));
48
+ const result = yield* client.read({
49
+ version: 5,
50
+ requests
51
+ });
52
+ let model = Data.reduce(config.model, {
53
+ _tag: "ReadReceived",
54
+ requests,
55
+ result,
56
+ now: config.now
57
+ });
58
+ const page = (ref) => client.query({
59
+ query: ref.query,
60
+ input: Schema.encodeSync(ref.Input)(ref.input),
61
+ window: { first: LIST_WINDOW }
62
+ });
63
+ for (const ref of lists) {
64
+ const served = yield* page(ref);
65
+ model = Data.reduce(model, {
66
+ _tag: "ConnectionMerged",
67
+ connection: ref.identity,
68
+ page: {
69
+ edges: served.edges.map((edge) => ({
70
+ key: edge.key,
71
+ ref: {
72
+ entity: edge.entity,
73
+ id: edge.id
74
+ }
75
+ })),
76
+ start: served.start,
77
+ end: served.end
78
+ }
79
+ });
80
+ }
81
+ const started = Data.mutate(model, config.mutation, config.input, { now: config.now });
82
+ const pending = started.model;
83
+ const remote = Data.store.get(pending);
84
+ const shown = Data.storeOf(pending);
85
+ const layer = remote.optimistic.layers.find((each) => each.id === started.requestId);
86
+ const predicted = /* @__PURE__ */ new Map();
87
+ for (const patch of layer?.patches ?? []) {
88
+ const fields = predicted.get(entityKey(patch.entity, patch.id)) ?? /* @__PURE__ */ new Set();
89
+ for (const field of patchedFields(patch)) fields.add(field);
90
+ predicted.set(entityKey(patch.entity, patch.id), fields);
91
+ }
92
+ for (const deleted of layer?.deletes ?? []) predicted.set(entityKey(deleted.entity, deleted.id), /* @__PURE__ */ new Set());
93
+ let sent = false;
94
+ const answer = yield* started.command.effect.pipe(Effect.provideService(RemoteClient, {
95
+ ...client,
96
+ mutate: (request) => {
97
+ sent = true;
98
+ return client.mutate(request);
99
+ }
100
+ }));
101
+ const agreed = [];
102
+ const disagreed = [];
103
+ const unknown = [];
104
+ const compare = (part, clientSays, serverSays, when = "pending") => {
105
+ if (same(clientSays, serverSays)) agreed.push(part);
106
+ else disagreed.push({
107
+ part,
108
+ when,
109
+ client: clientSays,
110
+ server: serverSays
111
+ });
112
+ };
113
+ if (answer._tag === "MutationFailed" && !sent) {
114
+ const served = yield* Effect.result(client.mutate({
115
+ requestId: started.requestId,
116
+ mutation: config.mutation.name,
117
+ input: Schema.encodeUnknownSync(config.mutation.Input)(config.input),
118
+ now: config.now
119
+ }));
120
+ compare(config.mutation.name, answer.error.refusal ?? answer.error._tag, Result.match(served, {
121
+ onFailure: (error) => remoteError(error).refusal ?? error._tag,
122
+ onSuccess: () => "applied"
123
+ }));
124
+ return {
125
+ agreed,
126
+ disagreed,
127
+ unknown
128
+ };
129
+ }
130
+ if (answer._tag === "MutationFailed") {
131
+ if (predicted.size > 0) compare(config.mutation.name, "applied", answer.error.refusal ?? answer.error);
132
+ return disagreed.length === 0 ? nothingCompared(config.mutation.name) : {
133
+ agreed,
134
+ disagreed,
135
+ unknown
136
+ };
137
+ }
138
+ const rows = [...predicted.keys()].map((key) => {
139
+ const at = key.indexOf(":");
140
+ return {
141
+ entity: key.slice(0, at),
142
+ id: key.slice(at + 1)
143
+ };
144
+ });
145
+ const after = yield* client.read({
146
+ version: 5,
147
+ requests: rows.map((row) => ({
148
+ ...row,
149
+ fields: [...predicted.get(entityKey(row.entity, row.id)) ?? []]
150
+ }))
151
+ });
152
+ const settledModel = Data.reduce(pending, answer);
153
+ const answered = Data.storeOf(settledModel);
154
+ for (const row of rows) {
155
+ const key = entityKey(row.entity, row.id);
156
+ const held = shown[key];
157
+ const stored = after.entities.find((each) => each.entity === row.entity && each.id === row.id);
158
+ const settled = new Set(after.settled.flatMap((each) => each.entity === row.entity && each.id === row.id ? each.fields : []));
159
+ const kept = answered[key];
160
+ compare(key, held !== void 0 && !held.tombstone, stored !== void 0);
161
+ if (kept !== void 0) compare(key, !kept.tombstone, stored !== void 0, "answered");
162
+ if (held === void 0 || held.tombstone || stored === void 0) continue;
163
+ for (const field of predicted.get(key) ?? []) {
164
+ const part = `${key}.${field}`;
165
+ if (!held.present.has(field) || settled.has(field)) {
166
+ unknown.push(part);
167
+ continue;
168
+ }
169
+ compare(part, held.values[field], stored.values[field]);
170
+ if (kept !== void 0 && !kept.tombstone && kept.present.has(field)) compare(part, kept.values[field], stored.values[field], "answered");
171
+ }
172
+ }
173
+ for (const ref of predicted.size === 0 ? [] : lists) {
174
+ const connection = remote.connections[ref.identity];
175
+ const descriptor = Data.definition.registry.queries.get(ref.query);
176
+ const encoded = Schema.encodeSync(ref.Input)(ref.input);
177
+ const judged = [...predicted.keys()].every((key) => descriptor === void 0 || belongsEncoded(shown, descriptor, encoded, key, Data.operators, {
178
+ viewer: layer?.viewer ?? Option.none(),
179
+ now: Option.none()
180
+ }) !== "unknown");
181
+ if (connection === void 0 || connection.stale || !judged) {
182
+ unknown.push(ref.identity);
183
+ continue;
184
+ }
185
+ const served = ids((yield* page(ref)).edges.map((edge) => ({ ref: edge })));
186
+ compare(ref.identity, ids(Data.visibleItems(remote, ref.identity)), served);
187
+ const kept = Data.store.get(settledModel);
188
+ if (kept.connections[ref.identity]?.stale === false) compare(ref.identity, ids(Data.visibleItems(kept, ref.identity)), served, "answered");
189
+ }
190
+ if (agreed.length === 0 && disagreed.length === 0) return nothingCompared(config.mutation.name);
191
+ const wrong = new Set(disagreed.map((each) => each.part));
192
+ return {
193
+ agreed: [...new Set(agreed)].filter((part) => !wrong.has(part)),
194
+ disagreed,
195
+ unknown
196
+ };
197
+ });
198
+ const Law = { check };
199
+ export { Law };
200
+
201
+ //# sourceMappingURL=law.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"law.js","names":[],"sources":["../src/law.ts"],"sourcesContent":["/**\n * `Law.check`: one operation run through both interpreters, the client's and\n * the server's, and every part the client predicted compared with what the\n * server then holds (operations-phase6-DESIGN §1). Where the client's answer\n * is not unknown, it must equal the server's, while the write is pending and\n * again once its answer has landed and the prediction is released.\n */\nimport { Effect, Option, Result, Schema } from 'effect'\nimport type { AnyEntity } from 'foldkit-entity'\nimport { RemoteClient, remoteError } from './client.js'\nimport type { RemoteDomain } from './index.js'\nimport type { MutationDescriptor } from './mutation.js'\nimport { stableStringify, type QueryRef } from './query.js'\nimport { belongsEncoded } from './matching.js'\nimport type { RemoteMessage, RemoteModel } from './model.js'\nimport { patchedFields } from './shown.js'\nimport { entityKey, type EntityStore } from './store.js'\nimport { REMOTE_PROTOCOL_VERSION } from './wire.js'\n\n/** One part both sides answered, and what each said. */\nexport interface LawPart {\n /** `Entity:id.field`, `Entity:id` for whether the row is there, or a list's identity. */\n readonly part: string\n /** When the client said it: while the write was pending, or once its answer had landed. */\n readonly when: 'pending' | 'answered'\n readonly client: unknown\n readonly server: unknown\n}\n\n/**\n * What `Law.check` compared: the parts both sides agreed on, pending and\n * answered, and those they did not.\n */\nexport interface LawReport {\n readonly agreed: ReadonlyArray<string>\n readonly disagreed: ReadonlyArray<LawPart>\n /** Parts the client left to the server: a list it did not place, a field it did not foresee. */\n readonly unknown: ReadonlyArray<string>\n}\n\nexport interface LawConfig<AppModel> {\n /** The application's Model before the client holds anything; `Remote.make`'s viewer reads it. */\n readonly model: AppModel\n /** The rows the client holds, each read whole as the server's principal sees it. */\n readonly holds: ReadonlyArray<{ readonly entity: AnyEntity; readonly ids: ReadonlyArray<string> }>\n /** Lists the client holds, wholly loaded, whose placements are compared. */\n readonly lists?: ReadonlyArray<QueryRef<string, unknown>> | undefined\n readonly mutation: MutationDescriptor<string, any, any, any>\n readonly input: unknown\n /** When the reader acted: what the client predicts with and the request sends. */\n readonly now: number\n}\n\n/** How many rows a list may hold for its placement to be compared: all of them, at once. */\nconst LIST_WINDOW = 500\n\n/** Every member of `entity` a read can ask for: its fields, its links, its derived members. */\nconst membersOf = (entity: AnyEntity): ReadonlyArray<string> => [\n ...Object.keys(entity.fields),\n ...Object.entries(entity.relations)\n .filter(([, relation]) => relation.cardinality === 'one')\n .map(([key]) => key),\n ...Object.keys(entity.derived),\n]\n\nconst same = (left: unknown, right: unknown): boolean =>\n stableStringify(left) === stableStringify(right)\n\nconst ids = (edges: ReadonlyArray<{ readonly ref: { readonly id: string } }>) =>\n edges.map(edge => edge.ref.id)\n\n/** Two sides that both said nothing agree, and that agreement is no evidence. */\nconst nothingCompared = (mutation: string): never => {\n throw new Error(\n `[foldkit-remote] Law.check: ${mutation} compared nothing, so it proves nothing. Hold the rows it writes and reads.`,\n )\n}\n\n/**\n * Runs `config.mutation` as the client would, through `Data.mutate` over a\n * Model holding what the server sends this principal, and through the server\n * (`RemoteClient`: `Remote.clientLayer(RemoteServer.handlers(server, principal))`),\n * then compares each predicted field, each predicted deletion and each list's\n * order with what the server answers after the write.\n *\n * Fails when nothing was compared: two sides that both said nothing agree,\n * and that agreement is no evidence.\n */\nconst check = <AppModel>(\n Data: RemoteDomain<AppModel, any, any, any, any>,\n config: LawConfig<AppModel>,\n): Effect.Effect<LawReport, unknown, RemoteClient> =>\n Effect.gen(function* () {\n const client = yield* RemoteClient\n const lists = config.lists ?? []\n\n // 1. The client holds what the server sends it.\n const requests = config.holds.flatMap(({ entity, ids }) =>\n ids.map(id => ({ entity: entity.name, id, fields: [...membersOf(entity)] })),\n )\n const result = yield* client.read({ version: REMOTE_PROTOCOL_VERSION, requests })\n let model = Data.reduce(config.model, {\n _tag: 'ReadReceived',\n requests,\n result,\n now: config.now,\n })\n const page = (ref: QueryRef<string, unknown>) =>\n client.query({\n query: ref.query,\n input: Schema.encodeSync(ref.Input)(ref.input),\n window: { first: LIST_WINDOW },\n })\n for (const ref of lists) {\n const served = yield* page(ref)\n model = Data.reduce(model, {\n _tag: 'ConnectionMerged',\n connection: ref.identity,\n page: {\n edges: served.edges.map(edge => ({\n key: edge.key,\n ref: { entity: edge.entity, id: edge.id },\n })),\n start: served.start,\n end: served.end,\n },\n })\n }\n\n // 2. The client predicts, as an application's `update` does.\n const started = Data.mutate(model, config.mutation, config.input, { now: config.now })\n const pending = started.model\n const remote: RemoteModel = Data.store.get(pending)\n const shown: EntityStore = Data.storeOf(pending)\n const layer = remote.optimistic.layers.find(each => each.id === started.requestId)\n const predicted = new Map<string, Set<string>>()\n for (const patch of layer?.patches ?? []) {\n const fields = predicted.get(entityKey(patch.entity, patch.id)) ?? new Set<string>()\n for (const field of patchedFields(patch)) fields.add(field)\n predicted.set(entityKey(patch.entity, patch.id), fields)\n }\n for (const deleted of layer?.deletes ?? [])\n predicted.set(entityKey(deleted.entity, deleted.id), new Set())\n\n // 3. The server writes, as the client's own Command sends it.\n let sent = false\n const answer: RemoteMessage = yield* started.command.effect.pipe(\n Effect.provideService(RemoteClient, {\n ...client,\n mutate: request => {\n sent = true\n return client.mutate(request)\n },\n }),\n )\n\n const agreed: Array<string> = []\n const disagreed: Array<LawPart> = []\n const unknown: Array<string> = []\n const compare = (\n part: string,\n clientSays: unknown,\n serverSays: unknown,\n when: LawPart['when'] = 'pending',\n ) => {\n if (same(clientSays, serverSays)) agreed.push(part)\n else disagreed.push({ part, when, client: clientSays, server: serverSays })\n }\n\n if (answer._tag === 'MutationFailed' && !sent) {\n // The client refused it itself: the server is asked, and must refuse it alike.\n const served = yield* Effect.result(\n client.mutate({\n requestId: started.requestId,\n mutation: config.mutation.name,\n input: Schema.encodeUnknownSync(config.mutation.Input)(config.input),\n now: config.now,\n }),\n )\n // A declared refusal by its value; one of who asks (`Forbidden`) by its tag.\n compare(\n config.mutation.name,\n answer.error.refusal ?? answer.error._tag,\n Result.match(served, {\n onFailure: error => remoteError(error).refusal ?? error._tag,\n onSuccess: () => 'applied',\n }),\n )\n return { agreed, disagreed, unknown }\n }\n if (answer._tag === 'MutationFailed') {\n // The client showed the write as made, where it showed anything; the server refused it.\n if (predicted.size > 0)\n compare(config.mutation.name, 'applied', answer.error.refusal ?? answer.error)\n return disagreed.length === 0\n ? nothingCompared(config.mutation.name)\n : { agreed, disagreed, unknown }\n }\n\n // 4. What the server holds after it, read as the same principal.\n const rows = [...predicted.keys()].map(key => {\n const at = key.indexOf(':')\n return { entity: key.slice(0, at), id: key.slice(at + 1) }\n })\n const after = yield* client.read({\n version: REMOTE_PROTOCOL_VERSION,\n requests: rows.map(row => ({\n ...row,\n fields: [...(predicted.get(entityKey(row.entity, row.id)) ?? [])],\n })),\n })\n // The answer lands and the prediction goes: what the client holds then must\n // still be the server's. A field it no longer holds is read again, not wrong.\n const settledModel = Data.reduce(pending, answer)\n const answered: EntityStore = Data.storeOf(settledModel)\n for (const row of rows) {\n const key = entityKey(row.entity, row.id)\n const held = shown[key]\n const stored = after.entities.find(each => each.entity === row.entity && each.id === row.id)\n const settled = new Set(\n after.settled.flatMap(each =>\n each.entity === row.entity && each.id === row.id ? each.fields : [],\n ),\n )\n const kept = answered[key]\n compare(key, held !== undefined && !held.tombstone, stored !== undefined)\n if (kept !== undefined) compare(key, !kept.tombstone, stored !== undefined, 'answered')\n if (held === undefined || held.tombstone || stored === undefined) continue\n for (const field of predicted.get(key) ?? []) {\n const part = `${key}.${field}`\n if (!held.present.has(field) || settled.has(field)) {\n unknown.push(part)\n continue\n }\n compare(part, held.values[field], stored.values[field])\n if (kept !== undefined && !kept.tombstone && kept.present.has(field))\n compare(part, kept.values[field], stored.values[field], 'answered')\n }\n }\n // A list the client changed nothing in agrees with the server's for no reason of its own.\n for (const ref of predicted.size === 0 ? [] : lists) {\n const connection = remote.connections[ref.identity]\n const descriptor = Data.definition.registry.queries.get(ref.query)\n const encoded = Schema.encodeSync(ref.Input)(ref.input) as Readonly<Record<string, unknown>>\n // A row the client could not judge is left where it was, or out: the server places it.\n const judged = [...predicted.keys()].every(\n key =>\n descriptor === undefined ||\n belongsEncoded(shown, descriptor, encoded, key, Data.operators, {\n viewer: layer?.viewer ?? Option.none(),\n now: Option.none(),\n }) !== 'unknown',\n )\n if (connection === undefined || connection.stale || !judged) {\n unknown.push(ref.identity)\n continue\n }\n const served = ids((yield* page(ref)).edges.map(edge => ({ ref: edge })))\n compare(ref.identity, ids(Data.visibleItems(remote, ref.identity)), served)\n const kept: RemoteModel = Data.store.get(settledModel)\n // A list the answer left stale is fetched again: the server places it.\n if (kept.connections[ref.identity]?.stale === false)\n compare(ref.identity, ids(Data.visibleItems(kept, ref.identity)), served, 'answered')\n }\n\n if (agreed.length === 0 && disagreed.length === 0) return nothingCompared(config.mutation.name)\n // A part agrees only where it agreed both pending and answered.\n const wrong = new Set(disagreed.map(each => each.part))\n return { agreed: [...new Set(agreed)].filter(part => !wrong.has(part)), disagreed, unknown }\n })\n\nexport const Law = { check }\n"],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ import { a as EdgeRef, i as Edge, n as Connection } from "./connection-BRyaJy89.js";
2
+ import { r as EntityStore } from "./store-ah2qjcJW.js";
3
+ import { o as OptimisticState } from "./optimistic-CECsfNGY.js";
4
+ import { i as LivePolicy } from "./query-BuIU93xA.js";
5
+ type LiveCursor = number;
6
+ type LiveEvent = {
7
+ readonly _tag: 'EntityPatched';
8
+ readonly ref: EdgeRef;
9
+ readonly values: Readonly<Record<string, unknown>>;
10
+ readonly changed: ReadonlyArray<string>;
11
+ readonly cursor: LiveCursor;
12
+ } | {
13
+ readonly _tag: 'EntityDeleted';
14
+ readonly ref: EdgeRef;
15
+ readonly cursor: LiveCursor;
16
+ } | {
17
+ readonly _tag: 'ConnectionInsert';
18
+ readonly connection: string;
19
+ readonly position: 'prepend' | 'append';
20
+ readonly edge: Edge;
21
+ readonly cursor: LiveCursor;
22
+ } | {
23
+ readonly _tag: 'ConnectionRemove';
24
+ readonly connection: string;
25
+ readonly edge: Edge;
26
+ readonly cursor: LiveCursor;
27
+ } | {
28
+ readonly _tag: 'ConnectionInvalidate';
29
+ readonly connection: string;
30
+ readonly cursor: LiveCursor;
31
+ };
32
+ interface BoundaryCounts {
33
+ readonly before: number;
34
+ readonly after: number;
35
+ }
36
+ interface LiveState {
37
+ /** Last applied cursor for this stream. */
38
+ readonly cursor: LiveCursor;
39
+ /** Edges recorded outside the loaded boundary, not visible in `items`. */
40
+ readonly boundary: Readonly<Record<string, BoundaryCounts>>;
41
+ }
42
+ declare const emptyLiveState: LiveState;
43
+ type LiveOutcome = 'applied' | 'duplicate' | 'gap';
44
+ /**
45
+ * Ordering per stream: at most the next cursor; ahead is a gap. A stream opens
46
+ * after the cursor it was given and counts on from it, so one opened from the
47
+ * start (cursor `0`) begins at 1. A stream the ledger dropped reads as cursor
48
+ * `0` too, and its next event, past 1, is a gap that restarts it (#289).
49
+ */
50
+ declare const classifyLive: (state: LiveState, cursor: LiveCursor) => LiveOutcome;
51
+ interface EntityApplied {
52
+ readonly state: LiveState;
53
+ readonly store: EntityStore;
54
+ readonly outcome: LiveOutcome;
55
+ }
56
+ declare const applyEntityEvent: (state: LiveState, store: EntityStore, event: Extract<LiveEvent, {
57
+ _tag: 'EntityPatched' | 'EntityDeleted';
58
+ }>, now?: number) => EntityApplied;
59
+ interface ConnectionApplied {
60
+ readonly state: LiveState;
61
+ readonly optimistic: OptimisticState;
62
+ readonly outcome: LiveOutcome;
63
+ /**
64
+ * A connection the event says to refetch. Reported rather than recorded here,
65
+ * because staleness lives on the connection in the Model — which is what the
66
+ * planner and every read consult. It used to be written into a set on this
67
+ * live state that nothing read, so an invalidating event did nothing at all.
68
+ */
69
+ readonly invalidated?: string | undefined;
70
+ }
71
+ declare const applyConnectionEvent: (state: LiveState, optimistic: OptimisticState, event: Extract<LiveEvent, {
72
+ _tag: 'ConnectionInsert' | 'ConnectionRemove' | 'ConnectionInvalidate';
73
+ }>, policy?: LivePolicy) => ConnectionApplied;
74
+ /** `hasPrevious` accounting for edges recorded outside the loaded boundary. */
75
+ declare const liveHasPrevious: (connection: Connection, state: LiveState, connectionId: string) => boolean;
76
+ /** `hasNext` accounting for edges recorded outside the loaded boundary. */
77
+ declare const liveHasNext: (connection: Connection, state: LiveState, connectionId: string) => boolean;
78
+ export { LiveEvent as a, applyConnectionEvent as c, emptyLiveState as d, liveHasNext as f, LiveCursor as i, applyEntityEvent as l, ConnectionApplied as n, LiveOutcome as o, liveHasPrevious as p, EntityApplied as r, LiveState as s, BoundaryCounts as t, classifyLive as u };
79
+ //# sourceMappingURL=live-BpTzlCnY.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-BpTzlCnY.d.ts","names":[],"sources":["../src/live.ts"],"mappings":";;;;KAYY;KAEA;WAEG;WACA,KAAK;WACL,QAAQ,SAAS;WACjB,SAAS;WACT,QAAQ;;WAER;WAAgC,KAAK;WAAkB,QAAQ;;WAE/D;WACA;WACA;WACA,MAAM;WACN,QAAQ;;WAGR;WACA;WACA,MAAM;WACN,QAAQ;;WAGR;WACA;WACA,QAAQ;;UAGN;WACN;WACA;;UAGM;;WAEN,QAAQ;;WAER,UAAU,SAAS,eAAe;;cAGhC,gBAAgB;KAEjB;;;;;;;cAQC,eAAY,OAAW,WAAS,QAAU,eAAa;UAkBnD;WACN,OAAO;WACP,OAAO;WACP,SAAS;;cAGP,mBAAgB,OACpB,WAAS,OACT,aAAW,OACX,QAAQ;EAAa;IAA0C,iBAErE;UAWc;WACN,OAAO;WACP,YAAY;WACZ,SAAS;;;;;;;WAOT;;cAgBE,uBAAoB,OACxB,WAAS,YACJ,iBAAe,OACpB,QACL;EACE;IACH,SACO,eACP;;cA6DU,kBAAe,YACd,YAAU,OACf,WAAS;;cAKL,cAAW,YACV,YAAU,OACf,WAAS"}
package/dist/live.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { a as LiveEvent, c as applyConnectionEvent, d as emptyLiveState, f as liveHasNext, i as LiveCursor, l as applyEntityEvent, n as ConnectionApplied, o as LiveOutcome, p as liveHasPrevious, r as EntityApplied, s as LiveState, t as BoundaryCounts, u as classifyLive } from "./live-BpTzlCnY.js";
2
+ export { BoundaryCounts, ConnectionApplied, EntityApplied, LiveCursor, LiveEvent, LiveOutcome, LiveState, applyConnectionEvent, applyEntityEvent, classifyLive, emptyLiveState, liveHasNext, liveHasPrevious };
package/dist/live.js ADDED
@@ -0,0 +1,126 @@
1
+ import { entityKey, tombstone, writeEntity } from "./store.js";
2
+ import { hasNext, hasPrevious } from "./connection.js";
3
+ import { addOverlay } from "./optimistic.js";
4
+ // src/live.ts
5
+ const emptyLiveState = {
6
+ cursor: 0,
7
+ boundary: {}
8
+ };
9
+ /**
10
+ * Ordering per stream: at most the next cursor; ahead is a gap. A stream opens
11
+ * after the cursor it was given and counts on from it, so one opened from the
12
+ * start (cursor `0`) begins at 1. A stream the ledger dropped reads as cursor
13
+ * `0` too, and its next event, past 1, is a gap that restarts it (#289).
14
+ */
15
+ const classifyLive = (state, cursor) => cursor <= state.cursor ? "duplicate" : cursor === state.cursor + 1 ? "applied" : "gap";
16
+ const advance = (state, cursor) => ({
17
+ ...state,
18
+ cursor
19
+ });
20
+ const recordBoundary = (state, connection, position) => {
21
+ const current = state.boundary[connection] ?? {
22
+ before: 0,
23
+ after: 0
24
+ };
25
+ const next = position === "prepend" ? {
26
+ ...current,
27
+ before: current.before + 1
28
+ } : {
29
+ ...current,
30
+ after: current.after + 1
31
+ };
32
+ return {
33
+ ...state,
34
+ boundary: {
35
+ ...state.boundary,
36
+ [connection]: next
37
+ }
38
+ };
39
+ };
40
+ const applyEntityEvent = (state, store, event, now = 0) => {
41
+ const outcome = classifyLive(state, event.cursor);
42
+ if (outcome !== "applied") return {
43
+ state,
44
+ store,
45
+ outcome
46
+ };
47
+ const key = entityKey(event.ref.entity, event.ref.id);
48
+ const next = event._tag === "EntityPatched" ? writeEntity(store, key, event.values, now) : tombstone(store, key);
49
+ return {
50
+ state: advance(state, event.cursor),
51
+ store: next,
52
+ outcome
53
+ };
54
+ };
55
+ const removeEdgeOverlays = (optimistic, connection, key) => ({
56
+ ...optimistic,
57
+ overlays: optimistic.overlays.map((overlay) => {
58
+ if (overlay.connection !== connection || overlay.position === "remove") return overlay;
59
+ const edges = overlay.edges.filter((edge) => edge.key !== key);
60
+ return edges.length === overlay.edges.length ? overlay : {
61
+ ...overlay,
62
+ edges
63
+ };
64
+ })
65
+ });
66
+ const applyConnectionEvent = (state, optimistic, event, policy = {}) => {
67
+ const outcome = classifyLive(state, event.cursor);
68
+ if (outcome !== "applied") return {
69
+ state,
70
+ optimistic,
71
+ outcome
72
+ };
73
+ switch (event._tag) {
74
+ case "ConnectionInsert": switch ((event.position === "prepend" ? policy.prepend : policy.append) ?? "visible") {
75
+ case "visible": return {
76
+ state: advance(state, event.cursor),
77
+ optimistic: addOverlay(optimistic, {
78
+ id: `live:${event.cursor}`,
79
+ connection: event.connection,
80
+ edges: [event.edge],
81
+ position: event.position
82
+ }),
83
+ outcome
84
+ };
85
+ case "boundary": return {
86
+ state: recordBoundary(advance(state, event.cursor), event.connection, event.position),
87
+ optimistic,
88
+ outcome
89
+ };
90
+ case "invalidate": return {
91
+ state: advance(state, event.cursor),
92
+ optimistic,
93
+ outcome,
94
+ invalidated: event.connection
95
+ };
96
+ case "ignore": return {
97
+ state: advance(state, event.cursor),
98
+ optimistic,
99
+ outcome
100
+ };
101
+ }
102
+ case "ConnectionRemove": return {
103
+ state: advance(state, event.cursor),
104
+ optimistic: addOverlay(removeEdgeOverlays(optimistic, event.connection, event.edge.key), {
105
+ id: `live:${event.cursor}`,
106
+ connection: event.connection,
107
+ edges: [event.edge],
108
+ position: "remove"
109
+ }),
110
+ outcome
111
+ };
112
+ case "ConnectionInvalidate": return {
113
+ state: advance(state, event.cursor),
114
+ invalidated: event.connection,
115
+ optimistic,
116
+ outcome
117
+ };
118
+ }
119
+ };
120
+ /** `hasPrevious` accounting for edges recorded outside the loaded boundary. */
121
+ const liveHasPrevious = (connection, state, connectionId) => hasPrevious(connection) || (state.boundary[connectionId]?.before ?? 0) > 0;
122
+ /** `hasNext` accounting for edges recorded outside the loaded boundary. */
123
+ const liveHasNext = (connection, state, connectionId) => hasNext(connection) || (state.boundary[connectionId]?.after ?? 0) > 0;
124
+ export { applyConnectionEvent, applyEntityEvent, classifyLive, emptyLiveState, liveHasNext, liveHasPrevious };
125
+
126
+ //# sourceMappingURL=live.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live.js","names":[],"sources":["../src/live.ts"],"sourcesContent":["/**\n * Live data. Entity facts update the store; connection facts change ordering and\n * membership. Events carry a monotonic cursor ordered **per stream**: duplicates\n * are ignored, and a gap is surfaced so the caller can resume or invalidate.\n * Subscriptions are selection-aware and driven by active Surfaces.\n */\nimport type { EdgeRef } from './connection.js'\nimport { type Connection, type Edge, hasNext, hasPrevious } from './connection.js'\nimport { addOverlay, type OptimisticState } from './optimistic.js'\nimport type { LiveInsertion, LivePolicy } from './query.js'\nimport { entityKey, tombstone, writeEntity, type EntityStore } from './store.js'\n\nexport type LiveCursor = number\n\nexport type LiveEvent =\n | {\n readonly _tag: 'EntityPatched'\n readonly ref: EdgeRef\n readonly values: Readonly<Record<string, unknown>>\n readonly changed: ReadonlyArray<string>\n readonly cursor: LiveCursor\n }\n | { readonly _tag: 'EntityDeleted'; readonly ref: EdgeRef; readonly cursor: LiveCursor }\n | {\n readonly _tag: 'ConnectionInsert'\n readonly connection: string\n readonly position: 'prepend' | 'append'\n readonly edge: Edge\n readonly cursor: LiveCursor\n }\n | {\n readonly _tag: 'ConnectionRemove'\n readonly connection: string\n readonly edge: Edge\n readonly cursor: LiveCursor\n }\n | {\n readonly _tag: 'ConnectionInvalidate'\n readonly connection: string\n readonly cursor: LiveCursor\n }\n\nexport interface BoundaryCounts {\n readonly before: number\n readonly after: number\n}\n\nexport interface LiveState {\n /** Last applied cursor for this stream. */\n readonly cursor: LiveCursor\n /** Edges recorded outside the loaded boundary, not visible in `items`. */\n readonly boundary: Readonly<Record<string, BoundaryCounts>>\n}\n\nexport const emptyLiveState: LiveState = { cursor: 0, boundary: {} }\n\nexport type LiveOutcome = 'applied' | 'duplicate' | 'gap'\n\n/**\n * Ordering per stream: at most the next cursor; ahead is a gap. A stream opens\n * after the cursor it was given and counts on from it, so one opened from the\n * start (cursor `0`) begins at 1. A stream the ledger dropped reads as cursor\n * `0` too, and its next event, past 1, is a gap that restarts it (#289).\n */\nexport const classifyLive = (state: LiveState, cursor: LiveCursor): LiveOutcome =>\n cursor <= state.cursor ? 'duplicate' : cursor === state.cursor + 1 ? 'applied' : 'gap'\n\nconst advance = (state: LiveState, cursor: LiveCursor): LiveState => ({ ...state, cursor })\n\nconst recordBoundary = (\n state: LiveState,\n connection: string,\n position: 'prepend' | 'append',\n): LiveState => {\n const current = state.boundary[connection] ?? { before: 0, after: 0 }\n const next =\n position === 'prepend'\n ? { ...current, before: current.before + 1 }\n : { ...current, after: current.after + 1 }\n return { ...state, boundary: { ...state.boundary, [connection]: next } }\n}\n\nexport interface EntityApplied {\n readonly state: LiveState\n readonly store: EntityStore\n readonly outcome: LiveOutcome\n}\n\nexport const applyEntityEvent = (\n state: LiveState,\n store: EntityStore,\n event: Extract<LiveEvent, { _tag: 'EntityPatched' | 'EntityDeleted' }>,\n now = 0,\n): EntityApplied => {\n const outcome = classifyLive(state, event.cursor)\n if (outcome !== 'applied') return { state, store, outcome }\n const key = entityKey(event.ref.entity, event.ref.id)\n const next =\n event._tag === 'EntityPatched'\n ? writeEntity(store, key, event.values, now)\n : tombstone(store, key)\n return { state: advance(state, event.cursor), store: next, outcome }\n}\n\nexport interface ConnectionApplied {\n readonly state: LiveState\n readonly optimistic: OptimisticState\n readonly outcome: LiveOutcome\n /**\n * A connection the event says to refetch. Reported rather than recorded here,\n * because staleness lives on the connection in the Model — which is what the\n * planner and every read consult. It used to be written into a set on this\n * live state that nothing read, so an invalidating event did nothing at all.\n */\n readonly invalidated?: string | undefined\n}\n\nconst removeEdgeOverlays = (\n optimistic: OptimisticState,\n connection: string,\n key: string,\n): OptimisticState => ({\n ...optimistic,\n overlays: optimistic.overlays.map(overlay => {\n if (overlay.connection !== connection || overlay.position === 'remove') return overlay\n const edges = overlay.edges.filter(edge => edge.key !== key)\n return edges.length === overlay.edges.length ? overlay : { ...overlay, edges }\n }),\n})\n\nexport const applyConnectionEvent = (\n state: LiveState,\n optimistic: OptimisticState,\n event: Extract<\n LiveEvent,\n { _tag: 'ConnectionInsert' | 'ConnectionRemove' | 'ConnectionInvalidate' }\n >,\n policy: LivePolicy = {},\n): ConnectionApplied => {\n const outcome = classifyLive(state, event.cursor)\n if (outcome !== 'applied') return { state, optimistic, outcome }\n\n switch (event._tag) {\n case 'ConnectionInsert': {\n const selection: LiveInsertion =\n (event.position === 'prepend' ? policy.prepend : policy.append) ?? 'visible'\n switch (selection) {\n case 'visible':\n return {\n state: advance(state, event.cursor),\n optimistic: addOverlay(optimistic, {\n id: `live:${event.cursor}`,\n connection: event.connection,\n edges: [event.edge],\n position: event.position,\n }),\n outcome,\n }\n case 'boundary':\n return {\n state: recordBoundary(advance(state, event.cursor), event.connection, event.position),\n optimistic,\n outcome,\n }\n case 'invalidate':\n return {\n state: advance(state, event.cursor),\n optimistic,\n outcome,\n invalidated: event.connection,\n }\n case 'ignore':\n return { state: advance(state, event.cursor), optimistic, outcome }\n }\n }\n case 'ConnectionRemove':\n // Strip the edge from pending inserts and hide it wherever else it is\n // (a server-known segment included) until a fresh page says otherwise.\n return {\n state: advance(state, event.cursor),\n optimistic: addOverlay(removeEdgeOverlays(optimistic, event.connection, event.edge.key), {\n id: `live:${event.cursor}`,\n connection: event.connection,\n edges: [event.edge],\n position: 'remove',\n }),\n outcome,\n }\n case 'ConnectionInvalidate':\n return {\n state: advance(state, event.cursor),\n invalidated: event.connection,\n optimistic,\n outcome,\n }\n }\n}\n\n/** `hasPrevious` accounting for edges recorded outside the loaded boundary. */\nexport const liveHasPrevious = (\n connection: Connection,\n state: LiveState,\n connectionId: string,\n): boolean => hasPrevious(connection) || (state.boundary[connectionId]?.before ?? 0) > 0\n\n/** `hasNext` accounting for edges recorded outside the loaded boundary. */\nexport const liveHasNext = (\n connection: Connection,\n state: LiveState,\n connectionId: string,\n): boolean => hasNext(connection) || (state.boundary[connectionId]?.after ?? 0) > 0\n"],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ import { n as EntityKey, r as EntityStore } from "./store-ah2qjcJW.js";
2
+ import { o as QueryDescriptor } from "./query-BuIU93xA.js";
3
+ import { ExprContext, MemoryOperators } from "foldkit-entity";
4
+ /**
5
+ * An answer over the rows the client holds — never over the rows that exist.
6
+ *
7
+ * `complete` is the whole reason this is its own type rather than a `Page`. A
8
+ * `Page` carries `hasNext`/`hasPrevious`, which are facts the *server* stated
9
+ * about rows beyond the ones it delivered; a local answer has no such facts and
10
+ * cannot invent them. What it can say is whether the population it judged was
11
+ * all of it, which is decidable: a connection terminal at both ends is wholly
12
+ * held, and nothing was skipped for a missing field.
13
+ *
14
+ * An incomplete answer is not wrong — it is an answer about less than the
15
+ * caller may have meant, and the flag is what lets a view say "3 so far" rather
16
+ * than "3", or a caller decide to ask the server.
17
+ */
18
+ interface Matched<A> {
19
+ readonly items: ReadonlyArray<A>;
20
+ /** Whether every row the answer is about was judged. */
21
+ readonly complete: boolean;
22
+ }
23
+ /**
24
+ * What the rows the client holds had to say about a body.
25
+ *
26
+ * `skipped` is the honest half. A row missing a field the body reads cannot be
27
+ * judged — it is neither a match nor a non-match — and dropping it silently
28
+ * would turn "I could not tell" into "no". Naming it lets a caller decide:
29
+ * fetch the field, ask the server, or say the answer is partial.
30
+ */
31
+ interface Judged {
32
+ /** Keys of the rows that satisfy the body, in the order the body asks for. */
33
+ readonly matched: ReadonlyArray<EntityKey>;
34
+ /**
35
+ * Keys of rows held but not judged either way: missing a field the body
36
+ * reads, or holding a value the interpreter refuses (text with a NUL).
37
+ */
38
+ readonly skipped: ReadonlyArray<EntityKey>;
39
+ }
40
+ interface MatchingOptions {
41
+ /**
42
+ * Judge only these keys. Without it every row of the body's Entity in the
43
+ * store is a candidate, which is what a filter over a loaded list wants; with
44
+ * it, a caller narrows to a connection's own edges.
45
+ */
46
+ readonly among?: ReadonlyArray<EntityKey> | undefined;
47
+ /** How the body's primitive operators are run: `Remote.make`'s `operators`. */
48
+ readonly operators?: MemoryOperators | undefined;
49
+ /**
50
+ * What the body's `Expr.viewer` and `Expr.now` read: `Remote.make`'s
51
+ * `viewer`. Without it, or without a reading of the time, a body reading
52
+ * one cannot be judged here.
53
+ */
54
+ readonly context?: ExprContext | undefined;
55
+ }
56
+ /**
57
+ * The rows of `descriptor`'s Entity that the store holds and the body matches.
58
+ *
59
+ * The input is given **decoded**, as an application writes it, and encoded here
60
+ * through the descriptor's own `Input` codec. That is deliberate and is the
61
+ * whole reason this takes a descriptor rather than a bare body: a store holds
62
+ * wire values, so an input has to be in that space to be compared against them,
63
+ * and handing an interpreter a decoded value is a mistake **no runtime error
64
+ * catches** — the reference interpreter answers "no rows", which reads exactly
65
+ * like a correct answer. Doing it here means a caller cannot get it wrong.
66
+ *
67
+ * Literals in the body are deliberately *not* encoded. A literal written in
68
+ * domain terms is already broken against the server, which binds it to a column
69
+ * the same way; accepting it here would make the client quietly more permissive
70
+ * than the thing it has to agree with.
71
+ */
72
+ declare const matching: <Name extends string, Input>(store: EntityStore, descriptor: QueryDescriptor<Name, Input, unknown>, input: Input, options?: MatchingOptions) => Judged;
73
+ /**
74
+ * Whether one row belongs to a query, as far as the client can tell.
75
+ *
76
+ * Three answers, and the third is the one that makes this usable. `'unknown'`
77
+ * is a row the store never fetched, or holds without a field the body reads —
78
+ * a caller that collapses it into `'no'` has turned "I could not tell" into an
79
+ * answer, which is the mistake this whole module is shaped against.
80
+ *
81
+ * **Membership is decidable where position is not.** Whether a row satisfies
82
+ * `eq`, `isNull` or `contains` needs no collation: equality on text is exact
83
+ * and `contains` folds ASCII, both stated in §6.0.1. *Where* a row sorts needs
84
+ * text collation, which is the backend's and deliberately outside the
85
+ * conformant subset — so this answers the half that is always answerable, and
86
+ * ordering stays the server's until a query can declare its collation.
87
+ */
88
+ type Belongs = 'yes' | 'no' | 'unknown';
89
+ /**
90
+ * Takes the input **encoded**, because its one caller holds it that way: a
91
+ * connection identity carries the canonical encoded input, and making it decode
92
+ * only for this to encode again would be a round trip that can fail.
93
+ *
94
+ * A decoded-input sibling was written first and deleted: nothing called it. The
95
+ * asymmetry with `matching`, which takes a decoded input, is the shape of the
96
+ * two real callers rather than an oversight.
97
+ */
98
+ declare const belongsEncoded: <Name extends string, Input>(store: EntityStore, descriptor: QueryDescriptor<Name, Input, unknown>, encoded: Readonly<Record<string, unknown>>, key: EntityKey, operators?: MemoryOperators | undefined, context?: ExprContext | undefined) => Belongs;
99
+ export { belongsEncoded as a, MatchingOptions as i, Judged as n, matching as o, Matched as r, Belongs as t };
100
+ //# sourceMappingURL=matching-DF5F9Us8.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matching-DF5F9Us8.d.ts","names":[],"sources":["../src/matching.ts"],"mappings":";;;;;;;;;;;;;;;;;UAwDiB,QAAQ;WACd,OAAO,cAAc;;WAErB;;;;;;;;;;UAWM;;WAEN,SAAS,cAAc;;;;;WAKvB,SAAS,cAAc;;UAejB;;;;;;WAMN,QAAQ,cAAc;;WAEtB,YAAY;;;;;;WAMZ,UAAU;;;;;;;;;;;;;;;;;;cAmBR,WAAY,qBAAqB,OAAK,OAC1C,aAAW,YACN,gBAAgB,MAAM,iBAAe,OAC1C,OAAK,UACH,oBACR;;;;;;;;;;;;;;;;KA2JS;;;;;;;;;;cAWC,iBAAkB,qBAAqB,OAAK,OAChD,aAAW,YACN,gBAAgB,MAAM,iBAAe,SACxC,SAAS,0BAAwB,KACrC,WAAS,YACF,6BAA2B,UAC7B,4BACT"}
@@ -0,0 +1,2 @@
1
+ import { a as belongsEncoded, i as MatchingOptions, n as Judged, o as matching, r as Matched, t as Belongs } from "./matching-DF5F9Us8.js";
2
+ export { Belongs, Judged, Matched, MatchingOptions, belongsEncoded, matching };