poe-code 10.0.2 → 10.0.4
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/cli/commands/harness.js +27 -1
- package/dist/cli/commands/harness.js.map +1 -1
- package/dist/index.js +1070 -758
- package/dist/index.js.map +4 -4
- package/dist/metafile.json +1 -1
- package/package.json +1 -1
- package/packages/agent-harness/dist/loader/run.js +4 -1
- package/packages/safejs/dist/chunks/{chunk-RSK47E4C.js → chunk-JLYBD3E6.js} +127 -3
- package/packages/safejs/dist/chunks/{chunk-RSK47E4C.js.map → chunk-JLYBD3E6.js.map} +3 -3
- package/packages/safejs/dist/chunks/{chunk-EUCMIR4E.js → chunk-SA7XBTIL.js} +213 -51
- package/packages/safejs/dist/chunks/chunk-SA7XBTIL.js.map +7 -0
- package/packages/safejs/dist/cli.js +51 -2
- package/packages/safejs/dist/cli.js.map +2 -2
- package/packages/safejs/dist/core.js +1 -1
- package/packages/safejs/dist/index.d.ts +3 -0
- package/packages/safejs/dist/index.js +8 -2
- package/packages/safejs/dist/index.js.map +1 -1
- package/packages/safejs/dist/migrate.d.ts +33 -0
- package/packages/safejs/dist/migrate.js +70 -0
- package/packages/safejs/dist/migration-file.d.ts +55 -0
- package/packages/safejs/dist/migration-file.js +79 -0
- package/packages/safejs/dist/restore.d.ts +2 -0
- package/packages/safejs/dist/restore.js +2 -0
- package/packages/safejs/dist/run.js +32 -3
- package/packages/safejs/dist/snapshot/migration.d.ts +34 -0
- package/packages/safejs/dist/snapshot/migration.js +92 -0
- package/packages/safejs/dist/chunks/chunk-EUCMIR4E.js.map +0 -7
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type SnapshotMigrationFileOptions = {
|
|
2
|
+
snapshotPath: string;
|
|
3
|
+
sourcePath: string;
|
|
4
|
+
targetSourcePath?: string;
|
|
5
|
+
planPath?: string;
|
|
6
|
+
outputPath?: string;
|
|
7
|
+
inspect?: boolean;
|
|
8
|
+
dryRun?: boolean;
|
|
9
|
+
cwd?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function migrateSnapshotFile(options: SnapshotMigrationFileOptions): Promise<{
|
|
12
|
+
inspection: {
|
|
13
|
+
checkpointDigest: string;
|
|
14
|
+
sourceHash: string;
|
|
15
|
+
executionSemantics: string;
|
|
16
|
+
calls: (Omit<import("./index.js").HostCallRecord, "outcome" | "asynchronous"> & {
|
|
17
|
+
asynchronous: boolean;
|
|
18
|
+
outcome?: {
|
|
19
|
+
status: "fulfilled" | "rejected";
|
|
20
|
+
data: import("./snapshot/replay-data.js").ReplayData;
|
|
21
|
+
};
|
|
22
|
+
})[];
|
|
23
|
+
unresolvedCalls: (Omit<import("./index.js").HostCallRecord, "outcome" | "asynchronous"> & {
|
|
24
|
+
asynchronous: boolean;
|
|
25
|
+
outcome?: {
|
|
26
|
+
status: "fulfilled" | "rejected";
|
|
27
|
+
data: import("./snapshot/replay-data.js").ReplayData;
|
|
28
|
+
};
|
|
29
|
+
})[];
|
|
30
|
+
};
|
|
31
|
+
outputPath?: undefined;
|
|
32
|
+
dryRun?: undefined;
|
|
33
|
+
} | {
|
|
34
|
+
inspection: {
|
|
35
|
+
checkpointDigest: string;
|
|
36
|
+
sourceHash: string;
|
|
37
|
+
executionSemantics: string;
|
|
38
|
+
calls: (Omit<import("./index.js").HostCallRecord, "outcome" | "asynchronous"> & {
|
|
39
|
+
asynchronous: boolean;
|
|
40
|
+
outcome?: {
|
|
41
|
+
status: "fulfilled" | "rejected";
|
|
42
|
+
data: import("./snapshot/replay-data.js").ReplayData;
|
|
43
|
+
};
|
|
44
|
+
})[];
|
|
45
|
+
unresolvedCalls: (Omit<import("./index.js").HostCallRecord, "outcome" | "asynchronous"> & {
|
|
46
|
+
asynchronous: boolean;
|
|
47
|
+
outcome?: {
|
|
48
|
+
status: "fulfilled" | "rejected";
|
|
49
|
+
data: import("./snapshot/replay-data.js").ReplayData;
|
|
50
|
+
};
|
|
51
|
+
})[];
|
|
52
|
+
};
|
|
53
|
+
outputPath: string | undefined;
|
|
54
|
+
dryRun: boolean;
|
|
55
|
+
}>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { link, open, readFile, unlink } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { hasOwnErrorCode } from "./error-codes.js";
|
|
5
|
+
import { inspectSnapshotMigration, migrateSnapshot } from "./migrate.js";
|
|
6
|
+
import { serializeSafeJSSnapshot } from "./snapshot/dump-format.js";
|
|
7
|
+
export async function migrateSnapshotFile(options) {
|
|
8
|
+
const cwd = options.cwd ?? process.cwd();
|
|
9
|
+
const resolvePath = (value, label) => {
|
|
10
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
11
|
+
throw new TypeError(`Migration requires ${label}.`);
|
|
12
|
+
return path.resolve(cwd, value);
|
|
13
|
+
};
|
|
14
|
+
const snapshotPath = resolvePath(options.snapshotPath, "a snapshot path");
|
|
15
|
+
const sourcePath = resolvePath(options.sourcePath, "--from <original.ajs>");
|
|
16
|
+
if (options.inspect &&
|
|
17
|
+
[options.targetSourcePath, options.planPath, options.outputPath].some((value) => value !== undefined))
|
|
18
|
+
throw new TypeError("Migration --inspect cannot be combined with --to, --plan, or --output.");
|
|
19
|
+
const outputPath = options.inspect
|
|
20
|
+
? undefined
|
|
21
|
+
: resolvePath(options.outputPath, "--output <new-checkpoint.json>");
|
|
22
|
+
const targetSourcePath = options.inspect
|
|
23
|
+
? undefined
|
|
24
|
+
: resolvePath(options.targetSourcePath, "--to <continuation.ajs>");
|
|
25
|
+
const planPath = options.inspect
|
|
26
|
+
? undefined
|
|
27
|
+
: resolvePath(options.planPath, "--plan <migration.json>");
|
|
28
|
+
for (const filepath of [sourcePath, targetSourcePath]) {
|
|
29
|
+
if (filepath !== undefined && ![".ajs", ".safejs"].includes(path.extname(filepath)))
|
|
30
|
+
throw new TypeError("Migration source paths must name executable .ajs or .safejs files, not harness Markdown.");
|
|
31
|
+
}
|
|
32
|
+
const snapshot = JSON.parse(await readFile(snapshotPath, "utf8"));
|
|
33
|
+
const source = await readFile(sourcePath, "utf8");
|
|
34
|
+
const inspection = inspectSnapshotMigration(snapshot, { source });
|
|
35
|
+
if (options.inspect)
|
|
36
|
+
return { inspection };
|
|
37
|
+
const plan = JSON.parse(await readFile(planPath, "utf8"));
|
|
38
|
+
if (plan === null ||
|
|
39
|
+
typeof plan !== "object" ||
|
|
40
|
+
!Object.hasOwn(plan, "state") ||
|
|
41
|
+
!Object.hasOwn(plan, "reconciliation"))
|
|
42
|
+
throw new TypeError("Migration plan requires explicit state and reconciliation fields.");
|
|
43
|
+
const targetSource = await readFile(targetSourcePath, "utf8");
|
|
44
|
+
const migrated = migrateSnapshot(snapshot, {
|
|
45
|
+
source,
|
|
46
|
+
targetSource,
|
|
47
|
+
state: plan.state,
|
|
48
|
+
reconciliation: plan.reconciliation
|
|
49
|
+
});
|
|
50
|
+
const contents = serializeSafeJSSnapshot(migrated);
|
|
51
|
+
if (!options.dryRun) {
|
|
52
|
+
const temporaryPath = path.join(path.dirname(outputPath), `.safejs-migration-${randomUUID()}.tmp`);
|
|
53
|
+
let created = false;
|
|
54
|
+
try {
|
|
55
|
+
const handle = await open(temporaryPath, "wx", 0o600);
|
|
56
|
+
created = true;
|
|
57
|
+
try {
|
|
58
|
+
await handle.writeFile(contents, "utf8");
|
|
59
|
+
await handle.sync();
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
await handle.close();
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await link(temporaryPath, outputPath);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (hasOwnErrorCode(error, "EEXIST"))
|
|
69
|
+
throw Object.assign(new Error(`Migration output already exists: ${JSON.stringify(outputPath)}. Choose a new output path; existing files are never overwritten.`, { cause: error }), { code: "EEXIST" });
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
if (created)
|
|
75
|
+
await unlink(temporaryPath);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { inspection, outputPath, dryRun: options.dryRun === true };
|
|
79
|
+
}
|
|
@@ -3,6 +3,7 @@ import { replaceErrorStack } from "./error/shape.js";
|
|
|
3
3
|
import { SnapshotValidationError, validateDumpEnvelope } from "./snapshot/validation.js";
|
|
4
4
|
import { EXECUTION_SEMANTICS } from "./snapshot/dump-format.js";
|
|
5
5
|
import { assertSnapshotInactive } from "./interp/running-state.js";
|
|
6
|
+
import { validateSnapshotMigration } from "./snapshot/migration.js";
|
|
6
7
|
export class SnapshotMismatchError extends Error {
|
|
7
8
|
actualHash;
|
|
8
9
|
expectedHash;
|
|
@@ -17,6 +18,7 @@ export class SnapshotMismatchError extends Error {
|
|
|
17
18
|
export function restore(snapshot, options) {
|
|
18
19
|
assertSnapshotInactive(snapshot);
|
|
19
20
|
validateDumpEnvelope(snapshot);
|
|
21
|
+
validateSnapshotMigration(snapshot.migration, snapshot.sourceHash);
|
|
20
22
|
if (snapshot.executionSemantics !== EXECUTION_SEMANTICS &&
|
|
21
23
|
(snapshot.executionSemantics !== undefined ||
|
|
22
24
|
snapshot.promiseReplay !== undefined ||
|
|
@@ -28,7 +28,7 @@ import { DUMP_FORMAT_VERSION, EXECUTION_SEMANTICS } from "./snapshot/dump-format
|
|
|
28
28
|
import { createSnapshotScheduler } from "./snapshot/scheduler.js";
|
|
29
29
|
import { UnsnapshotableValueError } from "./snapshot/serialize.js";
|
|
30
30
|
import { prepareReplayInputs } from "./snapshot/replay-inputs.js";
|
|
31
|
-
import { MissingReplayCapabilityError } from "./snapshot/replay-data.js";
|
|
31
|
+
import { decodeReplayData, MissingReplayCapabilityError } from "./snapshot/replay-data.js";
|
|
32
32
|
export class UnhandledRejectionError extends Error {
|
|
33
33
|
reason;
|
|
34
34
|
constructor(reason, span) {
|
|
@@ -44,6 +44,7 @@ export function run(source, options = {}) {
|
|
|
44
44
|
const lifecycle = { hostCallbackDepth: 0 };
|
|
45
45
|
const dumpController = createDumpController(lifecycle);
|
|
46
46
|
const promiseTracker = createSandboxPromiseRejectionTracker();
|
|
47
|
+
let completedSnapshot;
|
|
47
48
|
const execute = async () => {
|
|
48
49
|
const promiseReplay = new PromiseReplay(options.snapshot?.promiseReplay);
|
|
49
50
|
return promiseReplayContext.run(promiseReplay, async () => {
|
|
@@ -121,6 +122,17 @@ export function run(source, options = {}) {
|
|
|
121
122
|
}),
|
|
122
123
|
...createRegexGlobals()
|
|
123
124
|
};
|
|
125
|
+
const importMeta = deepCopyToSandbox(options.importMeta ?? {});
|
|
126
|
+
if (restoredSnapshot?.migration !== undefined) {
|
|
127
|
+
if (Object.hasOwn(importMeta, "migration"))
|
|
128
|
+
throw new TypeError("import.meta.migration is reserved for migrated checkpoint state.");
|
|
129
|
+
Object.defineProperty(importMeta, "migration", {
|
|
130
|
+
value: decodeReplayData(restoredSnapshot.migration.state),
|
|
131
|
+
enumerable: true,
|
|
132
|
+
configurable: true,
|
|
133
|
+
writable: true
|
|
134
|
+
});
|
|
135
|
+
}
|
|
124
136
|
const initialInputs = prepareReplayInputs({
|
|
125
137
|
bindings: callerBindings,
|
|
126
138
|
imports: resolveModuleImports(module, options.modules, {
|
|
@@ -132,7 +144,7 @@ export function run(source, options = {}) {
|
|
|
132
144
|
entryPointArgs: options.entryPointArgs === undefined
|
|
133
145
|
? undefined
|
|
134
146
|
: deepCopyToSandbox([...options.entryPointArgs]),
|
|
135
|
-
importMeta
|
|
147
|
+
importMeta
|
|
136
148
|
}, restoredSnapshot?.initialInputs, (promise, id) => {
|
|
137
149
|
if (promise !== undefined)
|
|
138
150
|
observeSandboxPromise(promise);
|
|
@@ -172,6 +184,7 @@ export function run(source, options = {}) {
|
|
|
172
184
|
});
|
|
173
185
|
snapshotScheduler = activeSnapshotScheduler;
|
|
174
186
|
createFailureSnapshot = () => createRunSnapshot({
|
|
187
|
+
migration: restoredSnapshot?.migration,
|
|
175
188
|
bindings: executionScope.snapshot().bindings,
|
|
176
189
|
clock: options.clock,
|
|
177
190
|
hostCalls: hostCalls.snapshot(),
|
|
@@ -197,6 +210,7 @@ export function run(source, options = {}) {
|
|
|
197
210
|
}
|
|
198
211
|
const interpreterSnapshot = yieldPoint.snapshot();
|
|
199
212
|
snapshot = createRunSnapshot({
|
|
213
|
+
migration: restoredSnapshot?.migration,
|
|
200
214
|
bindings: interpreterSnapshot.bindings,
|
|
201
215
|
clock: options.clock,
|
|
202
216
|
hostCalls: hostCalls.snapshot(),
|
|
@@ -240,6 +254,7 @@ export function run(source, options = {}) {
|
|
|
240
254
|
}
|
|
241
255
|
const interpreterSnapshot = yieldPoint.snapshot();
|
|
242
256
|
snapshot = createRunSnapshot({
|
|
257
|
+
migration: restoredSnapshot?.migration,
|
|
243
258
|
bindings: interpreterSnapshot.bindings,
|
|
244
259
|
clock: options.clock,
|
|
245
260
|
hostCalls: hostCalls.snapshot(),
|
|
@@ -277,6 +292,7 @@ export function run(source, options = {}) {
|
|
|
277
292
|
replayError = error.message;
|
|
278
293
|
}
|
|
279
294
|
const snapshot = createRunSnapshot({
|
|
295
|
+
migration: restoredSnapshot?.migration,
|
|
280
296
|
bindings: executionScope.snapshot().bindings,
|
|
281
297
|
clock: options.clock,
|
|
282
298
|
hostCalls: hostCalls.snapshot(),
|
|
@@ -289,6 +305,7 @@ export function run(source, options = {}) {
|
|
|
289
305
|
if (replayError !== undefined)
|
|
290
306
|
snapshot.replayError = replayError;
|
|
291
307
|
dumpController.finalize(snapshot);
|
|
308
|
+
completedSnapshot = snapshot;
|
|
292
309
|
return {
|
|
293
310
|
...result,
|
|
294
311
|
snapshot
|
|
@@ -327,7 +344,18 @@ export function run(source, options = {}) {
|
|
|
327
344
|
}
|
|
328
345
|
});
|
|
329
346
|
};
|
|
330
|
-
const result = withRunResources(options.signal, () => withSandboxPromiseRejectionTracker(promiseTracker, execute))
|
|
347
|
+
const result = withRunResources(options.signal, () => withSandboxPromiseRejectionTracker(promiseTracker, execute)).catch(async (error) => {
|
|
348
|
+
if (completedSnapshot !== undefined) {
|
|
349
|
+
try {
|
|
350
|
+
await createSnapshotScheduler(options).write(completedSnapshot);
|
|
351
|
+
}
|
|
352
|
+
catch (snapshotError) {
|
|
353
|
+
console.warn("Failed to write failure snapshot.", snapshotError);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
dumpController.fail(error);
|
|
357
|
+
throw error;
|
|
358
|
+
});
|
|
331
359
|
return attachDumpController(result, dumpController);
|
|
332
360
|
}
|
|
333
361
|
async function throwIfReturnedPromiseRejected(result) {
|
|
@@ -428,6 +456,7 @@ function createRunSnapshot(input) {
|
|
|
428
456
|
version: DUMP_FORMAT_VERSION,
|
|
429
457
|
executionSemantics: EXECUTION_SEMANTICS,
|
|
430
458
|
sourceHash: input.sourceHash,
|
|
459
|
+
...(input.migration === undefined ? {} : { migration: structuredClone(input.migration) }),
|
|
431
460
|
bindings: input.bindings,
|
|
432
461
|
clock: input.clock?.snapshot(),
|
|
433
462
|
...(input.hostCalls === undefined || input.hostCalls.length === 0
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type HostCallReplay } from "../interp/host-call.js";
|
|
2
|
+
import { type ReplayData } from "./replay-data.js";
|
|
3
|
+
export type SnapshotMigrationResolution = {
|
|
4
|
+
callId: string;
|
|
5
|
+
disposition: "not-performed";
|
|
6
|
+
} | {
|
|
7
|
+
callId: string;
|
|
8
|
+
disposition: "fulfilled";
|
|
9
|
+
value: unknown;
|
|
10
|
+
} | {
|
|
11
|
+
callId: string;
|
|
12
|
+
disposition: "rejected";
|
|
13
|
+
reason: unknown;
|
|
14
|
+
};
|
|
15
|
+
export type SnapshotMigrationReconciliation = {
|
|
16
|
+
checkpointDigest: string;
|
|
17
|
+
quiescent: boolean;
|
|
18
|
+
calls: SnapshotMigrationResolution[];
|
|
19
|
+
};
|
|
20
|
+
export type SnapshotMigration = {
|
|
21
|
+
version: 1;
|
|
22
|
+
state: ReplayData;
|
|
23
|
+
history: Array<{
|
|
24
|
+
sourceHash: string;
|
|
25
|
+
targetSourceHash: string;
|
|
26
|
+
executionSemantics: string;
|
|
27
|
+
replay: HostCallReplay;
|
|
28
|
+
reconciliation: SnapshotMigrationReconciliation;
|
|
29
|
+
}>;
|
|
30
|
+
};
|
|
31
|
+
export declare function validateMigrationSemantics(value: unknown): asserts value is string;
|
|
32
|
+
export declare function validateMigrationJournal(sourceHash: string, replay: unknown, hostCalls?: unknown): HostCallReplay;
|
|
33
|
+
export declare function validateMigrationReconciliation(value: unknown, checkpointDigest: string, calls: HostCallReplay["calls"]): asserts value is SnapshotMigrationReconciliation;
|
|
34
|
+
export declare function validateSnapshotMigration(value: unknown, sourceHash: string): SnapshotMigration | undefined;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { HostCallJournal } from "../interp/host-call.js";
|
|
2
|
+
import { decodeReplayData } from "./replay-data.js";
|
|
3
|
+
import { validateSnapshotData } from "./validation.js";
|
|
4
|
+
export function validateMigrationSemantics(value) {
|
|
5
|
+
if (!["jobs-v1", "jobs-v2", "jobs-v3", "jobs-v4", "jobs-v5"].includes(value))
|
|
6
|
+
throw new TypeError("Migration requires a supported execution-semantics marker (jobs-v1 through jobs-v5).");
|
|
7
|
+
}
|
|
8
|
+
export function validateMigrationJournal(sourceHash, replay, hostCalls = []) {
|
|
9
|
+
if (replay === undefined)
|
|
10
|
+
throw new TypeError("Migration requires a complete host replay journal.");
|
|
11
|
+
if (!Array.isArray(hostCalls))
|
|
12
|
+
throw new TypeError("Invalid migration hostCalls.");
|
|
13
|
+
const journal = new HostCallJournal(sourceHash, hostCalls, undefined, replay);
|
|
14
|
+
try {
|
|
15
|
+
const restored = journal.snapshotReplay();
|
|
16
|
+
for (const call of restored.calls) {
|
|
17
|
+
if (["created", "running"].includes(call.lifecycle) && call.outcome !== undefined)
|
|
18
|
+
throw new TypeError("Unresolved migration calls cannot contain a recorded outcome.");
|
|
19
|
+
for (const callback of call.callbacks ?? []) {
|
|
20
|
+
if (!call.functions?.includes(callback.id))
|
|
21
|
+
throw new TypeError("Migration callback has no registered source function.");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return restored;
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
journal.dispose();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function validateMigrationReconciliation(value, checkpointDigest, calls) {
|
|
31
|
+
validateSnapshotData(value);
|
|
32
|
+
const receipt = value;
|
|
33
|
+
if (receipt === null ||
|
|
34
|
+
typeof receipt !== "object" ||
|
|
35
|
+
receipt.checkpointDigest !== checkpointDigest ||
|
|
36
|
+
!isCheckpointDigest(receipt.checkpointDigest))
|
|
37
|
+
throw new TypeError("Migration reconciliation must identify this exact checkpoint digest.");
|
|
38
|
+
if (receipt.quiescent !== true)
|
|
39
|
+
throw new TypeError("Migration requires explicit host confirmation that execution, operations, and callbacks are quiescent.");
|
|
40
|
+
if (!Array.isArray(receipt.calls))
|
|
41
|
+
throw new TypeError("Migration reconciliation calls must be an array.");
|
|
42
|
+
const pending = new Map(calls
|
|
43
|
+
.filter((call) => !["settled", "consumed"].includes(call.lifecycle))
|
|
44
|
+
.map((call) => [call.id, call]));
|
|
45
|
+
for (const resolution of receipt.calls) {
|
|
46
|
+
if (resolution === null || typeof resolution !== "object" || !pending.has(resolution.callId))
|
|
47
|
+
throw new TypeError("Migration contains an unknown or duplicate call resolution.");
|
|
48
|
+
if (!["not-performed", "fulfilled", "rejected"].includes(resolution.disposition) ||
|
|
49
|
+
(resolution.disposition === "fulfilled" && !Object.hasOwn(resolution, "value")) ||
|
|
50
|
+
(resolution.disposition === "rejected" && !Object.hasOwn(resolution, "reason")))
|
|
51
|
+
throw new TypeError("Migration call resolution requires a reconciled outcome or confirmed non-performance.");
|
|
52
|
+
pending.delete(resolution.callId);
|
|
53
|
+
}
|
|
54
|
+
if (pending.size > 0)
|
|
55
|
+
throw new TypeError(`Migration requires external reconciliation for: ${[...pending.keys()].join(", ")}`);
|
|
56
|
+
}
|
|
57
|
+
export function validateSnapshotMigration(value, sourceHash) {
|
|
58
|
+
if (value === undefined)
|
|
59
|
+
return undefined;
|
|
60
|
+
validateSnapshotData(value);
|
|
61
|
+
const migration = value;
|
|
62
|
+
if (migration === null ||
|
|
63
|
+
typeof migration !== "object" ||
|
|
64
|
+
migration.version !== 1 ||
|
|
65
|
+
!Array.isArray(migration.history) ||
|
|
66
|
+
migration.history.length === 0)
|
|
67
|
+
throw new TypeError("Invalid snapshot migration history.");
|
|
68
|
+
decodeReplayData(migration.state);
|
|
69
|
+
let previousTarget;
|
|
70
|
+
for (const entry of migration.history) {
|
|
71
|
+
if (entry === null ||
|
|
72
|
+
typeof entry !== "object" ||
|
|
73
|
+
typeof entry.sourceHash !== "string" ||
|
|
74
|
+
entry.sourceHash.length === 0 ||
|
|
75
|
+
typeof entry.targetSourceHash !== "string" ||
|
|
76
|
+
entry.targetSourceHash.length === 0 ||
|
|
77
|
+
(previousTarget !== undefined && previousTarget !== entry.sourceHash))
|
|
78
|
+
throw new TypeError("Invalid snapshot migration ancestry.");
|
|
79
|
+
validateMigrationSemantics(entry.executionSemantics);
|
|
80
|
+
const replay = validateMigrationJournal(entry.sourceHash, entry.replay);
|
|
81
|
+
validateMigrationReconciliation(entry.reconciliation, entry.reconciliation?.checkpointDigest, replay.calls);
|
|
82
|
+
previousTarget = entry.targetSourceHash;
|
|
83
|
+
}
|
|
84
|
+
if (previousTarget !== sourceHash)
|
|
85
|
+
throw new TypeError("Migration ancestry does not match the target source.");
|
|
86
|
+
return migration;
|
|
87
|
+
}
|
|
88
|
+
function isCheckpointDigest(value) {
|
|
89
|
+
return (typeof value === "string" &&
|
|
90
|
+
value.length === 64 &&
|
|
91
|
+
[...value].every((character) => "0123456789abcdef".includes(character)));
|
|
92
|
+
}
|