@ny4n/durable 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +711 -2
- package/dist/documents.d.ts +66 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +98 -0
- package/dist/documents.js.map +1 -0
- package/dist/entries.d.ts +31 -0
- package/dist/entries.d.ts.map +1 -0
- package/dist/entries.js +31 -0
- package/dist/entries.js.map +1 -0
- package/dist/env/decode.d.ts +16 -0
- package/dist/env/decode.d.ts.map +1 -0
- package/dist/env/decode.js +28 -0
- package/dist/env/decode.js.map +1 -0
- package/dist/env/index.d.ts +263 -0
- package/dist/env/index.d.ts.map +1 -0
- package/dist/env/index.js +49 -0
- package/dist/env/index.js.map +1 -0
- package/dist/env/line-scan.d.ts +13 -0
- package/dist/env/line-scan.d.ts.map +1 -0
- package/dist/env/line-scan.js +147 -0
- package/dist/env/line-scan.js.map +1 -0
- package/dist/env/node-watch.d.ts +29 -0
- package/dist/env/node-watch.d.ts.map +1 -0
- package/dist/env/node-watch.js +485 -0
- package/dist/env/node-watch.js.map +1 -0
- package/dist/env/node.d.ts +56 -0
- package/dist/env/node.d.ts.map +1 -0
- package/dist/env/node.js +1166 -0
- package/dist/env/node.js.map +1 -0
- package/dist/errors.d.ts +32 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +53 -0
- package/dist/errors.js.map +1 -0
- package/dist/harness/agent.d.ts +33 -0
- package/dist/harness/agent.d.ts.map +1 -0
- package/dist/harness/agent.js +235 -0
- package/dist/harness/agent.js.map +1 -0
- package/dist/harness/assistant.d.ts +15 -0
- package/dist/harness/assistant.d.ts.map +1 -0
- package/dist/harness/assistant.js +23 -0
- package/dist/harness/assistant.js.map +1 -0
- package/dist/harness/compaction.d.ts +56 -0
- package/dist/harness/compaction.d.ts.map +1 -0
- package/dist/harness/compaction.js +384 -0
- package/dist/harness/compaction.js.map +1 -0
- package/dist/harness/context.d.ts +63 -0
- package/dist/harness/context.d.ts.map +1 -0
- package/dist/harness/context.js +221 -0
- package/dist/harness/context.js.map +1 -0
- package/dist/harness/define.d.ts +18 -0
- package/dist/harness/define.d.ts.map +1 -0
- package/dist/harness/define.js +25 -0
- package/dist/harness/define.js.map +1 -0
- package/dist/harness/events.d.ts +180 -0
- package/dist/harness/events.d.ts.map +1 -0
- package/dist/harness/events.js +370 -0
- package/dist/harness/events.js.map +1 -0
- package/dist/harness/generation.d.ts +54 -0
- package/dist/harness/generation.d.ts.map +1 -0
- package/dist/harness/generation.js +514 -0
- package/dist/harness/generation.js.map +1 -0
- package/dist/harness/harness.d.ts +17 -0
- package/dist/harness/harness.d.ts.map +1 -0
- package/dist/harness/harness.js +332 -0
- package/dist/harness/harness.js.map +1 -0
- package/dist/harness/inbox.d.ts +59 -0
- package/dist/harness/inbox.d.ts.map +1 -0
- package/dist/harness/inbox.js +97 -0
- package/dist/harness/inbox.js.map +1 -0
- package/dist/harness/json.d.ts +10 -0
- package/dist/harness/json.d.ts.map +1 -0
- package/dist/harness/json.js +33 -0
- package/dist/harness/json.js.map +1 -0
- package/dist/harness/live.d.ts +142 -0
- package/dist/harness/live.d.ts.map +1 -0
- package/dist/harness/live.js +149 -0
- package/dist/harness/live.js.map +1 -0
- package/dist/harness/output.d.ts +69 -0
- package/dist/harness/output.d.ts.map +1 -0
- package/dist/harness/output.js +318 -0
- package/dist/harness/output.js.map +1 -0
- package/dist/harness/prompt.d.ts +27 -0
- package/dist/harness/prompt.d.ts.map +1 -0
- package/dist/harness/prompt.js +131 -0
- package/dist/harness/prompt.js.map +1 -0
- package/dist/harness/provider.d.ts +14 -0
- package/dist/harness/provider.d.ts.map +1 -0
- package/dist/harness/provider.js +30 -0
- package/dist/harness/provider.js.map +1 -0
- package/dist/harness/registry.d.ts +6 -0
- package/dist/harness/registry.d.ts.map +1 -0
- package/dist/harness/registry.js +101 -0
- package/dist/harness/registry.js.map +1 -0
- package/dist/harness/run.d.ts +17 -0
- package/dist/harness/run.d.ts.map +1 -0
- package/dist/harness/run.js +26 -0
- package/dist/harness/run.js.map +1 -0
- package/dist/harness/scheduler.d.ts +106 -0
- package/dist/harness/scheduler.d.ts.map +1 -0
- package/dist/harness/scheduler.js +1743 -0
- package/dist/harness/scheduler.js.map +1 -0
- package/dist/harness/submissions.d.ts +29 -0
- package/dist/harness/submissions.d.ts.map +1 -0
- package/dist/harness/submissions.js +175 -0
- package/dist/harness/submissions.js.map +1 -0
- package/dist/harness/task-graph.d.ts +49 -0
- package/dist/harness/task-graph.d.ts.map +1 -0
- package/dist/harness/task-graph.js +168 -0
- package/dist/harness/task-graph.js.map +1 -0
- package/dist/harness/tool.d.ts +76 -0
- package/dist/harness/tool.d.ts.map +1 -0
- package/dist/harness/tool.js +736 -0
- package/dist/harness/tool.js.map +1 -0
- package/dist/harness/types.d.ts +713 -0
- package/dist/harness/types.d.ts.map +1 -0
- package/dist/harness/types.js +2 -0
- package/dist/harness/types.js.map +1 -0
- package/dist/harness/usage.d.ts +18 -0
- package/dist/harness/usage.d.ts.map +1 -0
- package/dist/harness/usage.js +57 -0
- package/dist/harness/usage.js.map +1 -0
- package/dist/harness/util.d.ts +19 -0
- package/dist/harness/util.d.ts.map +1 -0
- package/dist/harness/util.js +58 -0
- package/dist/harness/util.js.map +1 -0
- package/dist/harness/view.d.ts +47 -0
- package/dist/harness/view.d.ts.map +1 -0
- package/dist/harness/view.js +206 -0
- package/dist/harness/view.js.map +1 -0
- package/dist/ids.d.ts +6 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +9 -0
- package/dist/ids.js.map +1 -0
- package/dist/images/cloudflare.d.ts +4 -0
- package/dist/images/cloudflare.d.ts.map +1 -0
- package/dist/images/cloudflare.js +9 -0
- package/dist/images/cloudflare.js.map +1 -0
- package/dist/images/exif.d.ts +3 -0
- package/dist/images/exif.d.ts.map +1 -0
- package/dist/images/exif.js +71 -0
- package/dist/images/exif.js.map +1 -0
- package/dist/images/index.d.ts +18 -0
- package/dist/images/index.d.ts.map +1 -0
- package/dist/images/index.js +125 -0
- package/dist/images/index.js.map +1 -0
- package/dist/images/node-worker.d.ts +17 -0
- package/dist/images/node-worker.d.ts.map +1 -0
- package/dist/images/node-worker.js +16 -0
- package/dist/images/node-worker.js.map +1 -0
- package/dist/images/node.d.ts +21 -0
- package/dist/images/node.d.ts.map +1 -0
- package/dist/images/node.js +121 -0
- package/dist/images/node.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/session/forks.d.ts +10 -0
- package/dist/session/forks.d.ts.map +1 -0
- package/dist/session/forks.js +42 -0
- package/dist/session/forks.js.map +1 -0
- package/dist/session/observation.d.ts +42 -0
- package/dist/session/observation.d.ts.map +1 -0
- package/dist/session/observation.js +281 -0
- package/dist/session/observation.js.map +1 -0
- package/dist/session/session.d.ts +133 -0
- package/dist/session/session.d.ts.map +1 -0
- package/dist/session/session.js +811 -0
- package/dist/session/session.js.map +1 -0
- package/dist/session/transaction.d.ts +119 -0
- package/dist/session/transaction.d.ts.map +1 -0
- package/dist/session/transaction.js +795 -0
- package/dist/session/transaction.js.map +1 -0
- package/dist/storage/jsonl/index.d.ts +2 -0
- package/dist/storage/jsonl/index.d.ts.map +1 -0
- package/dist/storage/jsonl/index.js +2 -0
- package/dist/storage/jsonl/index.js.map +1 -0
- package/dist/storage/jsonl/node.d.ts +6 -0
- package/dist/storage/jsonl/node.d.ts.map +1 -0
- package/dist/storage/jsonl/node.js +8 -0
- package/dist/storage/jsonl/node.js.map +1 -0
- package/dist/storage/jsonl/storage.d.ts +143 -0
- package/dist/storage/jsonl/storage.d.ts.map +1 -0
- package/dist/storage/jsonl/storage.js +682 -0
- package/dist/storage/jsonl/storage.js.map +1 -0
- package/dist/storage/memory.d.ts +62 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +733 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/scan.d.ts +18 -0
- package/dist/storage/scan.d.ts.map +1 -0
- package/dist/storage/scan.js +29 -0
- package/dist/storage/scan.js.map +1 -0
- package/dist/storage/sqlite/cloudflare.d.ts +46 -0
- package/dist/storage/sqlite/cloudflare.d.ts.map +1 -0
- package/dist/storage/sqlite/cloudflare.js +106 -0
- package/dist/storage/sqlite/cloudflare.js.map +1 -0
- package/dist/storage/sqlite/database.d.ts +37 -0
- package/dist/storage/sqlite/database.d.ts.map +1 -0
- package/dist/storage/sqlite/database.js +2 -0
- package/dist/storage/sqlite/database.js.map +1 -0
- package/dist/storage/sqlite/index.d.ts +4 -0
- package/dist/storage/sqlite/index.d.ts.map +1 -0
- package/dist/storage/sqlite/index.js +3 -0
- package/dist/storage/sqlite/index.js.map +1 -0
- package/dist/storage/sqlite/migrations.d.ts +11 -0
- package/dist/storage/sqlite/migrations.d.ts.map +1 -0
- package/dist/storage/sqlite/migrations.js +111 -0
- package/dist/storage/sqlite/migrations.js.map +1 -0
- package/dist/storage/sqlite/node.d.ts +41 -0
- package/dist/storage/sqlite/node.d.ts.map +1 -0
- package/dist/storage/sqlite/node.js +179 -0
- package/dist/storage/sqlite/node.js.map +1 -0
- package/dist/storage/sqlite/storage.d.ts +65 -0
- package/dist/storage/sqlite/storage.d.ts.map +1 -0
- package/dist/storage/sqlite/storage.js +670 -0
- package/dist/storage/sqlite/storage.js.map +1 -0
- package/dist/tasks.d.ts +6 -0
- package/dist/tasks.d.ts.map +1 -0
- package/dist/tasks.js +5 -0
- package/dist/tasks.js.map +1 -0
- package/dist/testing/assertions.d.ts +5 -0
- package/dist/testing/assertions.d.ts.map +1 -0
- package/dist/testing/assertions.js +25 -0
- package/dist/testing/assertions.js.map +1 -0
- package/dist/testing/env-conformance.d.ts +7 -0
- package/dist/testing/env-conformance.d.ts.map +1 -0
- package/dist/testing/env-conformance.js +431 -0
- package/dist/testing/env-conformance.js.map +1 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +6 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/runner.d.ts +12 -0
- package/dist/testing/runner.d.ts.map +1 -0
- package/dist/testing/runner.js +23 -0
- package/dist/testing/runner.js.map +1 -0
- package/dist/testing/storage-benchmark.d.ts +41 -0
- package/dist/testing/storage-benchmark.d.ts.map +1 -0
- package/dist/testing/storage-benchmark.js +350 -0
- package/dist/testing/storage-benchmark.js.map +1 -0
- package/dist/testing/storage-conformance.d.ts +4 -0
- package/dist/testing/storage-conformance.d.ts.map +1 -0
- package/dist/testing/storage-conformance.js +1213 -0
- package/dist/testing/storage-conformance.js.map +1 -0
- package/dist/testing/types.d.ts +37 -0
- package/dist/testing/types.d.ts.map +1 -0
- package/dist/testing/types.js +2 -0
- package/dist/testing/types.js.map +1 -0
- package/dist/tools/bash.d.ts +55 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +138 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/edit-diff.d.ts +89 -0
- package/dist/tools/edit-diff.d.ts.map +1 -0
- package/dist/tools/edit-diff.js +386 -0
- package/dist/tools/edit-diff.js.map +1 -0
- package/dist/tools/edit.d.ts +18 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +117 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/env.d.ts +5 -0
- package/dist/tools/env.d.ts.map +1 -0
- package/dist/tools/env.js +7 -0
- package/dist/tools/env.js.map +1 -0
- package/dist/tools/file-mutation-queue.d.ts +9 -0
- package/dist/tools/file-mutation-queue.d.ts.map +1 -0
- package/dist/tools/file-mutation-queue.js +52 -0
- package/dist/tools/file-mutation-queue.js.map +1 -0
- package/dist/tools/image-processor.d.ts +47 -0
- package/dist/tools/image-processor.d.ts.map +1 -0
- package/dist/tools/image-processor.js +30 -0
- package/dist/tools/image-processor.js.map +1 -0
- package/dist/tools/image.d.ts +20 -0
- package/dist/tools/image.d.ts.map +1 -0
- package/dist/tools/image.js +178 -0
- package/dist/tools/image.js.map +1 -0
- package/dist/tools/index.d.ts +46 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/path-utils.d.ts +5 -0
- package/dist/tools/path-utils.d.ts.map +1 -0
- package/dist/tools/path-utils.js +26 -0
- package/dist/tools/path-utils.js.map +1 -0
- package/dist/tools/read.d.ts +25 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +235 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/write.d.ts +10 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +31 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/truncate.d.ts +64 -0
- package/dist/truncate.d.ts.map +1 -0
- package/dist/truncate.js +154 -0
- package/dist/truncate.js.map +1 -0
- package/dist/types.d.ts +932 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +140 -5
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import { StorageRequestError } from "../../errors.js";
|
|
2
|
+
import { idFromNumber, seqFromNumber } from "../../ids.js";
|
|
3
|
+
import { MemoryStorage } from "../memory.js";
|
|
4
|
+
const FORMAT_VERSION = 1;
|
|
5
|
+
const MAIN_FILE = "main.jsonl";
|
|
6
|
+
const RECLAIM_SUFFIX = ".reclaim";
|
|
7
|
+
const textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
8
|
+
export class JsonlCorruptionError extends Error {
|
|
9
|
+
constructor(message, cause) {
|
|
10
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
11
|
+
this.name = "JsonlCorruptionError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class JsonlStoragePoisonedError extends Error {
|
|
15
|
+
constructor(cause) {
|
|
16
|
+
super("JSONL storage is poisoned and must be reopened", { cause });
|
|
17
|
+
this.name = "JsonlStoragePoisonedError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
21
|
+
const isSafeInteger = (value) => Number.isSafeInteger(value);
|
|
22
|
+
const sidecarFileName = (kind, id) => `${kind}-${id}.jsonl`;
|
|
23
|
+
const isSidecarFileName = (name) => /^(?:doc|task)-(?:0|[1-9]\d*)\.jsonl$/.test(name);
|
|
24
|
+
const isReclaimFileName = (name) => /^(?:doc|task)-(?:0|[1-9]\d*)\.jsonl\.reclaim$/.test(name);
|
|
25
|
+
const isCurrentOnly = (record) => record.scope.kind !== "conversation" || record.history === "latest";
|
|
26
|
+
const sidecarKey = (file, seq, ordinal) => JSON.stringify([file, seq, ordinal]);
|
|
27
|
+
const jsonLine = (value) => `${JSON.stringify(value)}\n`;
|
|
28
|
+
const errorFromFile = (action, error) => new Error(`JSONL ${action} failed: ${error.message}`, { cause: error });
|
|
29
|
+
const parseJson = (text, description) => {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(text);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
throw new JsonlCorruptionError(`Malformed complete ${description}`, error instanceof Error ? error : undefined);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const validateMainOperation = (value, description) => {
|
|
38
|
+
if (!isObject(value) || typeof value.type !== "string") {
|
|
39
|
+
throw new JsonlCorruptionError(`Invalid write in ${description}`);
|
|
40
|
+
}
|
|
41
|
+
switch (value.type) {
|
|
42
|
+
case "conversation":
|
|
43
|
+
case "entry":
|
|
44
|
+
case "submission":
|
|
45
|
+
if (!isObject(value.value) || !isSafeInteger(value.value.id)) {
|
|
46
|
+
throw new JsonlCorruptionError(`Invalid ${value.type} write in ${description}`);
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
case "task":
|
|
50
|
+
if (!isObject(value.value) ||
|
|
51
|
+
!isSafeInteger(value.value.id) ||
|
|
52
|
+
!isObject(value.value.state) ||
|
|
53
|
+
value.value.state.status !== "terminal") {
|
|
54
|
+
throw new JsonlCorruptionError(`Invalid terminal task write in ${description}`);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
case "document.retire":
|
|
58
|
+
if (!isSafeInteger(value.id))
|
|
59
|
+
throw new JsonlCorruptionError(`Invalid document retirement in ${description}`);
|
|
60
|
+
return value;
|
|
61
|
+
case "task.sidecar":
|
|
62
|
+
if (!isSafeInteger(value.id) || !isSafeInteger(value.ordinal) || value.ordinal < 0) {
|
|
63
|
+
throw new JsonlCorruptionError(`Invalid task sidecar write in ${description}`);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
case "document.create":
|
|
67
|
+
if (!isObject(value.record) ||
|
|
68
|
+
!isSafeInteger(value.record.id) ||
|
|
69
|
+
!isSafeInteger(value.ordinal) ||
|
|
70
|
+
value.ordinal < 0) {
|
|
71
|
+
throw new JsonlCorruptionError(`Invalid document creation in ${description}`);
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
case "document.change":
|
|
75
|
+
if (!isSafeInteger(value.id) || !isSafeInteger(value.ordinal) || value.ordinal < 0) {
|
|
76
|
+
throw new JsonlCorruptionError(`Invalid document change in ${description}`);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
default:
|
|
80
|
+
throw new JsonlCorruptionError(`Unknown write type in ${description}`);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const parseMainMarker = (text, line) => {
|
|
84
|
+
const description = `${MAIN_FILE} line ${line}`;
|
|
85
|
+
const value = parseJson(text, description);
|
|
86
|
+
if (!isObject(value) ||
|
|
87
|
+
value.format !== FORMAT_VERSION ||
|
|
88
|
+
value.type !== "commit" ||
|
|
89
|
+
!isSafeInteger(value.seq) ||
|
|
90
|
+
value.seq < 1 ||
|
|
91
|
+
!Array.isArray(value.writes)) {
|
|
92
|
+
throw new JsonlCorruptionError(`Invalid commit marker in ${description}`);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
format: FORMAT_VERSION,
|
|
96
|
+
type: "commit",
|
|
97
|
+
seq: seqFromNumber(value.seq),
|
|
98
|
+
writes: value.writes.map((write) => validateMainOperation(write, description)),
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
const validateDocumentContent = (value, description) => {
|
|
102
|
+
if (!isObject(value) || !isSafeInteger(value.version) || value.version < 1) {
|
|
103
|
+
throw new JsonlCorruptionError(`Invalid document content in ${description}`);
|
|
104
|
+
}
|
|
105
|
+
if (value.kind === "base" && isObject(value.value))
|
|
106
|
+
return value;
|
|
107
|
+
if (value.kind === "delta" && Array.isArray(value.ops))
|
|
108
|
+
return value;
|
|
109
|
+
throw new JsonlCorruptionError(`Invalid document content in ${description}`);
|
|
110
|
+
};
|
|
111
|
+
const parseSidecarRecord = (text, file, line) => {
|
|
112
|
+
const description = `${file} line ${line}`;
|
|
113
|
+
const value = parseJson(text, description);
|
|
114
|
+
if (!isObject(value) ||
|
|
115
|
+
value.format !== FORMAT_VERSION ||
|
|
116
|
+
value.type !== "record" ||
|
|
117
|
+
!isSafeInteger(value.seq) ||
|
|
118
|
+
value.seq < 1 ||
|
|
119
|
+
!isSafeInteger(value.ordinal) ||
|
|
120
|
+
value.ordinal < 0 ||
|
|
121
|
+
!isObject(value.payload) ||
|
|
122
|
+
typeof value.payload.type !== "string") {
|
|
123
|
+
throw new JsonlCorruptionError(`Invalid sidecar record in ${description}`);
|
|
124
|
+
}
|
|
125
|
+
if (value.payload.type === "task") {
|
|
126
|
+
if (!isObject(value.payload.value) ||
|
|
127
|
+
!isSafeInteger(value.payload.value.id) ||
|
|
128
|
+
!isObject(value.payload.value.state) ||
|
|
129
|
+
value.payload.value.state.status === "terminal") {
|
|
130
|
+
throw new JsonlCorruptionError(`Invalid live task record in ${description}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (value.payload.type === "document") {
|
|
134
|
+
if (!isSafeInteger(value.payload.id)) {
|
|
135
|
+
throw new JsonlCorruptionError(`Invalid document record in ${description}`);
|
|
136
|
+
}
|
|
137
|
+
validateDocumentContent(value.payload.content, description);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
throw new JsonlCorruptionError(`Unknown sidecar record type in ${description}`);
|
|
141
|
+
}
|
|
142
|
+
return value;
|
|
143
|
+
};
|
|
144
|
+
/** Portable JSONL implementation of the storage contract. */
|
|
145
|
+
export class JsonlStorage {
|
|
146
|
+
fs;
|
|
147
|
+
directory;
|
|
148
|
+
mainPath;
|
|
149
|
+
fsync;
|
|
150
|
+
memory = new MemoryStorage();
|
|
151
|
+
currentOnlyDocuments = new Set();
|
|
152
|
+
liveTaskSidecars = new Set();
|
|
153
|
+
closed = false;
|
|
154
|
+
poisonError;
|
|
155
|
+
constructor(fs, directory, mainPath, options) {
|
|
156
|
+
this.fs = fs;
|
|
157
|
+
this.directory = directory;
|
|
158
|
+
this.mainPath = mainPath;
|
|
159
|
+
this.fsync = options.fsync ?? false;
|
|
160
|
+
}
|
|
161
|
+
/** Open or create a JSONL storage directory using the supplied filesystem. */
|
|
162
|
+
static async open(directory, fs, context, options = {}) {
|
|
163
|
+
const absolute = await fs.absolutePath(directory, context);
|
|
164
|
+
if (!absolute.ok)
|
|
165
|
+
throw errorFromFile("path resolution", absolute.error);
|
|
166
|
+
const created = await fs.createDir(absolute.value, { recursive: true }, context);
|
|
167
|
+
if (!created.ok)
|
|
168
|
+
throw errorFromFile("directory creation", created.error);
|
|
169
|
+
const mainPathResult = await fs.joinPath([absolute.value, MAIN_FILE], context);
|
|
170
|
+
if (!mainPathResult.ok)
|
|
171
|
+
throw errorFromFile("path join", mainPathResult.error);
|
|
172
|
+
const storage = new JsonlStorage(fs, absolute.value, mainPathResult.value, options);
|
|
173
|
+
await storage.recover(context);
|
|
174
|
+
return storage;
|
|
175
|
+
}
|
|
176
|
+
async commit(writes, context) {
|
|
177
|
+
this.assertUsable();
|
|
178
|
+
const prepared = this.memory.prepareCommit(writes);
|
|
179
|
+
const encoded = this.encodeCommit(prepared.seq, prepared.writes);
|
|
180
|
+
const reclamations = this.planReclamations(prepared.writes, encoded);
|
|
181
|
+
const sidecars = await Promise.all([...encoded.sidecars].map(async ([file, content]) => ({
|
|
182
|
+
file,
|
|
183
|
+
content,
|
|
184
|
+
path: await this.resolveFile(file, context),
|
|
185
|
+
})));
|
|
186
|
+
for (const sidecar of sidecars) {
|
|
187
|
+
const result = await this.fs.appendFile(sidecar.path, sidecar.content, context);
|
|
188
|
+
if (!result.ok)
|
|
189
|
+
throw this.poison(errorFromFile(`append to ${sidecar.file}`, result.error));
|
|
190
|
+
}
|
|
191
|
+
if (this.fsync) {
|
|
192
|
+
for (const sidecar of sidecars) {
|
|
193
|
+
const result = await this.fs.flushFile(sidecar.path, context);
|
|
194
|
+
if (!result.ok)
|
|
195
|
+
throw this.poison(errorFromFile(`flush of ${sidecar.file}`, result.error));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const marker = await this.fs.appendFile(this.mainPath, encoded.marker, context);
|
|
199
|
+
if (!marker.ok)
|
|
200
|
+
throw this.poison(errorFromFile(`append to ${MAIN_FILE}`, marker.error));
|
|
201
|
+
const seq = prepared.apply();
|
|
202
|
+
this.adoptSidecarState(prepared.writes);
|
|
203
|
+
await this.reclaimSidecars(reclamations, context);
|
|
204
|
+
return seq;
|
|
205
|
+
}
|
|
206
|
+
async mintId() {
|
|
207
|
+
return this.store.mintId();
|
|
208
|
+
}
|
|
209
|
+
async conversation(id, context) {
|
|
210
|
+
return this.store.conversation(id, context);
|
|
211
|
+
}
|
|
212
|
+
async scanConversations(query, limit, cursor, context) {
|
|
213
|
+
return this.store.scanConversations(query, limit, cursor, context);
|
|
214
|
+
}
|
|
215
|
+
async entry(idOrConversationId, idOrContext, context) {
|
|
216
|
+
if (context === undefined)
|
|
217
|
+
return this.store.entry(idFromNumber(idOrConversationId), idOrContext);
|
|
218
|
+
if (typeof idOrContext !== "number")
|
|
219
|
+
throw new StorageRequestError("Storage.entry() requires an entry ID");
|
|
220
|
+
return this.store.entry(idFromNumber(idOrConversationId), idFromNumber(idOrContext), context);
|
|
221
|
+
}
|
|
222
|
+
async findLatestHeadMarker(conversationId, atOrBeforeEntryId, context) {
|
|
223
|
+
return this.store.findLatestHeadMarker(conversationId, atOrBeforeEntryId, context);
|
|
224
|
+
}
|
|
225
|
+
async scanEntries(query, limit, cursor, context) {
|
|
226
|
+
return this.store.scanEntries(query, limit, cursor, context);
|
|
227
|
+
}
|
|
228
|
+
async task(id, context) {
|
|
229
|
+
return this.store.task(id, context);
|
|
230
|
+
}
|
|
231
|
+
async scanTasks(query, limit, cursor, context) {
|
|
232
|
+
return this.store.scanTasks(query, limit, cursor, context);
|
|
233
|
+
}
|
|
234
|
+
async submission(id, context) {
|
|
235
|
+
return this.store.submission(id, context);
|
|
236
|
+
}
|
|
237
|
+
async scanSubmissions(query, limit, cursor, context) {
|
|
238
|
+
return this.store.scanSubmissions(query, limit, cursor, context);
|
|
239
|
+
}
|
|
240
|
+
async submissionByRequest(conversationId, requestId, context) {
|
|
241
|
+
return this.store.submissionByRequest(conversationId, requestId, context);
|
|
242
|
+
}
|
|
243
|
+
async findDocument(address, at, context) {
|
|
244
|
+
return this.store.findDocument(address, at, context);
|
|
245
|
+
}
|
|
246
|
+
async document(id, at, context) {
|
|
247
|
+
return this.store.document(id, at, context);
|
|
248
|
+
}
|
|
249
|
+
async scanDocuments(query, limit, cursor, context) {
|
|
250
|
+
return this.store.scanDocuments(query, limit, cursor, context);
|
|
251
|
+
}
|
|
252
|
+
async close(context) {
|
|
253
|
+
if (this.closed)
|
|
254
|
+
return;
|
|
255
|
+
this.closed = true;
|
|
256
|
+
await this.memory.close(context);
|
|
257
|
+
}
|
|
258
|
+
encodeCommit(seq, writes) {
|
|
259
|
+
const mainWrites = [];
|
|
260
|
+
const records = new Map();
|
|
261
|
+
let nextOrdinal = 0;
|
|
262
|
+
const addSidecar = (file, payload) => {
|
|
263
|
+
const ordinal = nextOrdinal++;
|
|
264
|
+
let fileRecords = records.get(file);
|
|
265
|
+
if (fileRecords === undefined) {
|
|
266
|
+
fileRecords = [];
|
|
267
|
+
records.set(file, fileRecords);
|
|
268
|
+
}
|
|
269
|
+
fileRecords.push({ format: FORMAT_VERSION, type: "record", seq, ordinal, payload });
|
|
270
|
+
return ordinal;
|
|
271
|
+
};
|
|
272
|
+
for (const write of writes) {
|
|
273
|
+
switch (write.type) {
|
|
274
|
+
case "conversation":
|
|
275
|
+
case "entry":
|
|
276
|
+
case "submission":
|
|
277
|
+
case "document.retire":
|
|
278
|
+
mainWrites.push(write);
|
|
279
|
+
break;
|
|
280
|
+
case "task":
|
|
281
|
+
if (write.value.state.status === "terminal")
|
|
282
|
+
mainWrites.push(write);
|
|
283
|
+
else {
|
|
284
|
+
const ordinal = addSidecar(sidecarFileName("task", write.value.id), {
|
|
285
|
+
type: "task",
|
|
286
|
+
value: write.value,
|
|
287
|
+
});
|
|
288
|
+
mainWrites.push({ type: "task.sidecar", id: write.value.id, ordinal });
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
case "document.create": {
|
|
292
|
+
const ordinal = addSidecar(sidecarFileName("doc", write.record.id), {
|
|
293
|
+
type: "document",
|
|
294
|
+
id: write.record.id,
|
|
295
|
+
content: write.content,
|
|
296
|
+
});
|
|
297
|
+
mainWrites.push({ type: "document.create", record: write.record, ordinal });
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
case "document.change": {
|
|
301
|
+
const ordinal = addSidecar(sidecarFileName("doc", write.id), {
|
|
302
|
+
type: "document",
|
|
303
|
+
id: write.id,
|
|
304
|
+
content: write.content,
|
|
305
|
+
});
|
|
306
|
+
mainWrites.push({ type: "document.change", id: write.id, ordinal });
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const sidecars = new Map();
|
|
312
|
+
for (const [file, fileRecords] of records) {
|
|
313
|
+
sidecars.set(file, fileRecords.map((record) => jsonLine(record)).join(""));
|
|
314
|
+
}
|
|
315
|
+
const marker = {
|
|
316
|
+
format: FORMAT_VERSION,
|
|
317
|
+
type: "commit",
|
|
318
|
+
seq,
|
|
319
|
+
writes: mainWrites,
|
|
320
|
+
};
|
|
321
|
+
return { marker: jsonLine(marker), sidecars };
|
|
322
|
+
}
|
|
323
|
+
planReclamations(writes, encoded) {
|
|
324
|
+
const createdCurrentOnlyDocuments = new Set();
|
|
325
|
+
const retiredDocuments = new Set();
|
|
326
|
+
const baseDocuments = new Set();
|
|
327
|
+
const finalTasks = new Map();
|
|
328
|
+
for (const write of writes) {
|
|
329
|
+
switch (write.type) {
|
|
330
|
+
case "document.create":
|
|
331
|
+
if (isCurrentOnly(write.record))
|
|
332
|
+
createdCurrentOnlyDocuments.add(write.record.id);
|
|
333
|
+
break;
|
|
334
|
+
case "document.change":
|
|
335
|
+
if (write.content.kind === "base")
|
|
336
|
+
baseDocuments.add(write.id);
|
|
337
|
+
break;
|
|
338
|
+
case "document.retire":
|
|
339
|
+
retiredDocuments.add(write.id);
|
|
340
|
+
break;
|
|
341
|
+
case "task":
|
|
342
|
+
finalTasks.set(write.value.id, write.value);
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const replacements = new Map();
|
|
347
|
+
const isCurrentOnlyDocument = (id) => this.currentOnlyDocuments.has(id) || createdCurrentOnlyDocuments.has(id);
|
|
348
|
+
for (const id of retiredDocuments) {
|
|
349
|
+
if (isCurrentOnlyDocument(id))
|
|
350
|
+
replacements.set(sidecarFileName("doc", id), "");
|
|
351
|
+
}
|
|
352
|
+
for (const id of baseDocuments) {
|
|
353
|
+
if (!isCurrentOnlyDocument(id) || retiredDocuments.has(id))
|
|
354
|
+
continue;
|
|
355
|
+
const file = sidecarFileName("doc", id);
|
|
356
|
+
const content = encoded.sidecars.get(file);
|
|
357
|
+
if (content !== undefined)
|
|
358
|
+
replacements.set(file, content);
|
|
359
|
+
}
|
|
360
|
+
for (const [id, task] of finalTasks) {
|
|
361
|
+
if (task.state.status === "terminal" &&
|
|
362
|
+
(this.liveTaskSidecars.has(id) || encoded.sidecars.has(sidecarFileName("task", id)))) {
|
|
363
|
+
replacements.set(sidecarFileName("task", id), "");
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return replacements;
|
|
367
|
+
}
|
|
368
|
+
adoptSidecarState(writes) {
|
|
369
|
+
for (const write of writes) {
|
|
370
|
+
if (write.type === "document.create") {
|
|
371
|
+
if (isCurrentOnly(write.record))
|
|
372
|
+
this.currentOnlyDocuments.add(write.record.id);
|
|
373
|
+
}
|
|
374
|
+
else if (write.type === "task") {
|
|
375
|
+
if (write.value.state.status === "terminal")
|
|
376
|
+
this.liveTaskSidecars.delete(write.value.id);
|
|
377
|
+
else
|
|
378
|
+
this.liveTaskSidecars.add(write.value.id);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/** The marker already published this state, so reclamation is retryable best-effort maintenance. */
|
|
383
|
+
async reclaimSidecars(replacements, context) {
|
|
384
|
+
if (replacements.size === 0)
|
|
385
|
+
return;
|
|
386
|
+
if (this.fsync) {
|
|
387
|
+
const flushed = await this.fs.flushFile(this.mainPath, context);
|
|
388
|
+
if (!flushed.ok)
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
for (const [file, content] of replacements)
|
|
392
|
+
await this.replaceSidecar(file, content, context);
|
|
393
|
+
}
|
|
394
|
+
async replaceSidecar(file, content, context) {
|
|
395
|
+
const path = await this.fs.joinPath([this.directory, file], context);
|
|
396
|
+
if (!path.ok)
|
|
397
|
+
return;
|
|
398
|
+
if (content === "") {
|
|
399
|
+
await this.fs.remove(path.value, { force: true }, context);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const temporaryPath = await this.fs.joinPath([this.directory, `${file}${RECLAIM_SUFFIX}`], context);
|
|
403
|
+
if (!temporaryPath.ok)
|
|
404
|
+
return;
|
|
405
|
+
const written = await this.fs.writeFile(temporaryPath.value, content, context);
|
|
406
|
+
if (!written.ok)
|
|
407
|
+
return;
|
|
408
|
+
if (this.fsync) {
|
|
409
|
+
const flushed = await this.fs.flushFile(temporaryPath.value, context);
|
|
410
|
+
if (!flushed.ok)
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
await this.fs.renameFile(temporaryPath.value, path.value, context);
|
|
414
|
+
}
|
|
415
|
+
async recover(context) {
|
|
416
|
+
const fs = this.fs;
|
|
417
|
+
const directory = this.directory;
|
|
418
|
+
const memory = this.memory;
|
|
419
|
+
const main = await JsonlStorage.readLines(fs, this.mainPath, MAIN_FILE, context, (text, line) => parseMainMarker(text, line));
|
|
420
|
+
let previousSeq = 0;
|
|
421
|
+
for (const marker of main.lines) {
|
|
422
|
+
if (marker.value.seq <= previousSeq) {
|
|
423
|
+
throw new JsonlCorruptionError(`Commit sequence does not strictly increase in ${MAIN_FILE}`);
|
|
424
|
+
}
|
|
425
|
+
previousSeq = marker.value.seq;
|
|
426
|
+
}
|
|
427
|
+
const listed = await fs.listDir(directory, context);
|
|
428
|
+
if (!listed.ok)
|
|
429
|
+
throw errorFromFile("directory listing", listed.error);
|
|
430
|
+
for (const info of listed.value) {
|
|
431
|
+
if (info.kind === "file" && isReclaimFileName(info.name)) {
|
|
432
|
+
await fs.remove(info.path, { force: true }, context);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const sidecarFiles = listed.value
|
|
436
|
+
.filter((info) => info.kind === "file" && isSidecarFileName(info.name))
|
|
437
|
+
.map((info) => info.name)
|
|
438
|
+
.sort();
|
|
439
|
+
const parsedFiles = new Map();
|
|
440
|
+
const recordByKey = new Map();
|
|
441
|
+
for (const file of sidecarFiles) {
|
|
442
|
+
const pathResult = await fs.joinPath([directory, file], context);
|
|
443
|
+
if (!pathResult.ok)
|
|
444
|
+
throw errorFromFile("path join", pathResult.error);
|
|
445
|
+
const parsed = await JsonlStorage.readLines(fs, pathResult.value, file, context, (text, line) => parseSidecarRecord(text, file, line));
|
|
446
|
+
parsedFiles.set(file, parsed);
|
|
447
|
+
let previous;
|
|
448
|
+
for (const line of parsed.lines) {
|
|
449
|
+
if (previous !== undefined &&
|
|
450
|
+
(line.value.seq < previous.seq ||
|
|
451
|
+
(line.value.seq === previous.seq && line.value.ordinal <= previous.ordinal))) {
|
|
452
|
+
throw new JsonlCorruptionError(`Sidecar records are out of order in ${file}`);
|
|
453
|
+
}
|
|
454
|
+
previous = line.value;
|
|
455
|
+
recordByKey.set(sidecarKey(file, line.value.seq, line.value.ordinal), line);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const currentOnlyDocuments = new Set();
|
|
459
|
+
const retiredDocuments = new Set();
|
|
460
|
+
const finalTaskIsLive = new Map();
|
|
461
|
+
for (const { value: marker } of main.lines) {
|
|
462
|
+
for (const operation of marker.writes) {
|
|
463
|
+
if (operation.type === "document.create") {
|
|
464
|
+
if (isCurrentOnly(operation.record))
|
|
465
|
+
currentOnlyDocuments.add(operation.record.id);
|
|
466
|
+
}
|
|
467
|
+
else if (operation.type === "document.retire") {
|
|
468
|
+
retiredDocuments.add(operation.id);
|
|
469
|
+
}
|
|
470
|
+
else if (operation.type === "task") {
|
|
471
|
+
finalTaskIsLive.set(operation.value.id, false);
|
|
472
|
+
}
|
|
473
|
+
else if (operation.type === "task.sidecar") {
|
|
474
|
+
finalTaskIsLive.set(operation.id, true);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const retiredCurrentOnlyDocuments = new Set([...retiredDocuments].filter((id) => currentOnlyDocuments.has(id)));
|
|
479
|
+
const latestBases = new Map();
|
|
480
|
+
for (const { value: marker } of main.lines) {
|
|
481
|
+
for (const operation of marker.writes) {
|
|
482
|
+
if (operation.type !== "document.create" && operation.type !== "document.change")
|
|
483
|
+
continue;
|
|
484
|
+
const id = operation.type === "document.create" ? operation.record.id : operation.id;
|
|
485
|
+
if (!currentOnlyDocuments.has(id))
|
|
486
|
+
continue;
|
|
487
|
+
const record = recordByKey.get(sidecarKey(sidecarFileName("doc", id), marker.seq, operation.ordinal))?.value;
|
|
488
|
+
if (record?.payload.type !== "document" ||
|
|
489
|
+
record.payload.id !== id ||
|
|
490
|
+
record.payload.content.kind !== "base") {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const previous = latestBases.get(id);
|
|
494
|
+
if (previous === undefined ||
|
|
495
|
+
record.seq > previous.seq ||
|
|
496
|
+
(record.seq === previous.seq && record.ordinal > previous.ordinal)) {
|
|
497
|
+
latestBases.set(id, record);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const isBeforeLatestBase = (id, seq, ordinal) => {
|
|
502
|
+
const base = latestBases.get(id);
|
|
503
|
+
return base !== undefined && (seq < base.seq || (seq === base.seq && ordinal < base.ordinal));
|
|
504
|
+
};
|
|
505
|
+
const terminalTasks = new Set([...finalTaskIsLive].filter(([, live]) => !live).map(([id]) => id));
|
|
506
|
+
const confirmed = new Set();
|
|
507
|
+
for (const line of main.lines) {
|
|
508
|
+
const marker = line.value;
|
|
509
|
+
const writes = [];
|
|
510
|
+
for (const operation of marker.writes) {
|
|
511
|
+
switch (operation.type) {
|
|
512
|
+
case "conversation":
|
|
513
|
+
case "entry":
|
|
514
|
+
case "submission":
|
|
515
|
+
case "task":
|
|
516
|
+
case "document.retire":
|
|
517
|
+
writes.push(operation);
|
|
518
|
+
break;
|
|
519
|
+
case "task.sidecar": {
|
|
520
|
+
const optional = terminalTasks.has(operation.id);
|
|
521
|
+
const record = JsonlStorage.confirmRecord(marker, operation.ordinal, sidecarFileName("task", operation.id), recordByKey, confirmed, optional);
|
|
522
|
+
if (record !== undefined) {
|
|
523
|
+
if (record.payload.type !== "task" || record.payload.value.id !== operation.id) {
|
|
524
|
+
throw new JsonlCorruptionError(`Confirmed task sidecar data does not match commit ${marker.seq}`);
|
|
525
|
+
}
|
|
526
|
+
if (!optional)
|
|
527
|
+
writes.push({ type: "task", value: record.payload.value });
|
|
528
|
+
}
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
case "document.create":
|
|
532
|
+
case "document.change": {
|
|
533
|
+
const id = operation.type === "document.create" ? operation.record.id : operation.id;
|
|
534
|
+
const reclaimed = retiredCurrentOnlyDocuments.has(id) || isBeforeLatestBase(id, marker.seq, operation.ordinal);
|
|
535
|
+
const record = JsonlStorage.confirmRecord(marker, operation.ordinal, sidecarFileName("doc", id), recordByKey, confirmed, reclaimed);
|
|
536
|
+
let content;
|
|
537
|
+
if (record !== undefined) {
|
|
538
|
+
if (record.payload.type !== "document" || record.payload.id !== id) {
|
|
539
|
+
throw new JsonlCorruptionError(`Confirmed document sidecar data does not match commit ${marker.seq}`);
|
|
540
|
+
}
|
|
541
|
+
content = record.payload.content;
|
|
542
|
+
}
|
|
543
|
+
if (operation.type === "document.create") {
|
|
544
|
+
if (content !== undefined && content.kind !== "base") {
|
|
545
|
+
throw new JsonlCorruptionError(`Document creation lacks a confirmed base in commit ${marker.seq}`);
|
|
546
|
+
}
|
|
547
|
+
writes.push({
|
|
548
|
+
type: "document.create",
|
|
549
|
+
record: operation.record,
|
|
550
|
+
content: reclaimed || content === undefined ? { kind: "base", version: 1, value: {} } : content,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
else if (!reclaimed && content !== undefined) {
|
|
554
|
+
writes.push({ type: "document.change", id, content });
|
|
555
|
+
}
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
memory.prepareCommit(writes, marker.seq).apply();
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
throw new JsonlCorruptionError(`Invalid committed state at sequence ${marker.seq}`, error instanceof Error ? error : undefined);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const reclamations = new Map();
|
|
568
|
+
for (const [file, parsed] of parsedFiles) {
|
|
569
|
+
let unconfirmedAt;
|
|
570
|
+
for (const line of parsed.lines) {
|
|
571
|
+
const key = sidecarKey(file, line.value.seq, line.value.ordinal);
|
|
572
|
+
if (confirmed.has(key)) {
|
|
573
|
+
if (unconfirmedAt !== undefined) {
|
|
574
|
+
throw new JsonlCorruptionError(`Confirmed record follows an unconfirmed tail in ${file}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
else if (unconfirmedAt === undefined) {
|
|
578
|
+
unconfirmedAt = line.start;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (unconfirmedAt !== undefined) {
|
|
582
|
+
const truncated = await fs.truncateFile(parsed.path, unconfirmedAt, context);
|
|
583
|
+
if (!truncated.ok)
|
|
584
|
+
throw errorFromFile(`tail truncation of ${file}`, truncated.error);
|
|
585
|
+
}
|
|
586
|
+
const numericId = Number(file.slice(file.indexOf("-") + 1, -".jsonl".length));
|
|
587
|
+
const confirmedLines = parsed.lines.filter((line) => confirmed.has(sidecarKey(file, line.value.seq, line.value.ordinal)));
|
|
588
|
+
let retainedLines;
|
|
589
|
+
if (file.startsWith("task-") && terminalTasks.has(idFromNumber(numericId))) {
|
|
590
|
+
retainedLines = [];
|
|
591
|
+
}
|
|
592
|
+
else if (file.startsWith("doc-")) {
|
|
593
|
+
const documentId = idFromNumber(numericId);
|
|
594
|
+
if (retiredCurrentOnlyDocuments.has(documentId)) {
|
|
595
|
+
retainedLines = [];
|
|
596
|
+
}
|
|
597
|
+
else if (latestBases.has(documentId)) {
|
|
598
|
+
retainedLines = confirmedLines.filter((line) => !isBeforeLatestBase(documentId, line.value.seq, line.value.ordinal));
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (retainedLines !== undefined &&
|
|
602
|
+
(retainedLines.length < confirmedLines.length || retainedLines.length === 0)) {
|
|
603
|
+
reclamations.set(file, retainedLines.map((line) => jsonLine(line.value)).join(""));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
await this.reclaimSidecars(reclamations, context);
|
|
607
|
+
for (const id of currentOnlyDocuments)
|
|
608
|
+
this.currentOnlyDocuments.add(id);
|
|
609
|
+
for (const [id, live] of finalTaskIsLive) {
|
|
610
|
+
if (live)
|
|
611
|
+
this.liveTaskSidecars.add(id);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
static confirmRecord(marker, ordinal, file, recordByKey, confirmed, optional) {
|
|
615
|
+
const key = sidecarKey(file, marker.seq, ordinal);
|
|
616
|
+
if (confirmed.has(key))
|
|
617
|
+
throw new JsonlCorruptionError(`Sidecar record is confirmed more than once`);
|
|
618
|
+
const line = recordByKey.get(key);
|
|
619
|
+
if (line === undefined) {
|
|
620
|
+
if (optional)
|
|
621
|
+
return undefined;
|
|
622
|
+
throw new JsonlCorruptionError(`Missing confirmed sidecar record ${file} at sequence ${marker.seq}`);
|
|
623
|
+
}
|
|
624
|
+
confirmed.add(key);
|
|
625
|
+
return line.value;
|
|
626
|
+
}
|
|
627
|
+
static async readLines(fs, path, name, context, parse) {
|
|
628
|
+
const read = await fs.readBinaryFile(path, context);
|
|
629
|
+
if (!read.ok) {
|
|
630
|
+
if (read.error.code === "not_found")
|
|
631
|
+
return { path, lines: [] };
|
|
632
|
+
throw errorFromFile(`read of ${name}`, read.error);
|
|
633
|
+
}
|
|
634
|
+
const bytes = read.value;
|
|
635
|
+
let completeSize = bytes.length;
|
|
636
|
+
if (completeSize > 0 && bytes[completeSize - 1] !== 0x0a) {
|
|
637
|
+
completeSize = bytes.lastIndexOf(0x0a) + 1;
|
|
638
|
+
const truncated = await fs.truncateFile(path, completeSize, context);
|
|
639
|
+
if (!truncated.ok)
|
|
640
|
+
throw errorFromFile(`torn-line truncation of ${name}`, truncated.error);
|
|
641
|
+
}
|
|
642
|
+
const lines = [];
|
|
643
|
+
let start = 0;
|
|
644
|
+
let lineNumber = 1;
|
|
645
|
+
for (let end = 0; end < completeSize; end++) {
|
|
646
|
+
if (bytes[end] !== 0x0a)
|
|
647
|
+
continue;
|
|
648
|
+
let text;
|
|
649
|
+
try {
|
|
650
|
+
text = textDecoder.decode(bytes.subarray(start, end));
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
throw new JsonlCorruptionError(`Invalid UTF-8 in complete ${name} line ${lineNumber}`, error instanceof Error ? error : undefined);
|
|
654
|
+
}
|
|
655
|
+
lines.push({ value: parse(text, lineNumber), start });
|
|
656
|
+
start = end + 1;
|
|
657
|
+
lineNumber++;
|
|
658
|
+
}
|
|
659
|
+
return { path, lines };
|
|
660
|
+
}
|
|
661
|
+
async resolveFile(file, context) {
|
|
662
|
+
const path = await this.fs.joinPath([this.directory, file], context);
|
|
663
|
+
if (!path.ok)
|
|
664
|
+
throw errorFromFile("path join", path.error);
|
|
665
|
+
return path.value;
|
|
666
|
+
}
|
|
667
|
+
get store() {
|
|
668
|
+
this.assertUsable();
|
|
669
|
+
return this.memory;
|
|
670
|
+
}
|
|
671
|
+
poison(cause) {
|
|
672
|
+
this.poisonError ??= new JsonlStoragePoisonedError(cause);
|
|
673
|
+
return this.poisonError;
|
|
674
|
+
}
|
|
675
|
+
assertUsable() {
|
|
676
|
+
if (this.closed)
|
|
677
|
+
throw new Error("JsonlStorage is closed");
|
|
678
|
+
if (this.poisonError !== undefined)
|
|
679
|
+
throw this.poisonError;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
//# sourceMappingURL=storage.js.map
|