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.
- package/README.md +987 -198
- package/dist/answerGaps.d.ts +16 -0
- package/dist/answerGaps.d.ts.map +1 -0
- package/dist/answerGaps.js +60 -0
- package/dist/answerGaps.js.map +1 -0
- package/dist/client-B055kQuP.d.ts +110 -0
- package/dist/client-B055kQuP.d.ts.map +1 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +142 -0
- package/dist/client.js.map +1 -0
- package/dist/coalesce-CLAr92Nk.d.ts +34 -0
- package/dist/coalesce-CLAr92Nk.d.ts.map +1 -0
- package/dist/coalesce.d.ts +2 -0
- package/dist/coalesce.js +131 -0
- package/dist/coalesce.js.map +1 -0
- package/dist/connection-BRyaJy89.d.ts +45 -0
- package/dist/connection-BRyaJy89.d.ts.map +1 -0
- package/dist/connection.d.ts +2 -0
- package/dist/connection.js +170 -0
- package/dist/connection.js.map +1 -0
- package/dist/data.d.ts +7 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +21 -0
- package/dist/data.js.map +1 -0
- package/dist/diagnostics-DHhVYflV.d.ts +125 -0
- package/dist/diagnostics-DHhVYflV.d.ts.map +1 -0
- package/dist/diagnostics.d.ts +2 -0
- package/dist/diagnostics.js +60 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/entity-CgsG5Uel.d.ts +196 -0
- package/dist/entity-CgsG5Uel.d.ts.map +1 -0
- package/dist/entity-DuDxaa3n.js +279 -0
- package/dist/entity-DuDxaa3n.js.map +1 -0
- package/dist/entity.d.ts +2 -0
- package/dist/entity.js +1 -0
- package/dist/held.d.ts +19 -0
- package/dist/held.d.ts.map +1 -0
- package/dist/held.js +70 -0
- package/dist/held.js.map +1 -0
- package/dist/impact.d.ts +79 -0
- package/dist/impact.d.ts.map +1 -0
- package/dist/impact.js +166 -0
- package/dist/impact.js.map +1 -0
- package/dist/index.d.ts +1542 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2791 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect-DtRQGCRP.d.ts +134 -0
- package/dist/inspect-DtRQGCRP.d.ts.map +1 -0
- package/dist/inspect.d.ts +2 -0
- package/dist/inspect.js +33 -0
- package/dist/inspect.js.map +1 -0
- package/dist/json-DowLl1eB.d.ts +52 -0
- package/dist/json-DowLl1eB.d.ts.map +1 -0
- package/dist/json.d.ts +2 -0
- package/dist/json.js +123 -0
- package/dist/json.js.map +1 -0
- package/dist/law.d.ts +56 -0
- package/dist/law.d.ts.map +1 -0
- package/dist/law.js +201 -0
- package/dist/law.js.map +1 -0
- package/dist/live-BpTzlCnY.d.ts +79 -0
- package/dist/live-BpTzlCnY.d.ts.map +1 -0
- package/dist/live.d.ts +2 -0
- package/dist/live.js +126 -0
- package/dist/live.js.map +1 -0
- package/dist/matching-DF5F9Us8.d.ts +100 -0
- package/dist/matching-DF5F9Us8.d.ts.map +1 -0
- package/dist/matching.d.ts +2 -0
- package/dist/matching.js +172 -0
- package/dist/matching.js.map +1 -0
- package/dist/model-B6zk1NMB.d.ts +710 -0
- package/dist/model-B6zk1NMB.d.ts.map +1 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +1308 -0
- package/dist/model.js.map +1 -0
- package/dist/mutation.d.ts +2 -0
- package/dist/mutation.js +612 -0
- package/dist/mutation.js.map +1 -0
- package/dist/optimistic-CECsfNGY.d.ts +523 -0
- package/dist/optimistic-CECsfNGY.d.ts.map +1 -0
- package/dist/optimistic.d.ts +2 -0
- package/dist/optimistic.js +336 -0
- package/dist/optimistic.js.map +1 -0
- package/dist/outbox-DNuc4kZp.d.ts +259 -0
- package/dist/outbox-DNuc4kZp.d.ts.map +1 -0
- package/dist/outbox.d.ts +2 -0
- package/dist/outbox.js +326 -0
- package/dist/outbox.js.map +1 -0
- package/dist/patchValues.d.ts +10 -0
- package/dist/patchValues.d.ts.map +1 -0
- package/dist/patchValues.js +57 -0
- package/dist/patchValues.js.map +1 -0
- package/dist/persistence-CCMnvg7-.d.ts +114 -0
- package/dist/persistence-CCMnvg7-.d.ts.map +1 -0
- package/dist/persistence.d.ts +2 -0
- package/dist/persistence.js +205 -0
- package/dist/persistence.js.map +1 -0
- package/dist/placement-jZdsIpmj.d.ts +50 -0
- package/dist/placement-jZdsIpmj.d.ts.map +1 -0
- package/dist/placement.d.ts +2 -0
- package/dist/placement.js +116 -0
- package/dist/placement.js.map +1 -0
- package/dist/plan-D9cftxC_.d.ts +35 -0
- package/dist/plan-D9cftxC_.d.ts.map +1 -0
- package/dist/plan.d.ts +2 -0
- package/dist/plan.js +100 -0
- package/dist/plan.js.map +1 -0
- package/dist/policy-DsWHmIAq.d.ts +42 -0
- package/dist/policy-DsWHmIAq.d.ts.map +1 -0
- package/dist/policy.d.ts +2 -0
- package/dist/policy.js +32 -0
- package/dist/policy.js.map +1 -0
- package/dist/{port.d.mts → port.d.ts} +3 -2
- package/dist/port.d.ts.map +1 -0
- package/dist/{port.mjs → port.js} +3 -2
- package/dist/port.js.map +1 -0
- package/dist/query-BuIU93xA.d.ts +141 -0
- package/dist/query-BuIU93xA.d.ts.map +1 -0
- package/dist/query.d.ts +2 -0
- package/dist/query.js +0 -0
- package/dist/query.js.map +1 -0
- package/dist/record.d.ts +11 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +21 -0
- package/dist/record.js.map +1 -0
- package/dist/relation-BLBGOZom.d.ts +59 -0
- package/dist/relation-BLBGOZom.d.ts.map +1 -0
- package/dist/relation.d.ts +2 -0
- package/dist/relation.js +93 -0
- package/dist/relation.js.map +1 -0
- package/dist/remoteData-mK1suYi1.d.ts +148 -0
- package/dist/remoteData-mK1suYi1.d.ts.map +1 -0
- package/dist/remoteData.d.ts +2 -0
- package/dist/remoteData.js +179 -0
- package/dist/remoteData.js.map +1 -0
- package/dist/reporting.d.ts +12 -0
- package/dist/reporting.d.ts.map +1 -0
- package/dist/reporting.js +45 -0
- package/dist/reporting.js.map +1 -0
- package/dist/requirement-D57S2LpH.d.ts +64 -0
- package/dist/requirement-D57S2LpH.d.ts.map +1 -0
- package/dist/requirement.d.ts +2 -0
- package/dist/requirement.js +114 -0
- package/dist/requirement.js.map +1 -0
- package/dist/resume-Ct5R6yli.d.ts +92 -0
- package/dist/resume-Ct5R6yli.d.ts.map +1 -0
- package/dist/resume.d.ts +2 -0
- package/dist/resume.js +222 -0
- package/dist/resume.js.map +1 -0
- package/dist/retain-BfNTvBRb.d.ts +48 -0
- package/dist/retain-BfNTvBRb.d.ts.map +1 -0
- package/dist/retain.d.ts +2 -0
- package/dist/retain.js +107 -0
- package/dist/retain.js.map +1 -0
- package/dist/selection.d.ts +2 -0
- package/dist/selection.js +1 -0
- package/dist/shown.d.ts +20 -0
- package/dist/shown.d.ts.map +1 -0
- package/dist/shown.js +40 -0
- package/dist/shown.js.map +1 -0
- package/dist/sse-C7LhdbiQ.d.ts +17 -0
- package/dist/sse-C7LhdbiQ.d.ts.map +1 -0
- package/dist/sse.d.ts +2 -0
- package/dist/sse.js +127 -0
- package/dist/sse.js.map +1 -0
- package/dist/store-ah2qjcJW.d.ts +100 -0
- package/dist/store-ah2qjcJW.d.ts.map +1 -0
- package/dist/store.d.ts +2 -0
- package/dist/store.js +218 -0
- package/dist/store.js.map +1 -0
- package/dist/testing.d.ts +133 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +0 -0
- package/dist/testing.js.map +1 -0
- package/dist/why-kgp_Wr3z.d.ts +483 -0
- package/dist/why-kgp_Wr3z.d.ts.map +1 -0
- package/dist/why.d.ts +2 -0
- package/dist/why.js +293 -0
- package/dist/why.js.map +1 -0
- package/dist/whyReasons.d.ts +42 -0
- package/dist/whyReasons.d.ts.map +1 -0
- package/dist/whyReasons.js +211 -0
- package/dist/whyReasons.js.map +1 -0
- package/dist/wire-DthPcTM6.d.ts +787 -0
- package/dist/wire-DthPcTM6.d.ts.map +1 -0
- package/dist/wire.d.ts +2 -0
- package/dist/wire.js +329 -0
- package/dist/wire.js.map +1 -0
- package/package.json +19 -14
- package/dist/index-BEvVAL76.d.mts +0 -4097
- package/dist/index-BEvVAL76.d.mts.map +0 -1
- package/dist/index.d.mts +0 -2
- package/dist/index.mjs +0 -1
- package/dist/port.d.mts.map +0 -1
- package/dist/port.mjs.map +0 -1
- package/dist/src-BDqn7Ocr.mjs +0 -6344
- 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
|
package/dist/law.js.map
ADDED
|
@@ -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
|
package/dist/live.js.map
ADDED
|
@@ -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"}
|