@gaganref/confect-test 10.3.1-fork.91 → 10.3.1-fork.93
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/dist/TestConfect.d.ts +26 -0
- package/dist/TestConfect.d.ts.map +1 -1
- package/dist/TestConfect.js +9 -1
- package/dist/TestConfect.js.map +1 -1
- package/dist/internal/Attempt.d.ts +14 -1
- package/dist/internal/Attempt.d.ts.map +1 -1
- package/dist/internal/Attempt.js +18 -1
- package/dist/internal/Attempt.js.map +1 -1
- package/dist/internal/ConvexTest.d.ts +26 -1
- package/dist/internal/ConvexTest.d.ts.map +1 -1
- package/dist/internal/ConvexTest.js +55 -27
- package/dist/internal/ConvexTest.js.map +1 -1
- package/dist/internal/Execution.d.ts +151 -1
- package/dist/internal/Execution.d.ts.map +1 -1
- package/dist/internal/Execution.js +731 -214
- package/dist/internal/Execution.js.map +1 -1
- package/dist/internal/KernelQuery.d.ts +8 -3
- package/dist/internal/KernelQuery.d.ts.map +1 -1
- package/dist/internal/KernelQuery.js +10 -3
- package/dist/internal/KernelQuery.js.map +1 -1
- package/dist/internal/KernelRefusal.d.ts +134 -0
- package/dist/internal/KernelRefusal.d.ts.map +1 -1
- package/dist/internal/KernelRefusal.js +134 -1
- package/dist/internal/KernelRefusal.js.map +1 -1
- package/dist/internal/Metering.d.ts +186 -0
- package/dist/internal/Metering.d.ts.map +1 -0
- package/dist/internal/Metering.js +401 -0
- package/dist/internal/Metering.js.map +1 -0
- package/dist/internal/Redaction.d.ts +142 -0
- package/dist/internal/Redaction.d.ts.map +1 -0
- package/dist/internal/Redaction.js +448 -0
- package/dist/internal/Redaction.js.map +1 -0
- package/dist/internal/Snapshot.d.ts +298 -0
- package/dist/internal/Snapshot.d.ts.map +1 -0
- package/dist/internal/Snapshot.js +532 -0
- package/dist/internal/Snapshot.js.map +1 -0
- package/dist/internal/Subscriptions.d.ts +13 -0
- package/dist/internal/Subscriptions.d.ts.map +1 -1
- package/dist/internal/Subscriptions.js +27 -3
- package/dist/internal/Subscriptions.js.map +1 -1
- package/package.json +3 -3
- package/src/TestConfect.ts +63 -4
- package/src/internal/Attempt.ts +24 -0
- package/src/internal/ConvexTest.ts +135 -38
- package/src/internal/Execution.ts +1141 -324
- package/src/internal/KernelQuery.ts +17 -6
- package/src/internal/KernelRefusal.ts +160 -0
- package/src/internal/Metering.ts +520 -0
- package/src/internal/Redaction.ts +730 -0
- package/src/internal/Snapshot.ts +945 -0
- package/src/internal/Subscriptions.ts +34 -4
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import { tableNameFromId } from "./KernelValues.js";
|
|
2
|
+
import { SnapshotAllocatorBehind, SnapshotDigestMismatch, SnapshotJobInconsistent, SnapshotMalformed, SnapshotRowInconsistent, SnapshotSecretMissing, SnapshotTimestampInconsistent } from "./KernelRefusal.js";
|
|
3
|
+
import * as Schema from "effect/Schema";
|
|
4
|
+
import { convexToJson, jsonToConvex } from "convex/values";
|
|
5
|
+
import * as Result from "effect/Result";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
//#region src/internal/Snapshot.ts
|
|
8
|
+
/**
|
|
9
|
+
* The snapshot's contract with the kernel's state. Every field of the committed
|
|
10
|
+
* state, of a job's target and of a `World` is listed here as exported or
|
|
11
|
+
* excluded with its reason. The first two are checked by type: a field added to
|
|
12
|
+
* `Kernel.State` or `Scheduling.Target` without an entry fails to compile. A
|
|
13
|
+
* `World`'s fields are private, which no type can enumerate, so
|
|
14
|
+
* `Snapshot.test.ts` compares a fresh world's own fields with `world` below,
|
|
15
|
+
* and a field added without an entry fails that test.
|
|
16
|
+
*
|
|
17
|
+
* That test sees only the own fields a fresh world has. It cannot see state
|
|
18
|
+
* created lazily after construction, held under a symbol or a `#private` name,
|
|
19
|
+
* or kept in a closure or another object, as subscribers are. So every live
|
|
20
|
+
* registry, state that exists only while something runs or a client is
|
|
21
|
+
* attached, is also listed in `live`. `World.liveCounts` reports each one,
|
|
22
|
+
* typed against `live`, and a restore refuses while any of them is non-empty,
|
|
23
|
+
* so a piece of live state the snapshot does not carry fails loudly rather than
|
|
24
|
+
* surviving a restore silently. A new live registry adds itself there.
|
|
25
|
+
*
|
|
26
|
+
* Owed by later rows, which add their state here when they land on `main`: cron
|
|
27
|
+
* state (#133), stored file bytes, storage ID mappings, `nextFile` and
|
|
28
|
+
* upload-token counters (#138), and component init markers. A restore never
|
|
29
|
+
* redeclares a cron and never draws a fresh random value: it installs the
|
|
30
|
+
* declared state as the snapshot holds it, so a row whose state comes from a
|
|
31
|
+
* declaration or a random draw exports what was declared or drawn.
|
|
32
|
+
*/
|
|
33
|
+
const contract = {
|
|
34
|
+
state: {
|
|
35
|
+
tables: { exported: "tables" },
|
|
36
|
+
version: { exported: "version" },
|
|
37
|
+
commitTs: { exported: "commitTs" },
|
|
38
|
+
log: { exported: "log" }
|
|
39
|
+
},
|
|
40
|
+
target: {
|
|
41
|
+
functionPath: { exported: "jobs[].target" },
|
|
42
|
+
standing: { exported: "jobs[].standing" },
|
|
43
|
+
world: { exported: "jobs[].world, by name" }
|
|
44
|
+
},
|
|
45
|
+
world: {
|
|
46
|
+
components: { excluded: "Registered code, not state: the restoring world registers the same components, which `components` checks." },
|
|
47
|
+
state: { exported: "the committed state above" },
|
|
48
|
+
opened: { excluded: "Whether modules loaded. A restore opens its world first." },
|
|
49
|
+
nextDocId: { exported: "identity.nextDocumentId" },
|
|
50
|
+
lastCreationTime: { exported: "identity.lastCreationTime" },
|
|
51
|
+
nextQueryId: { excluded: "Ids of query streams, which live only inside a running function, and a snapshot is never taken while one runs." },
|
|
52
|
+
queries: { excluded: "Open query streams, which live only inside a running function." },
|
|
53
|
+
holding: { excluded: "Held attempts are running closures. A world holding one refuses to export." },
|
|
54
|
+
nextAttemptId: { exported: "identity.nextAttemptId" },
|
|
55
|
+
floor: { exported: "floor" },
|
|
56
|
+
targets: { exported: "jobs" },
|
|
57
|
+
live: { excluded: "Live registries, listed under `live` below. A world refuses to restore while any is non-empty." },
|
|
58
|
+
identityEpoch: { exported: "identity.identityEpoch" },
|
|
59
|
+
runtime: { excluded: "The driver's clock and commit timestamps. The restoring driver supplies its own." },
|
|
60
|
+
redaction: { excluded: "The secret policy is configuration: the restoring world is built with its own." },
|
|
61
|
+
secrets: { excluded: "Raw secrets are replay state and never reach an artifact. A snapshot writes markers, and a restore takes the raw values from the source's `secrets()`." }
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* Live registries: what blocks a snapshot while it is non-empty, and why.
|
|
65
|
+
*/
|
|
66
|
+
live: {
|
|
67
|
+
running: {
|
|
68
|
+
blocks: "export and restore",
|
|
69
|
+
reason: "Calls from the test in flight, whose effects a snapshot would split."
|
|
70
|
+
},
|
|
71
|
+
walks: {
|
|
72
|
+
blocks: "export and restore",
|
|
73
|
+
reason: "Scheduler walks in flight, which would leave a job running."
|
|
74
|
+
},
|
|
75
|
+
functions: {
|
|
76
|
+
blocks: "export and restore",
|
|
77
|
+
reason: "Function executions, nested and detached ones included, which may still write. The binary lets detached work finish within its function's lifetime, so a snapshot waits for it rather than cancel it."
|
|
78
|
+
},
|
|
79
|
+
held: {
|
|
80
|
+
blocks: "export and restore",
|
|
81
|
+
reason: "Held attempts are running closures a snapshot does not carry."
|
|
82
|
+
},
|
|
83
|
+
subscribers: {
|
|
84
|
+
blocks: "restore",
|
|
85
|
+
reason: "Live client state. A restore would rewind the identity versions an existing subscriber holds, so it needs a world with none."
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
/**
|
|
89
|
+
* State outside the kernel's world, live per client or per process.
|
|
90
|
+
*/
|
|
91
|
+
outside: {
|
|
92
|
+
subscriptions: { excluded: "Live client state: subscribers, their callers and runs. A restore requires a world with no subscribers (`live.subscribers`) and starts with none. Identity versions continue through `identityEpoch`." },
|
|
93
|
+
requestIds: { excluded: "Request IDs, a process-wide counter, reach no committed state." }
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const header = "confect-kernel-snapshot 1 sha256:";
|
|
97
|
+
const FunctionPathJson = Schema.Struct({
|
|
98
|
+
componentPath: Schema.String,
|
|
99
|
+
udfPath: Schema.String
|
|
100
|
+
});
|
|
101
|
+
const Body = Schema.Struct({
|
|
102
|
+
components: Schema.Array(Schema.String),
|
|
103
|
+
version: Schema.Int,
|
|
104
|
+
commitTs: Schema.String,
|
|
105
|
+
floor: Schema.NullOr(Schema.Finite),
|
|
106
|
+
identity: Schema.Struct({
|
|
107
|
+
nextDocumentId: Schema.Int,
|
|
108
|
+
lastCreationTime: Schema.Finite,
|
|
109
|
+
nextAttemptId: Schema.Int,
|
|
110
|
+
identityEpoch: Schema.Int
|
|
111
|
+
}),
|
|
112
|
+
tables: Schema.Array(Schema.Struct({
|
|
113
|
+
componentPath: Schema.String,
|
|
114
|
+
table: Schema.String,
|
|
115
|
+
rows: Schema.Array(Schema.Struct({
|
|
116
|
+
version: Schema.Int,
|
|
117
|
+
doc: Schema.Unknown
|
|
118
|
+
}))
|
|
119
|
+
})),
|
|
120
|
+
jobs: Schema.Array(Schema.Struct({
|
|
121
|
+
id: Schema.String,
|
|
122
|
+
target: FunctionPathJson,
|
|
123
|
+
standing: Schema.Boolean,
|
|
124
|
+
world: Schema.NullOr(Schema.String)
|
|
125
|
+
})),
|
|
126
|
+
log: Schema.Array(Schema.Struct({
|
|
127
|
+
version: Schema.Int,
|
|
128
|
+
commitTs: Schema.String,
|
|
129
|
+
changes: Schema.Array(Schema.Struct({
|
|
130
|
+
componentPath: Schema.String,
|
|
131
|
+
id: Schema.String,
|
|
132
|
+
before: Schema.Unknown,
|
|
133
|
+
after: Schema.Unknown
|
|
134
|
+
}))
|
|
135
|
+
}))
|
|
136
|
+
});
|
|
137
|
+
const byString = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
138
|
+
const sorted = (items, key) => Array.from(items).sort((a, b) => byString(key(a), key(b)));
|
|
139
|
+
const asIs = (_componentPath, _table, json) => json;
|
|
140
|
+
const digestOf = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
141
|
+
/**
|
|
142
|
+
* Encode `image` as snapshot bytes: a header line naming the format and the
|
|
143
|
+
* SHA-256 digest of the body, then the body as JSON. Components, tables, rows
|
|
144
|
+
* and jobs are sorted, and every document keeps its own field order, so the
|
|
145
|
+
* same state always encodes to the same bytes.
|
|
146
|
+
*/
|
|
147
|
+
const encode = (image, redact = asIs) => {
|
|
148
|
+
const { state } = image;
|
|
149
|
+
const encodeDocument = (componentPath, doc) => doc === null ? null : redact(componentPath, tableNameFromId(doc._id) ?? "", convexToJson(doc));
|
|
150
|
+
const tables = [];
|
|
151
|
+
for (const [componentPath, byTable] of sorted(state.tables, ([path]) => path)) for (const [table, rows] of sorted(byTable, ([name]) => name)) {
|
|
152
|
+
if (rows.size === 0) continue;
|
|
153
|
+
tables.push({
|
|
154
|
+
componentPath,
|
|
155
|
+
table,
|
|
156
|
+
rows: sorted(rows, ([id]) => id).map(([, row]) => ({
|
|
157
|
+
version: row.version,
|
|
158
|
+
doc: encodeDocument(componentPath, row.doc)
|
|
159
|
+
}))
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const body = {
|
|
163
|
+
components: sorted(image.components, (path) => path),
|
|
164
|
+
version: state.version,
|
|
165
|
+
commitTs: state.commitTs.toString(),
|
|
166
|
+
floor: Number.isFinite(image.floor) ? image.floor : null,
|
|
167
|
+
identity: {
|
|
168
|
+
nextDocumentId: image.identity.nextDocumentId,
|
|
169
|
+
lastCreationTime: image.identity.lastCreationTime,
|
|
170
|
+
nextAttemptId: image.identity.nextAttemptId,
|
|
171
|
+
identityEpoch: image.identity.identityEpoch
|
|
172
|
+
},
|
|
173
|
+
tables,
|
|
174
|
+
jobs: sorted(image.jobs, ([id]) => id).map(([id, target]) => ({
|
|
175
|
+
id,
|
|
176
|
+
target: {
|
|
177
|
+
componentPath: target.functionPath.componentPath,
|
|
178
|
+
udfPath: target.functionPath.udfPath
|
|
179
|
+
},
|
|
180
|
+
standing: target.standing,
|
|
181
|
+
world: target.world
|
|
182
|
+
})),
|
|
183
|
+
log: state.log.map((commit) => ({
|
|
184
|
+
version: commit.version,
|
|
185
|
+
commitTs: commit.commitTs.toString(),
|
|
186
|
+
changes: commit.changes.map((change) => ({
|
|
187
|
+
componentPath: change.componentPath,
|
|
188
|
+
id: change.id,
|
|
189
|
+
before: encodeDocument(change.componentPath, change.before),
|
|
190
|
+
after: encodeDocument(change.componentPath, change.after)
|
|
191
|
+
}))
|
|
192
|
+
}))
|
|
193
|
+
};
|
|
194
|
+
const bodyBytes = new TextEncoder().encode(JSON.stringify(body));
|
|
195
|
+
const head = new TextEncoder().encode(`${header}${digestOf(bodyBytes)}\n`);
|
|
196
|
+
const bytes = new Uint8Array(head.length + bodyBytes.length);
|
|
197
|
+
bytes.set(head);
|
|
198
|
+
bytes.set(bodyBytes, head.length);
|
|
199
|
+
return bytes;
|
|
200
|
+
};
|
|
201
|
+
const malformed = (reason, detail) => new SnapshotMalformed({
|
|
202
|
+
reason,
|
|
203
|
+
message: `These bytes are not a test kernel snapshot this kernel reads (${reason}): ${detail}`
|
|
204
|
+
});
|
|
205
|
+
const decodeDocument = (json, unredact) => {
|
|
206
|
+
if (json === null) return null;
|
|
207
|
+
const unredacted = unredact(json);
|
|
208
|
+
if (Result.isFailure(unredacted)) throw unredacted.failure;
|
|
209
|
+
const doc = jsonToConvex(unredacted.success);
|
|
210
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc) || typeof doc._id !== "string") throw new Error("a document is not an object with an `_id`");
|
|
211
|
+
return doc;
|
|
212
|
+
};
|
|
213
|
+
const idOf = (doc) => {
|
|
214
|
+
const id = doc?._id;
|
|
215
|
+
return typeof id === "string" ? id : "";
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* Why a document's system fields are not ones the kernel writes, or nothing: an
|
|
219
|
+
* `_id` string and a finite, non-negative `_creationTime`.
|
|
220
|
+
*/
|
|
221
|
+
const systemFieldsProblem = (doc) => {
|
|
222
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return "is not an object";
|
|
223
|
+
const { _id, _creationTime } = doc;
|
|
224
|
+
if (typeof _id !== "string") return "has no string `_id`";
|
|
225
|
+
if (typeof _creationTime !== "number" || !Number.isFinite(_creationTime) || _creationTime < 0) return "has no finite, non-negative `_creationTime`";
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* Check the decoded body before any map is built from it, so its order cannot
|
|
229
|
+
* change the result: components, tables, rows and jobs each appear once, and
|
|
230
|
+
* every document carries the system fields the kernel writes.
|
|
231
|
+
*/
|
|
232
|
+
const checkBody = (body) => {
|
|
233
|
+
const once = (items, key) => {
|
|
234
|
+
const seen = /* @__PURE__ */ new Set();
|
|
235
|
+
for (const item of items) {
|
|
236
|
+
const k = key(item);
|
|
237
|
+
if (seen.has(k)) return item;
|
|
238
|
+
seen.add(k);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
const component = once(body.components, (path) => path);
|
|
242
|
+
if (component !== void 0) return Result.fail(malformed("duplicate", `component "${component}" appears twice`));
|
|
243
|
+
const twice = once(body.tables, (entry) => `${entry.componentPath}\u0000${entry.table}`);
|
|
244
|
+
if (twice !== void 0) return Result.fail(rowRefusal(twice.componentPath, twice.table, "duplicate", `names table "${twice.table}" twice`));
|
|
245
|
+
for (const { componentPath, rows } of body.tables) for (const { doc } of rows) {
|
|
246
|
+
const problem = systemFieldsProblem(doc);
|
|
247
|
+
if (problem !== void 0) return Result.fail(rowRefusal(componentPath, idOf(doc), "systemFields", problem));
|
|
248
|
+
}
|
|
249
|
+
const byComponent = /* @__PURE__ */ new Map();
|
|
250
|
+
for (const { componentPath, rows } of body.tables) byComponent.set(componentPath, [...byComponent.get(componentPath) ?? [], ...rows.map(({ doc }) => doc._id)]);
|
|
251
|
+
for (const [componentPath, ids] of byComponent) {
|
|
252
|
+
const id = once(ids, (i) => i);
|
|
253
|
+
if (id !== void 0) return Result.fail(rowRefusal(componentPath, id, "duplicate", "is stored twice"));
|
|
254
|
+
}
|
|
255
|
+
for (const { changes } of body.log) for (const { componentPath, id, before, after } of changes) for (const doc of [before, after]) {
|
|
256
|
+
if (doc === null) continue;
|
|
257
|
+
const problem = systemFieldsProblem(doc) ?? (doc._id === id ? void 0 : "is logged under another ID");
|
|
258
|
+
if (problem !== void 0) return Result.fail(rowRefusal(componentPath, id, "systemFields", problem));
|
|
259
|
+
}
|
|
260
|
+
const job = once(body.jobs, ({ id }) => id);
|
|
261
|
+
if (job !== void 0) return Result.fail(new SnapshotJobInconsistent({
|
|
262
|
+
id: job.id,
|
|
263
|
+
reason: "duplicate",
|
|
264
|
+
message: `The snapshot's job ${job.id} appears twice.`
|
|
265
|
+
}));
|
|
266
|
+
return Result.succeed(void 0);
|
|
267
|
+
};
|
|
268
|
+
/**
|
|
269
|
+
* `body` with every document's marked secrets put back, each document checked
|
|
270
|
+
* to decode as a Convex value. No map is built, so the body checks that follow
|
|
271
|
+
* see the documents as the kernel stored them, in the order they were written.
|
|
272
|
+
*/
|
|
273
|
+
const unredactBody = (body, unredact) => {
|
|
274
|
+
let refusal;
|
|
275
|
+
const document = (json) => {
|
|
276
|
+
if (json === null || refusal !== void 0) return json;
|
|
277
|
+
const unredacted = unredact(json);
|
|
278
|
+
if (Result.isFailure(unredacted)) {
|
|
279
|
+
refusal = unredacted.failure;
|
|
280
|
+
return json;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
jsonToConvex(unredacted.success);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
refusal = malformed("value", error instanceof Error ? error.message : String(error));
|
|
286
|
+
}
|
|
287
|
+
return unredacted.success;
|
|
288
|
+
};
|
|
289
|
+
const unredacted = {
|
|
290
|
+
...body,
|
|
291
|
+
tables: body.tables.map((entry) => ({
|
|
292
|
+
...entry,
|
|
293
|
+
rows: entry.rows.map((row) => ({
|
|
294
|
+
...row,
|
|
295
|
+
doc: document(row.doc)
|
|
296
|
+
}))
|
|
297
|
+
})),
|
|
298
|
+
log: body.log.map((commit) => ({
|
|
299
|
+
...commit,
|
|
300
|
+
changes: commit.changes.map((change) => ({
|
|
301
|
+
...change,
|
|
302
|
+
before: document(change.before),
|
|
303
|
+
after: document(change.after)
|
|
304
|
+
}))
|
|
305
|
+
}))
|
|
306
|
+
};
|
|
307
|
+
return refusal === void 0 ? Result.succeed(unredacted) : Result.fail(refusal);
|
|
308
|
+
};
|
|
309
|
+
const imageOf = (body, unredact) => {
|
|
310
|
+
const tables = /* @__PURE__ */ new Map();
|
|
311
|
+
for (const { componentPath, table, rows } of body.tables) {
|
|
312
|
+
const byTable = tables.get(componentPath) ?? /* @__PURE__ */ new Map();
|
|
313
|
+
tables.set(componentPath, byTable);
|
|
314
|
+
const byId = /* @__PURE__ */ new Map();
|
|
315
|
+
byTable.set(table, byId);
|
|
316
|
+
for (const { version, doc } of rows) {
|
|
317
|
+
const decoded = decodeDocument(doc, unredact);
|
|
318
|
+
byId.set(decoded._id, {
|
|
319
|
+
doc: decoded,
|
|
320
|
+
version
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const log = body.log.map((commit) => ({
|
|
325
|
+
version: commit.version,
|
|
326
|
+
commitTs: BigInt(commit.commitTs),
|
|
327
|
+
changes: commit.changes.map((change) => ({
|
|
328
|
+
componentPath: change.componentPath,
|
|
329
|
+
id: change.id,
|
|
330
|
+
before: decodeDocument(change.before, unredact),
|
|
331
|
+
after: decodeDocument(change.after, unredact)
|
|
332
|
+
}))
|
|
333
|
+
}));
|
|
334
|
+
if (log.length !== body.version || log.some((c, i) => c.version !== i + 1)) throw new Error(`the write log holds ${log.length} commits for version ${body.version}`);
|
|
335
|
+
return {
|
|
336
|
+
components: body.components,
|
|
337
|
+
state: {
|
|
338
|
+
tables,
|
|
339
|
+
version: body.version,
|
|
340
|
+
commitTs: BigInt(body.commitTs),
|
|
341
|
+
log
|
|
342
|
+
},
|
|
343
|
+
floor: body.floor ?? Number.NEGATIVE_INFINITY,
|
|
344
|
+
identity: body.identity,
|
|
345
|
+
jobs: new Map(body.jobs.map((job) => [job.id, {
|
|
346
|
+
functionPath: job.target,
|
|
347
|
+
standing: job.standing,
|
|
348
|
+
world: job.world
|
|
349
|
+
}]))
|
|
350
|
+
};
|
|
351
|
+
};
|
|
352
|
+
const documentText = (doc) => doc === null ? "null" : JSON.stringify(convexToJson(doc));
|
|
353
|
+
/**
|
|
354
|
+
* The counter a kernel-minted document ID carries, or `undefined` for an ID the
|
|
355
|
+
* kernel never mints.
|
|
356
|
+
*/
|
|
357
|
+
const counterOf = (id) => {
|
|
358
|
+
const table = tableNameFromId(id);
|
|
359
|
+
const digits = table === null ? "" : id.slice(0, id.length - table.length);
|
|
360
|
+
return /^[0-9]+$/.test(digits) ? Number(digits) : void 0;
|
|
361
|
+
};
|
|
362
|
+
const rowRefusal = (componentPath, id, reason, detail) => new SnapshotRowInconsistent({
|
|
363
|
+
componentPath,
|
|
364
|
+
id,
|
|
365
|
+
reason,
|
|
366
|
+
message: `The snapshot's document ${id} in "${componentPath}" ${detail}.`
|
|
367
|
+
});
|
|
368
|
+
/**
|
|
369
|
+
* Rows match the write log: replaying every commit from the empty world, each
|
|
370
|
+
* change replacing the value the log last published, gives exactly the
|
|
371
|
+
* snapshot's rows, each at the version that last wrote it, in a registered
|
|
372
|
+
* component and under its ID's table.
|
|
373
|
+
*/
|
|
374
|
+
const checkRows = (image) => {
|
|
375
|
+
const components = new Set(image.components);
|
|
376
|
+
const replayed = /* @__PURE__ */ new Map();
|
|
377
|
+
const key = (componentPath, id) => `${componentPath}\u0000${id}`;
|
|
378
|
+
for (const commit of image.state.log) for (const change of commit.changes) {
|
|
379
|
+
const current = replayed.get(key(change.componentPath, change.id));
|
|
380
|
+
if (documentText(current?.doc ?? null) !== documentText(change.before)) return Result.fail(rowRefusal(change.componentPath, change.id, "log", `changes at version ${commit.version} from a value the log never published`));
|
|
381
|
+
if (change.after === null) replayed.delete(key(change.componentPath, change.id));
|
|
382
|
+
else replayed.set(key(change.componentPath, change.id), {
|
|
383
|
+
doc: change.after,
|
|
384
|
+
version: commit.version
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
const filed = /* @__PURE__ */ new Set();
|
|
388
|
+
for (const [componentPath, byTable] of image.state.tables) for (const [table, byId] of byTable) for (const [id, row] of byId) {
|
|
389
|
+
filed.add(key(componentPath, id));
|
|
390
|
+
if (counterOf(id) === void 0 || tableNameFromId(id) !== table) return Result.fail(rowRefusal(componentPath, id, "id", `is filed under "${table}"`));
|
|
391
|
+
if (!components.has(componentPath)) return Result.fail(rowRefusal(componentPath, id, "component", "belongs to no component the snapshot registers"));
|
|
392
|
+
const published = replayed.get(key(componentPath, id));
|
|
393
|
+
if (published === void 0 || published.version !== row.version || documentText(published.doc) !== documentText(row.doc)) return Result.fail(rowRefusal(componentPath, id, "row", "is not the value at the version the write log last published for it"));
|
|
394
|
+
}
|
|
395
|
+
for (const published of replayed.keys()) {
|
|
396
|
+
if (filed.has(published)) continue;
|
|
397
|
+
const [componentPath, id] = published.split("\0");
|
|
398
|
+
return Result.fail(rowRefusal(componentPath, id, "row", "is published by the log but missing"));
|
|
399
|
+
}
|
|
400
|
+
return Result.succeed(void 0);
|
|
401
|
+
};
|
|
402
|
+
/**
|
|
403
|
+
* Identity counters run ahead of every identity the state holds: the next
|
|
404
|
+
* document ID past every ID's counter, the latest creation time at or past
|
|
405
|
+
* every document's, a positive next attempt ID and a non-negative epoch.
|
|
406
|
+
*/
|
|
407
|
+
const checkAllocators = (image) => {
|
|
408
|
+
let maxCounter = -1;
|
|
409
|
+
let maxCreation = 0;
|
|
410
|
+
const see = (doc) => {
|
|
411
|
+
if (doc === null) return;
|
|
412
|
+
maxCounter = Math.max(maxCounter, counterOf(doc._id) ?? -1);
|
|
413
|
+
maxCreation = Math.max(maxCreation, doc._creationTime);
|
|
414
|
+
};
|
|
415
|
+
for (const byTable of image.state.tables.values()) for (const byId of byTable.values()) for (const { doc } of byId.values()) see(doc);
|
|
416
|
+
for (const { changes } of image.state.log) for (const { before, after } of changes) {
|
|
417
|
+
see(before);
|
|
418
|
+
see(after);
|
|
419
|
+
}
|
|
420
|
+
for (const id of image.jobs.keys()) maxCounter = Math.max(maxCounter, counterOf(id) ?? -1);
|
|
421
|
+
const { identity } = image;
|
|
422
|
+
const behind = (allocator, value, required) => Result.fail(new SnapshotAllocatorBehind({
|
|
423
|
+
allocator,
|
|
424
|
+
value,
|
|
425
|
+
required,
|
|
426
|
+
message: `The snapshot's ${allocator} is ${value}, but its state needs at least ${required}, or the next allocation would reuse an identity it already holds.`
|
|
427
|
+
}));
|
|
428
|
+
if (identity.nextDocumentId <= maxCounter) return behind("nextDocumentId", identity.nextDocumentId, maxCounter + 1);
|
|
429
|
+
if (identity.lastCreationTime < maxCreation) return behind("lastCreationTime", identity.lastCreationTime, maxCreation);
|
|
430
|
+
if (identity.nextAttemptId < 1) return behind("nextAttemptId", identity.nextAttemptId, 1);
|
|
431
|
+
if (identity.identityEpoch < 0) return behind("identityEpoch", identity.identityEpoch, 0);
|
|
432
|
+
return Result.succeed(void 0);
|
|
433
|
+
};
|
|
434
|
+
/**
|
|
435
|
+
* Commit timestamps strictly increase through the write log, and the latest
|
|
436
|
+
* commit timestamp is the log's last, or zero before any commit.
|
|
437
|
+
*/
|
|
438
|
+
const checkTimestamps = (image) => {
|
|
439
|
+
let previous = 0n;
|
|
440
|
+
const refuse = (version, commitTs, detail) => Result.fail(new SnapshotTimestampInconsistent({
|
|
441
|
+
version,
|
|
442
|
+
commitTs: commitTs.toString(),
|
|
443
|
+
message: `The snapshot's commit timestamp ${commitTs} at version ${version} ${detail}.`
|
|
444
|
+
}));
|
|
445
|
+
for (const { version, commitTs } of image.state.log) {
|
|
446
|
+
if (commitTs <= previous) return refuse(version, commitTs, `is not past the previous ${previous}`);
|
|
447
|
+
previous = commitTs;
|
|
448
|
+
}
|
|
449
|
+
if (image.state.commitTs !== previous) return refuse(image.state.version, image.state.commitTs, `is not the write log's last, ${previous}`);
|
|
450
|
+
return Result.succeed(void 0);
|
|
451
|
+
};
|
|
452
|
+
/**
|
|
453
|
+
* Every job names a committed job row and a registered component, every job row
|
|
454
|
+
* has its job, and no job is left running: a snapshot is never taken while a
|
|
455
|
+
* job runs.
|
|
456
|
+
*/
|
|
457
|
+
const checkJobs = (image, table) => {
|
|
458
|
+
const refuse = (id, reason, detail) => Result.fail(new SnapshotJobInconsistent({
|
|
459
|
+
id,
|
|
460
|
+
reason,
|
|
461
|
+
message: `The snapshot's job ${id} ${detail}.`
|
|
462
|
+
}));
|
|
463
|
+
const rows = /* @__PURE__ */ new Map();
|
|
464
|
+
for (const byTable of image.state.tables.values()) for (const { doc } of byTable.get(table)?.values() ?? []) rows.set(doc._id, doc);
|
|
465
|
+
const components = new Set(image.components);
|
|
466
|
+
for (const [id, target] of image.jobs) {
|
|
467
|
+
if (!rows.has(id)) return refuse(id, "row", "has no committed job row");
|
|
468
|
+
if (!components.has(target.functionPath.componentPath)) return refuse(id, "component", `targets "${target.functionPath.componentPath}", which the snapshot does not register`);
|
|
469
|
+
}
|
|
470
|
+
for (const [id, doc] of rows) {
|
|
471
|
+
if (!image.jobs.has(id)) return refuse(id, "untargeted", "has a row but no target");
|
|
472
|
+
if (doc["state"]?.kind === "inProgress") return refuse(id, "inProgress", "is recorded as running");
|
|
473
|
+
}
|
|
474
|
+
return Result.succeed(void 0);
|
|
475
|
+
};
|
|
476
|
+
/**
|
|
477
|
+
* Check a decoded image against every rule the kernel's own exports keep,
|
|
478
|
+
* before any of it is installed.
|
|
479
|
+
*/
|
|
480
|
+
const check = (image, jobTable) => {
|
|
481
|
+
const checks = [
|
|
482
|
+
() => checkRows(image),
|
|
483
|
+
() => checkAllocators(image),
|
|
484
|
+
() => checkTimestamps(image),
|
|
485
|
+
() => checkJobs(image, jobTable)
|
|
486
|
+
];
|
|
487
|
+
for (const run of checks) {
|
|
488
|
+
const checked = run();
|
|
489
|
+
if (Result.isFailure(checked)) return checked;
|
|
490
|
+
}
|
|
491
|
+
return Result.succeed(void 0);
|
|
492
|
+
};
|
|
493
|
+
/**
|
|
494
|
+
* Read snapshot bytes back into the state they encode. Bytes whose body no
|
|
495
|
+
* longer hashes to their header's digest refuse with `SnapshotDigestMismatch`
|
|
496
|
+
* before any of the body is read.
|
|
497
|
+
*/
|
|
498
|
+
const decode = (bytes, unredact = Result.succeed) => {
|
|
499
|
+
const newline = bytes.indexOf(10);
|
|
500
|
+
const head = newline === -1 ? "" : new TextDecoder("utf-8", { fatal: false }).decode(bytes.subarray(0, newline));
|
|
501
|
+
const expected = head.startsWith(header) ? head.slice(33) : "";
|
|
502
|
+
if (!/^[0-9a-f]{64}$/.test(expected)) return Result.fail(malformed("header", `the first line is not \`${header}<sha256>\``));
|
|
503
|
+
const bodyBytes = bytes.subarray(newline + 1);
|
|
504
|
+
const actual = digestOf(bodyBytes);
|
|
505
|
+
if (actual !== expected) return Result.fail(new SnapshotDigestMismatch({
|
|
506
|
+
expected,
|
|
507
|
+
actual,
|
|
508
|
+
message: `The snapshot's body hashes to sha256:${actual}, not the sha256:${expected} its header names. Its bytes changed after export.`
|
|
509
|
+
}));
|
|
510
|
+
let json;
|
|
511
|
+
try {
|
|
512
|
+
json = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bodyBytes));
|
|
513
|
+
} catch (error) {
|
|
514
|
+
return Result.fail(malformed("json", String(error)));
|
|
515
|
+
}
|
|
516
|
+
const body = Schema.decodeUnknownResult(Body)(json);
|
|
517
|
+
if (Result.isFailure(body)) return Result.fail(malformed("shape", body.failure.message));
|
|
518
|
+
const unredacted = unredactBody(body.success, unredact);
|
|
519
|
+
if (Result.isFailure(unredacted)) return Result.fail(unredacted.failure);
|
|
520
|
+
const checkedBody = checkBody(unredacted.success);
|
|
521
|
+
if (Result.isFailure(checkedBody)) return Result.fail(checkedBody.failure);
|
|
522
|
+
try {
|
|
523
|
+
return Result.succeed(imageOf(unredacted.success, Result.succeed));
|
|
524
|
+
} catch (error) {
|
|
525
|
+
if (Schema.is(SnapshotSecretMissing)(error)) return Result.fail(error);
|
|
526
|
+
return Result.fail(malformed("value", error instanceof Error ? error.message : String(error)));
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
//#endregion
|
|
530
|
+
export { check, checkAllocators, checkBody, checkJobs, checkRows, checkTimestamps, contract, decode, encode };
|
|
531
|
+
|
|
532
|
+
//# sourceMappingURL=Snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Snapshot.js","names":["KernelRefusal.SnapshotMalformed","KernelRefusal.SnapshotJobInconsistent","KernelRefusal.SnapshotRowInconsistent","KernelRefusal.SnapshotAllocatorBehind","KernelRefusal.SnapshotTimestampInconsistent","KernelRefusal.SnapshotDigestMismatch","KernelRefusal.SnapshotSecretMissing"],"sources":["../../src/internal/Snapshot.ts"],"sourcesContent":["// oxlint-disable-next-line effecttsgo/node-builtin-import -- Export is synchronous, and Effect 4.0's digests are asynchronous.\nimport { createHash } from \"node:crypto\";\nimport {\n convexToJson,\n jsonToConvex,\n type JSONValue,\n type Value,\n} from \"convex/values\";\nimport * as Result from \"effect/Result\";\nimport * as Schema from \"effect/Schema\";\nimport type * as Kernel from \"./Kernel.js\";\nimport * as KernelRefusal from \"./KernelRefusal.js\";\nimport type * as Scheduling from \"./Scheduling.js\";\nimport {\n tableNameFromId,\n type DocumentId,\n type FunctionPath,\n type StoredDocument,\n} from \"./KernelValues.js\";\n\n/**\n * The counters the kernel allocates identities from: the next document ID, the\n * latest creation time handed out, the next held attempt's ID, and the identity\n * epoch, which every subscriber's `authenticate` advances. A restored world\n * continues each where its source stopped, so the same calls mint the same\n * identities and the same identity versions.\n */\nexport interface Identity {\n readonly nextDocumentId: number;\n readonly lastCreationTime: number;\n readonly nextAttemptId: number;\n readonly identityEpoch: number;\n}\n\n/**\n * What the kernel keeps beside a job's row, with the job's test world named\n * rather than held. `null` is a job scheduled with no invocation bound.\n */\nexport interface JobTarget {\n readonly functionPath: FunctionPath;\n readonly standing: boolean;\n readonly world: string | null;\n}\n\n/**\n * The kernel state a snapshot carries: committed rows of every component with\n * the version that wrote each, the write log, the latest commit timestamp,\n * every job's target, the clock's floor and the identity counters.\n */\nexport interface Image {\n readonly components: ReadonlyArray<string>;\n readonly state: Kernel.State;\n /**\n * The latest deadline the clock was advanced to. `-Infinity` before any.\n */\n readonly floor: number;\n readonly identity: Identity;\n /**\n * The target of every job whose row is committed, by job ID.\n */\n readonly jobs: ReadonlyMap<DocumentId, JobTarget>;\n}\n\n/**\n * Whether a piece of kernel state is in a snapshot. `exported` names the\n * snapshot field that carries it. `excluded` says why a restore starts without\n * it.\n */\nexport type Participation =\n | { readonly exported: string }\n | { readonly excluded: string };\n\n/**\n * The snapshot's contract with the kernel's state. Every field of the committed\n * state, of a job's target and of a `World` is listed here as exported or\n * excluded with its reason. The first two are checked by type: a field added to\n * `Kernel.State` or `Scheduling.Target` without an entry fails to compile. A\n * `World`'s fields are private, which no type can enumerate, so\n * `Snapshot.test.ts` compares a fresh world's own fields with `world` below,\n * and a field added without an entry fails that test.\n *\n * That test sees only the own fields a fresh world has. It cannot see state\n * created lazily after construction, held under a symbol or a `#private` name,\n * or kept in a closure or another object, as subscribers are. So every live\n * registry, state that exists only while something runs or a client is\n * attached, is also listed in `live`. `World.liveCounts` reports each one,\n * typed against `live`, and a restore refuses while any of them is non-empty,\n * so a piece of live state the snapshot does not carry fails loudly rather than\n * surviving a restore silently. A new live registry adds itself there.\n *\n * Owed by later rows, which add their state here when they land on `main`: cron\n * state (#133), stored file bytes, storage ID mappings, `nextFile` and\n * upload-token counters (#138), and component init markers. A restore never\n * redeclares a cron and never draws a fresh random value: it installs the\n * declared state as the snapshot holds it, so a row whose state comes from a\n * declaration or a random draw exports what was declared or drawn.\n */\nexport const contract = {\n state: {\n tables: { exported: \"tables\" },\n version: { exported: \"version\" },\n commitTs: { exported: \"commitTs\" },\n log: { exported: \"log\" },\n } satisfies { readonly [K in keyof Kernel.State]-?: Participation },\n target: {\n functionPath: { exported: \"jobs[].target\" },\n standing: { exported: \"jobs[].standing\" },\n world: { exported: \"jobs[].world, by name\" },\n } satisfies { readonly [K in keyof Scheduling.Target]-?: Participation },\n world: {\n components: {\n excluded:\n \"Registered code, not state: the restoring world registers the same components, which `components` checks.\",\n },\n state: { exported: \"the committed state above\" },\n opened: {\n excluded: \"Whether modules loaded. A restore opens its world first.\",\n },\n nextDocId: { exported: \"identity.nextDocumentId\" },\n lastCreationTime: { exported: \"identity.lastCreationTime\" },\n nextQueryId: {\n excluded:\n \"Ids of query streams, which live only inside a running function, and a snapshot is never taken while one runs.\",\n },\n queries: {\n excluded:\n \"Open query streams, which live only inside a running function.\",\n },\n holding: {\n excluded:\n \"Held attempts are running closures. A world holding one refuses to export.\",\n },\n nextAttemptId: { exported: \"identity.nextAttemptId\" },\n floor: { exported: \"floor\" },\n targets: { exported: \"jobs\" },\n live: {\n excluded:\n \"Live registries, listed under `live` below. A world refuses to restore while any is non-empty.\",\n },\n identityEpoch: { exported: \"identity.identityEpoch\" },\n runtime: {\n excluded:\n \"The driver's clock and commit timestamps. The restoring driver supplies its own.\",\n },\n redaction: {\n excluded:\n \"The secret policy is configuration: the restoring world is built with its own.\",\n },\n secrets: {\n excluded:\n \"Raw secrets are replay state and never reach an artifact. A snapshot writes markers, and a restore takes the raw values from the source's `secrets()`.\",\n },\n } satisfies Readonly<Record<string, Participation>>,\n /**\n * Live registries: what blocks a snapshot while it is non-empty, and why.\n */\n live: {\n running: {\n blocks: \"export and restore\",\n reason:\n \"Calls from the test in flight, whose effects a snapshot would split.\",\n },\n walks: {\n blocks: \"export and restore\",\n reason: \"Scheduler walks in flight, which would leave a job running.\",\n },\n functions: {\n blocks: \"export and restore\",\n reason:\n \"Function executions, nested and detached ones included, which may still write. The binary lets detached work finish within its function's lifetime, so a snapshot waits for it rather than cancel it.\",\n },\n held: {\n blocks: \"export and restore\",\n reason: \"Held attempts are running closures a snapshot does not carry.\",\n },\n subscribers: {\n blocks: \"restore\",\n reason:\n \"Live client state. A restore would rewind the identity versions an existing subscriber holds, so it needs a world with none.\",\n },\n } satisfies Readonly<\n Record<\n string,\n {\n readonly blocks: \"export and restore\" | \"restore\";\n readonly reason: string;\n }\n >\n >,\n /**\n * State outside the kernel's world, live per client or per process.\n */\n outside: {\n subscriptions: {\n excluded:\n \"Live client state: subscribers, their callers and runs. A restore requires a world with no subscribers (`live.subscribers`) and starts with none. Identity versions continue through `identityEpoch`.\",\n },\n requestIds: {\n excluded:\n \"Request IDs, a process-wide counter, reach no committed state.\",\n },\n } satisfies Readonly<Record<string, Participation>>,\n} as const;\n\nconst header = \"confect-kernel-snapshot 1 sha256:\";\n\nconst FunctionPathJson = Schema.Struct({\n componentPath: Schema.String,\n udfPath: Schema.String,\n});\n\nconst Body = Schema.Struct({\n components: Schema.Array(Schema.String),\n version: Schema.Int,\n commitTs: Schema.String,\n floor: Schema.NullOr(Schema.Finite),\n identity: Schema.Struct({\n nextDocumentId: Schema.Int,\n lastCreationTime: Schema.Finite,\n nextAttemptId: Schema.Int,\n identityEpoch: Schema.Int,\n }),\n tables: Schema.Array(\n Schema.Struct({\n componentPath: Schema.String,\n table: Schema.String,\n rows: Schema.Array(\n Schema.Struct({ version: Schema.Int, doc: Schema.Unknown }),\n ),\n }),\n ),\n jobs: Schema.Array(\n Schema.Struct({\n id: Schema.String,\n target: FunctionPathJson,\n standing: Schema.Boolean,\n world: Schema.NullOr(Schema.String),\n }),\n ),\n log: Schema.Array(\n Schema.Struct({\n version: Schema.Int,\n commitTs: Schema.String,\n changes: Schema.Array(\n Schema.Struct({\n componentPath: Schema.String,\n id: Schema.String,\n before: Schema.Unknown,\n after: Schema.Unknown,\n }),\n ),\n }),\n ),\n});\n\ntype Body = typeof Body.Type;\n\nconst byString = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);\n\nconst sorted = <A>(items: Iterable<A>, key: (item: A) => string): Array<A> =>\n Array.from(items).sort((a, b) => byString(key(a), key(b)));\n\n/**\n * What an export writes for one document, before it reaches the bytes: the\n * kernel marks secrets here. `table` is the document's table.\n */\nexport type Redact = (\n componentPath: string,\n table: string,\n json: JSONValue,\n) => JSONValue;\n\n/**\n * What a restore reads back for one document before decoding it: the kernel\n * puts marked secrets back here.\n */\nexport type Unredact = (\n json: JSONValue,\n) => Result.Result<JSONValue, KernelRefusal.SnapshotSecretMissing>;\n\nconst asIs: Redact = (_componentPath, _table, json) => json;\n\nconst digestOf = (bytes: Uint8Array): string =>\n createHash(\"sha256\").update(bytes).digest(\"hex\");\n\n/**\n * Encode `image` as snapshot bytes: a header line naming the format and the\n * SHA-256 digest of the body, then the body as JSON. Components, tables, rows\n * and jobs are sorted, and every document keeps its own field order, so the\n * same state always encodes to the same bytes.\n */\nexport const encode = (image: Image, redact: Redact = asIs): Uint8Array => {\n const { state } = image;\n const encodeDocument = (\n componentPath: string,\n doc: StoredDocument | null,\n ): JSONValue | null =>\n doc === null\n ? null\n : redact(\n componentPath,\n tableNameFromId(doc._id) ?? \"\",\n convexToJson(doc as Value),\n );\n const tables: Array<Body[\"tables\"][number]> = [];\n for (const [componentPath, byTable] of sorted(\n state.tables,\n ([path]) => path,\n )) {\n for (const [table, rows] of sorted(byTable, ([name]) => name)) {\n if (rows.size === 0) continue;\n tables.push({\n componentPath,\n table,\n rows: sorted(rows, ([id]) => id).map(([, row]) => ({\n version: row.version,\n doc: encodeDocument(componentPath, row.doc),\n })),\n });\n }\n }\n const body: Body = {\n components: sorted(image.components, (path) => path),\n version: state.version,\n commitTs: state.commitTs.toString(),\n floor: Number.isFinite(image.floor) ? image.floor : null,\n identity: {\n nextDocumentId: image.identity.nextDocumentId,\n lastCreationTime: image.identity.lastCreationTime,\n nextAttemptId: image.identity.nextAttemptId,\n identityEpoch: image.identity.identityEpoch,\n },\n tables,\n jobs: sorted(image.jobs, ([id]) => id).map(([id, target]) => ({\n id,\n target: {\n componentPath: target.functionPath.componentPath,\n udfPath: target.functionPath.udfPath,\n },\n standing: target.standing,\n world: target.world,\n })),\n log: state.log.map((commit) => ({\n version: commit.version,\n commitTs: commit.commitTs.toString(),\n changes: commit.changes.map((change) => ({\n componentPath: change.componentPath,\n id: change.id,\n before: encodeDocument(change.componentPath, change.before),\n after: encodeDocument(change.componentPath, change.after),\n })),\n })),\n };\n const bodyBytes = new TextEncoder().encode(JSON.stringify(body));\n const head = new TextEncoder().encode(`${header}${digestOf(bodyBytes)}\\n`);\n const bytes = new Uint8Array(head.length + bodyBytes.length);\n bytes.set(head);\n bytes.set(bodyBytes, head.length);\n return bytes;\n};\n\nconst malformed = (reason: string, detail: string) =>\n new KernelRefusal.SnapshotMalformed({\n reason,\n message: `These bytes are not a test kernel snapshot this kernel reads (${reason}): ${detail}`,\n });\n\nconst decodeDocument = (\n json: unknown,\n unredact: Unredact,\n): StoredDocument | null => {\n if (json === null) return null;\n const unredacted = unredact(json as JSONValue);\n if (Result.isFailure(unredacted)) throw unredacted.failure;\n const doc = jsonToConvex(unredacted.success);\n if (\n typeof doc !== \"object\" ||\n doc === null ||\n Array.isArray(doc) ||\n typeof (doc as { _id?: unknown })._id !== \"string\"\n ) {\n throw new Error(\"a document is not an object with an `_id`\");\n }\n return doc as StoredDocument;\n};\n\nconst idOf = (doc: unknown): string => {\n const id = (doc as { readonly _id?: unknown } | null)?._id;\n return typeof id === \"string\" ? id : \"\";\n};\n\n/**\n * Why a document's system fields are not ones the kernel writes, or nothing: an\n * `_id` string and a finite, non-negative `_creationTime`.\n */\nconst systemFieldsProblem = (doc: unknown): string | undefined => {\n if (typeof doc !== \"object\" || doc === null || Array.isArray(doc))\n return \"is not an object\";\n const { _id, _creationTime } = doc as {\n readonly _id?: unknown;\n readonly _creationTime?: unknown;\n };\n if (typeof _id !== \"string\") return \"has no string `_id`\";\n if (\n typeof _creationTime !== \"number\" ||\n !Number.isFinite(_creationTime) ||\n _creationTime < 0\n )\n return \"has no finite, non-negative `_creationTime`\";\n return undefined;\n};\n\n/**\n * Check the decoded body before any map is built from it, so its order cannot\n * change the result: components, tables, rows and jobs each appear once, and\n * every document carries the system fields the kernel writes.\n */\nexport const checkBody = (\n body: Body,\n): Result.Result<\n void,\n | KernelRefusal.SnapshotMalformed\n | KernelRefusal.SnapshotRowInconsistent\n | KernelRefusal.SnapshotJobInconsistent\n> => {\n const once = <A>(items: ReadonlyArray<A>, key: (item: A) => string) => {\n const seen = new Set<string>();\n for (const item of items) {\n const k = key(item);\n if (seen.has(k)) return item;\n seen.add(k);\n }\n return undefined;\n };\n const component = once(body.components, (path) => path);\n if (component !== undefined)\n return Result.fail(\n malformed(\"duplicate\", `component \"${component}\" appears twice`),\n );\n const twice = once(\n body.tables,\n (entry) => `${entry.componentPath}\\u0000${entry.table}`,\n );\n if (twice !== undefined)\n return Result.fail(\n rowRefusal(\n twice.componentPath,\n twice.table,\n \"duplicate\",\n `names table \"${twice.table}\" twice`,\n ),\n );\n for (const { componentPath, rows } of body.tables) {\n for (const { doc } of rows) {\n const problem = systemFieldsProblem(doc);\n if (problem !== undefined)\n return Result.fail(\n rowRefusal(componentPath, idOf(doc), \"systemFields\", problem),\n );\n }\n }\n const byComponent = new Map<string, Array<string>>();\n for (const { componentPath, rows } of body.tables)\n byComponent.set(componentPath, [\n ...(byComponent.get(componentPath) ?? []),\n ...rows.map(({ doc }) => (doc as { _id: string })._id),\n ]);\n for (const [componentPath, ids] of byComponent) {\n const id = once(ids, (i) => i);\n if (id !== undefined)\n return Result.fail(\n rowRefusal(componentPath, id, \"duplicate\", \"is stored twice\"),\n );\n }\n for (const { changes } of body.log) {\n for (const { componentPath, id, before, after } of changes) {\n for (const doc of [before, after]) {\n if (doc === null) continue;\n const problem =\n systemFieldsProblem(doc) ??\n ((doc as { _id: unknown })._id === id\n ? undefined\n : \"is logged under another ID\");\n if (problem !== undefined)\n return Result.fail(\n rowRefusal(componentPath, id, \"systemFields\", problem),\n );\n }\n }\n }\n const job = once(body.jobs, ({ id }) => id);\n if (job !== undefined)\n return Result.fail(\n new KernelRefusal.SnapshotJobInconsistent({\n id: job.id,\n reason: \"duplicate\",\n message: `The snapshot's job ${job.id} appears twice.`,\n }),\n );\n return Result.succeed(undefined);\n};\n\n/**\n * `body` with every document's marked secrets put back, each document checked\n * to decode as a Convex value. No map is built, so the body checks that follow\n * see the documents as the kernel stored them, in the order they were written.\n */\nconst unredactBody = (\n body: Body,\n unredact: Unredact,\n): Result.Result<\n Body,\n KernelRefusal.SnapshotSecretMissing | KernelRefusal.SnapshotMalformed\n> => {\n let refusal:\n | KernelRefusal.SnapshotSecretMissing\n | KernelRefusal.SnapshotMalformed\n | undefined;\n const document = (json: unknown): unknown => {\n if (json === null || refusal !== undefined) return json;\n const unredacted = unredact(json as JSONValue);\n if (Result.isFailure(unredacted)) {\n refusal = unredacted.failure;\n return json;\n }\n try {\n jsonToConvex(unredacted.success);\n } catch (error) {\n refusal = malformed(\n \"value\",\n error instanceof Error ? error.message : String(error),\n );\n }\n return unredacted.success;\n };\n const unredacted: Body = {\n ...body,\n tables: body.tables.map((entry) => ({\n ...entry,\n rows: entry.rows.map((row) => ({ ...row, doc: document(row.doc) })),\n })),\n log: body.log.map((commit) => ({\n ...commit,\n changes: commit.changes.map((change) => ({\n ...change,\n before: document(change.before),\n after: document(change.after),\n })),\n })),\n };\n return refusal === undefined\n ? Result.succeed(unredacted)\n : Result.fail(refusal);\n};\n\nconst imageOf = (body: Body, unredact: Unredact): Image => {\n const tables = new Map<string, Map<string, Map<DocumentId, Kernel.Row>>>();\n for (const { componentPath, table, rows } of body.tables) {\n const byTable = tables.get(componentPath) ?? new Map();\n tables.set(componentPath, byTable);\n const byId = new Map<DocumentId, Kernel.Row>();\n byTable.set(table, byId);\n for (const { version, doc } of rows) {\n const decoded = decodeDocument(doc, unredact)!;\n byId.set(decoded._id, { doc: decoded, version });\n }\n }\n const log = body.log.map((commit): Kernel.Commit => ({\n version: commit.version,\n commitTs: BigInt(commit.commitTs),\n changes: commit.changes.map((change) => ({\n componentPath: change.componentPath,\n id: change.id as DocumentId,\n before: decodeDocument(change.before, unredact),\n after: decodeDocument(change.after, unredact),\n })),\n }));\n if (log.length !== body.version || log.some((c, i) => c.version !== i + 1)) {\n throw new Error(\n `the write log holds ${log.length} commits for version ${body.version}`,\n );\n }\n return {\n components: body.components,\n state: {\n tables,\n version: body.version,\n commitTs: BigInt(body.commitTs),\n log,\n },\n floor: body.floor ?? Number.NEGATIVE_INFINITY,\n identity: body.identity,\n jobs: new Map(\n body.jobs.map((job) => [\n job.id as DocumentId,\n {\n functionPath: job.target,\n standing: job.standing,\n world: job.world,\n },\n ]),\n ),\n };\n};\n\n/**\n * Why a decoded snapshot is not state the kernel could have reached.\n */\nexport type Inconsistency =\n | KernelRefusal.SnapshotAllocatorBehind\n | KernelRefusal.SnapshotRowInconsistent\n | KernelRefusal.SnapshotTimestampInconsistent\n | KernelRefusal.SnapshotJobInconsistent;\n\nconst documentText = (doc: StoredDocument | null): string =>\n doc === null ? \"null\" : JSON.stringify(convexToJson(doc as Value));\n\n/**\n * The counter a kernel-minted document ID carries, or `undefined` for an ID the\n * kernel never mints.\n */\nconst counterOf = (id: string): number | undefined => {\n const table = tableNameFromId(id);\n const digits = table === null ? \"\" : id.slice(0, id.length - table.length);\n return /^[0-9]+$/.test(digits) ? Number(digits) : undefined;\n};\n\nconst rowRefusal = (\n componentPath: string,\n id: string,\n reason: KernelRefusal.SnapshotRowInconsistent[\"reason\"],\n detail: string,\n) =>\n new KernelRefusal.SnapshotRowInconsistent({\n componentPath,\n id,\n reason,\n message: `The snapshot's document ${id} in \"${componentPath}\" ${detail}.`,\n });\n\n/**\n * Rows match the write log: replaying every commit from the empty world, each\n * change replacing the value the log last published, gives exactly the\n * snapshot's rows, each at the version that last wrote it, in a registered\n * component and under its ID's table.\n */\nexport const checkRows = (\n image: Image,\n): Result.Result<void, KernelRefusal.SnapshotRowInconsistent> => {\n const components = new Set(image.components);\n const replayed = new Map<string, { doc: StoredDocument; version: number }>();\n const key = (componentPath: string, id: string) =>\n `${componentPath}\\u0000${id}`;\n for (const commit of image.state.log) {\n for (const change of commit.changes) {\n const current = replayed.get(key(change.componentPath, change.id));\n if (documentText(current?.doc ?? null) !== documentText(change.before)) {\n return Result.fail(\n rowRefusal(\n change.componentPath,\n change.id,\n \"log\",\n `changes at version ${commit.version} from a value the log never published`,\n ),\n );\n }\n if (change.after === null)\n replayed.delete(key(change.componentPath, change.id));\n else\n replayed.set(key(change.componentPath, change.id), {\n doc: change.after,\n version: commit.version,\n });\n }\n }\n const filed = new Set<string>();\n for (const [componentPath, byTable] of image.state.tables) {\n for (const [table, byId] of byTable) {\n for (const [id, row] of byId) {\n filed.add(key(componentPath, id));\n if (counterOf(id) === undefined || tableNameFromId(id) !== table) {\n return Result.fail(\n rowRefusal(componentPath, id, \"id\", `is filed under \"${table}\"`),\n );\n }\n if (!components.has(componentPath)) {\n return Result.fail(\n rowRefusal(\n componentPath,\n id,\n \"component\",\n \"belongs to no component the snapshot registers\",\n ),\n );\n }\n const published = replayed.get(key(componentPath, id));\n if (\n published === undefined ||\n published.version !== row.version ||\n documentText(published.doc) !== documentText(row.doc)\n ) {\n return Result.fail(\n rowRefusal(\n componentPath,\n id,\n \"row\",\n \"is not the value at the version the write log last published for it\",\n ),\n );\n }\n }\n }\n }\n for (const published of replayed.keys()) {\n if (filed.has(published)) continue;\n const [componentPath, id] = published.split(\"\\u0000\") as [string, string];\n return Result.fail(\n rowRefusal(\n componentPath,\n id,\n \"row\",\n \"is published by the log but missing\",\n ),\n );\n }\n return Result.succeed(undefined);\n};\n\n/**\n * Identity counters run ahead of every identity the state holds: the next\n * document ID past every ID's counter, the latest creation time at or past\n * every document's, a positive next attempt ID and a non-negative epoch.\n */\nexport const checkAllocators = (\n image: Image,\n): Result.Result<void, KernelRefusal.SnapshotAllocatorBehind> => {\n let maxCounter = -1;\n let maxCreation = 0;\n const see = (doc: StoredDocument | null) => {\n if (doc === null) return;\n maxCounter = Math.max(maxCounter, counterOf(doc._id) ?? -1);\n maxCreation = Math.max(maxCreation, doc._creationTime);\n };\n for (const byTable of image.state.tables.values())\n for (const byId of byTable.values())\n for (const { doc } of byId.values()) see(doc);\n for (const { changes } of image.state.log)\n for (const { before, after } of changes) {\n see(before);\n see(after);\n }\n for (const id of image.jobs.keys())\n maxCounter = Math.max(maxCounter, counterOf(id) ?? -1);\n const { identity } = image;\n const behind = (\n allocator: KernelRefusal.SnapshotAllocatorBehind[\"allocator\"],\n value: number,\n required: number,\n ) =>\n Result.fail(\n new KernelRefusal.SnapshotAllocatorBehind({\n allocator,\n value,\n required,\n message: `The snapshot's ${allocator} is ${value}, but its state needs at least ${required}, or the next allocation would reuse an identity it already holds.`,\n }),\n );\n if (identity.nextDocumentId <= maxCounter)\n return behind(\"nextDocumentId\", identity.nextDocumentId, maxCounter + 1);\n if (identity.lastCreationTime < maxCreation)\n return behind(\"lastCreationTime\", identity.lastCreationTime, maxCreation);\n if (identity.nextAttemptId < 1)\n return behind(\"nextAttemptId\", identity.nextAttemptId, 1);\n if (identity.identityEpoch < 0)\n return behind(\"identityEpoch\", identity.identityEpoch, 0);\n return Result.succeed(undefined);\n};\n\n/**\n * Commit timestamps strictly increase through the write log, and the latest\n * commit timestamp is the log's last, or zero before any commit.\n */\nexport const checkTimestamps = (\n image: Image,\n): Result.Result<void, KernelRefusal.SnapshotTimestampInconsistent> => {\n let previous = 0n;\n const refuse = (version: number, commitTs: bigint, detail: string) =>\n Result.fail(\n new KernelRefusal.SnapshotTimestampInconsistent({\n version,\n commitTs: commitTs.toString(),\n message: `The snapshot's commit timestamp ${commitTs} at version ${version} ${detail}.`,\n }),\n );\n for (const { version, commitTs } of image.state.log) {\n if (commitTs <= previous)\n return refuse(version, commitTs, `is not past the previous ${previous}`);\n previous = commitTs;\n }\n if (image.state.commitTs !== previous)\n return refuse(\n image.state.version,\n image.state.commitTs,\n `is not the write log's last, ${previous}`,\n );\n return Result.succeed(undefined);\n};\n\n/**\n * Every job names a committed job row and a registered component, every job row\n * has its job, and no job is left running: a snapshot is never taken while a\n * job runs.\n */\nexport const checkJobs = (\n image: Image,\n table: string,\n): Result.Result<void, KernelRefusal.SnapshotJobInconsistent> => {\n const refuse = (\n id: string,\n reason: KernelRefusal.SnapshotJobInconsistent[\"reason\"],\n detail: string,\n ) =>\n Result.fail(\n new KernelRefusal.SnapshotJobInconsistent({\n id,\n reason,\n message: `The snapshot's job ${id} ${detail}.`,\n }),\n );\n const rows = new Map<string, StoredDocument>();\n for (const byTable of image.state.tables.values())\n for (const { doc } of byTable.get(table)?.values() ?? [])\n rows.set(doc._id, doc);\n const components = new Set(image.components);\n for (const [id, target] of image.jobs) {\n if (!rows.has(id)) return refuse(id, \"row\", \"has no committed job row\");\n if (!components.has(target.functionPath.componentPath))\n return refuse(\n id,\n \"component\",\n `targets \"${target.functionPath.componentPath}\", which the snapshot does not register`,\n );\n }\n for (const [id, doc] of rows) {\n if (!image.jobs.has(id as DocumentId))\n return refuse(id, \"untargeted\", \"has a row but no target\");\n if ((doc[\"state\"] as { kind?: unknown } | undefined)?.kind === \"inProgress\")\n return refuse(id, \"inProgress\", \"is recorded as running\");\n }\n return Result.succeed(undefined);\n};\n\n/**\n * Check a decoded image against every rule the kernel's own exports keep,\n * before any of it is installed.\n */\nexport const check = (\n image: Image,\n jobTable: string,\n): Result.Result<void, Inconsistency> => {\n const checks: ReadonlyArray<() => Result.Result<void, Inconsistency>> = [\n () => checkRows(image),\n () => checkAllocators(image),\n () => checkTimestamps(image),\n () => checkJobs(image, jobTable),\n ];\n for (const run of checks) {\n const checked = run();\n if (Result.isFailure(checked)) return checked;\n }\n return Result.succeed(undefined);\n};\n\n/**\n * Read snapshot bytes back into the state they encode. Bytes whose body no\n * longer hashes to their header's digest refuse with `SnapshotDigestMismatch`\n * before any of the body is read.\n */\nexport const decode = (\n bytes: Uint8Array,\n unredact: Unredact = Result.succeed,\n): Result.Result<\n Image,\n | KernelRefusal.SnapshotMalformed\n | KernelRefusal.SnapshotDigestMismatch\n | KernelRefusal.SnapshotSecretMissing\n | KernelRefusal.SnapshotRowInconsistent\n | KernelRefusal.SnapshotJobInconsistent\n> => {\n const newline = bytes.indexOf(0x0a);\n const head =\n newline === -1\n ? \"\"\n : new TextDecoder(\"utf-8\", { fatal: false }).decode(\n bytes.subarray(0, newline),\n );\n const expected = head.startsWith(header) ? head.slice(header.length) : \"\";\n if (!/^[0-9a-f]{64}$/.test(expected)) {\n return Result.fail(\n malformed(\"header\", `the first line is not \\`${header}<sha256>\\``),\n );\n }\n const bodyBytes = bytes.subarray(newline + 1);\n const actual = digestOf(bodyBytes);\n if (actual !== expected) {\n return Result.fail(\n new KernelRefusal.SnapshotDigestMismatch({\n expected,\n actual,\n message: `The snapshot's body hashes to sha256:${actual}, not the sha256:${expected} its header names. Its bytes changed after export.`,\n }),\n );\n }\n let json: unknown;\n try {\n json = JSON.parse(\n new TextDecoder(\"utf-8\", { fatal: true }).decode(bodyBytes),\n );\n } catch (error) {\n return Result.fail(malformed(\"json\", String(error)));\n }\n const body = Schema.decodeUnknownResult(Body)(json);\n if (Result.isFailure(body)) {\n return Result.fail(malformed(\"shape\", body.failure.message));\n }\n // Secrets go back first, so a document's system fields and IDs are checked as\n // the kernel stored them, and every check runs before any map is built.\n const unredacted = unredactBody(body.success, unredact);\n if (Result.isFailure(unredacted)) return Result.fail(unredacted.failure);\n const checkedBody = checkBody(unredacted.success);\n if (Result.isFailure(checkedBody)) return Result.fail(checkedBody.failure);\n try {\n return Result.succeed(imageOf(unredacted.success, Result.succeed));\n } catch (error) {\n if (Schema.is(KernelRefusal.SnapshotSecretMissing)(error)) {\n return Result.fail(error);\n }\n return Result.fail(\n malformed(\n \"value\",\n error instanceof Error ? error.message : String(error),\n ),\n );\n }\n};\n"],"mappings":"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"}
|
|
@@ -88,6 +88,19 @@ export declare class Registry {
|
|
|
88
88
|
private nextId;
|
|
89
89
|
constructor(world: Execution.World);
|
|
90
90
|
subscriber(caller: Execution.Caller): Subscriber;
|
|
91
|
+
/**
|
|
92
|
+
* The reference a live subscription runs, for marking its diffs by the shape
|
|
93
|
+
* of what its query returns.
|
|
94
|
+
*/
|
|
95
|
+
referenceOf(subscription: string): unknown;
|
|
96
|
+
/**
|
|
97
|
+
* The world's identity epoch, where a new subscriber's version starts.
|
|
98
|
+
*/
|
|
99
|
+
identityEpoch(): number;
|
|
100
|
+
/**
|
|
101
|
+
* Advance the world's identity epoch for a subscriber whose identity changed.
|
|
102
|
+
*/
|
|
103
|
+
nextIdentityEpoch(): number;
|
|
91
104
|
subscribe(subscriber: Subscriber, reference: unknown, args: unknown): Promise<Subscription>;
|
|
92
105
|
/**
|
|
93
106
|
* Rerun each stale subscription at the latest commit and the kernel's time
|