@ny4n/durable 0.0.0-stage → 1.0.1
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,670 @@
|
|
|
1
|
+
import { apply } from "@ny4n/chord/delta";
|
|
2
|
+
import { StorageRequestError } from "../../errors.js";
|
|
3
|
+
import { idFromNumber, seqFromNumber } from "../../ids.js";
|
|
4
|
+
import { nextCursor, scanStart } from "../scan.js";
|
|
5
|
+
import { applySqliteMigrations } from "./migrations.js";
|
|
6
|
+
const parseJson = (value) => JSON.parse(value);
|
|
7
|
+
const encodeJson = (value) => JSON.stringify(value);
|
|
8
|
+
// Some SQLite bindings replace lone UTF-16 surrogates. JSON encoding keeps indexed identities lossless.
|
|
9
|
+
const encodeIndexedString = (value) => JSON.stringify(value);
|
|
10
|
+
const cursorId = (cursor) => {
|
|
11
|
+
const after = cursor?.after;
|
|
12
|
+
if (after === undefined)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (typeof after !== "number" || !Number.isSafeInteger(after))
|
|
15
|
+
throw new StorageRequestError("Invalid storage cursor");
|
|
16
|
+
return idFromNumber(after);
|
|
17
|
+
};
|
|
18
|
+
const page = (values, limit, order = "ascending") => {
|
|
19
|
+
const items = values.slice(0, limit);
|
|
20
|
+
if (values.length <= limit)
|
|
21
|
+
return { items };
|
|
22
|
+
return { items, next: nextCursor(items.at(-1).id, order) };
|
|
23
|
+
};
|
|
24
|
+
/** The ID condition, its parameter, and the `ORDER BY` direction of a table scan. */
|
|
25
|
+
const scanSql = (start) => start.order === "ascending"
|
|
26
|
+
? { clause: "id > ?", param: start.after ?? -1, direction: "ASC" }
|
|
27
|
+
: { clause: "id < ?", param: start.after ?? Number.MAX_SAFE_INTEGER, direction: "DESC" };
|
|
28
|
+
const scopeColumns = (scope) => {
|
|
29
|
+
switch (scope.kind) {
|
|
30
|
+
case "session":
|
|
31
|
+
return { scopeKind: "session", ownerId: 0 };
|
|
32
|
+
case "conversation":
|
|
33
|
+
return { scopeKind: "conversation", ownerId: scope.conversationId };
|
|
34
|
+
case "task":
|
|
35
|
+
return { scopeKind: "task", ownerId: scope.taskId };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const addressParts = (address) => {
|
|
39
|
+
const scope = scopeColumns(address.scope);
|
|
40
|
+
return {
|
|
41
|
+
kind: encodeIndexedString(address.kind),
|
|
42
|
+
...scope,
|
|
43
|
+
family: address.key === undefined ? 0 : 1,
|
|
44
|
+
keyValue: encodeIndexedString(address.key ?? ""),
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
const addressKey = (address) => {
|
|
48
|
+
const parts = addressParts(address);
|
|
49
|
+
return JSON.stringify([parts.kind, parts.scopeKind, parts.ownerId, parts.family, parts.keyValue]);
|
|
50
|
+
};
|
|
51
|
+
const isAliveAt = (record, at) => {
|
|
52
|
+
if (at === "current")
|
|
53
|
+
return record.retiredAt === undefined;
|
|
54
|
+
return record.createdAt <= at && (record.retiredAt === undefined || at < record.retiredAt);
|
|
55
|
+
};
|
|
56
|
+
const isCurrentOnly = (record) => record.scope.kind !== "conversation" || record.history === "latest";
|
|
57
|
+
const writeId = (write) => {
|
|
58
|
+
switch (write.type) {
|
|
59
|
+
case "conversation":
|
|
60
|
+
case "entry":
|
|
61
|
+
case "task":
|
|
62
|
+
case "submission":
|
|
63
|
+
return write.value.id;
|
|
64
|
+
case "document.create":
|
|
65
|
+
case "document.copy":
|
|
66
|
+
return write.record.id;
|
|
67
|
+
case "document.change":
|
|
68
|
+
case "document.retire":
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
/** Portable SQLite implementation of the Pico storage contract. */
|
|
73
|
+
export class SqliteStorage {
|
|
74
|
+
db;
|
|
75
|
+
nextId;
|
|
76
|
+
closed = false;
|
|
77
|
+
closing;
|
|
78
|
+
admittedReads = 0;
|
|
79
|
+
readsDrained;
|
|
80
|
+
constructor(db, nextId) {
|
|
81
|
+
this.db = db;
|
|
82
|
+
this.nextId = nextId;
|
|
83
|
+
}
|
|
84
|
+
/** Initialize storage over an owned SQLite database facade. */
|
|
85
|
+
static async open(db) {
|
|
86
|
+
try {
|
|
87
|
+
await applySqliteMigrations(db);
|
|
88
|
+
const metadata = await db.get("SELECT next_id, next_seq FROM durable_metadata WHERE singleton = 1");
|
|
89
|
+
if (metadata === undefined)
|
|
90
|
+
throw new Error("Durable SQLite metadata is missing");
|
|
91
|
+
return new SqliteStorage(db, Number(metadata.next_id));
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
try {
|
|
95
|
+
await db.close();
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Preserve the initialization failure.
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async commit(writes, _context) {
|
|
104
|
+
this.assertOpen();
|
|
105
|
+
const documentActions = this.prepareDocumentActions(writes);
|
|
106
|
+
const candidateNextId = this.candidateNextId(writes);
|
|
107
|
+
const seq = await this.db.transaction(async (transaction) => {
|
|
108
|
+
const metadata = await transaction.get("SELECT next_id, next_seq FROM durable_metadata WHERE singleton = 1");
|
|
109
|
+
if (metadata === undefined)
|
|
110
|
+
throw new Error("Durable SQLite metadata is missing");
|
|
111
|
+
const committedSeq = seqFromNumber(metadata.next_seq);
|
|
112
|
+
await this.checkGlobalIds(transaction, writes);
|
|
113
|
+
await this.checkDocumentActions(transaction, documentActions);
|
|
114
|
+
for (const write of writes)
|
|
115
|
+
await this.applyTableWrite(transaction, write, committedSeq);
|
|
116
|
+
await this.applyDocumentActions(transaction, documentActions, committedSeq);
|
|
117
|
+
await transaction.run("UPDATE durable_metadata SET next_id = ?, next_seq = ? WHERE singleton = 1", String(Math.max(Number(metadata.next_id), candidateNextId)), committedSeq + 1);
|
|
118
|
+
return committedSeq;
|
|
119
|
+
});
|
|
120
|
+
this.nextId = Math.max(this.nextId, candidateNextId);
|
|
121
|
+
return seq;
|
|
122
|
+
}
|
|
123
|
+
async mintId() {
|
|
124
|
+
this.assertOpen();
|
|
125
|
+
if (!Number.isSafeInteger(this.nextId))
|
|
126
|
+
throw new Error("ID space is exhausted");
|
|
127
|
+
return idFromNumber(this.nextId++);
|
|
128
|
+
}
|
|
129
|
+
async conversation(id, _context) {
|
|
130
|
+
this.assertOpen();
|
|
131
|
+
const row = await this.db.get("SELECT record FROM conversations WHERE id = ?", id);
|
|
132
|
+
return row === undefined ? undefined : parseJson(row.record);
|
|
133
|
+
}
|
|
134
|
+
async scanConversations(query, limit, cursor, _context) {
|
|
135
|
+
this.assertOpen();
|
|
136
|
+
const start = scanStart(query.order, cursor, "ascending");
|
|
137
|
+
const scan = scanSql(start);
|
|
138
|
+
const clauses = [scan.clause];
|
|
139
|
+
const params = [scan.param];
|
|
140
|
+
if (query.ownerConversationId !== undefined) {
|
|
141
|
+
clauses.push("owner_conversation_id = ?");
|
|
142
|
+
params.push(query.ownerConversationId);
|
|
143
|
+
}
|
|
144
|
+
if (query.ownerTaskId !== undefined) {
|
|
145
|
+
clauses.push("owner_task_id = ?");
|
|
146
|
+
params.push(query.ownerTaskId);
|
|
147
|
+
}
|
|
148
|
+
params.push(limit + 1);
|
|
149
|
+
const rows = await this.db.all(`SELECT record FROM conversations WHERE ${clauses.join(" AND ")} ORDER BY id ${scan.direction} LIMIT ?`, ...params);
|
|
150
|
+
return page(rows.map((row) => parseJson(row.record)), limit, start.order);
|
|
151
|
+
}
|
|
152
|
+
entry(idOrConversationId, idOrContext, context) {
|
|
153
|
+
return this.admitRead(() => this.readEntry(idOrConversationId, idOrContext, context));
|
|
154
|
+
}
|
|
155
|
+
findLatestHeadMarker(conversationId, atOrBeforeEntryId, _context) {
|
|
156
|
+
return this.admitRead(() => this.readLatestHeadMarker(conversationId, atOrBeforeEntryId));
|
|
157
|
+
}
|
|
158
|
+
scanEntries(query, limit, cursor, _context) {
|
|
159
|
+
return this.admitRead(() => this.readEntries(query, limit, cursor));
|
|
160
|
+
}
|
|
161
|
+
async readEntry(idOrConversationId, idOrContext, context) {
|
|
162
|
+
const id = context === undefined
|
|
163
|
+
? idFromNumber(idOrConversationId)
|
|
164
|
+
: typeof idOrContext === "number"
|
|
165
|
+
? idFromNumber(idOrContext)
|
|
166
|
+
: undefined;
|
|
167
|
+
if (id === undefined)
|
|
168
|
+
throw new StorageRequestError("Storage.entry() requires an entry ID");
|
|
169
|
+
let conversation;
|
|
170
|
+
if (context !== undefined) {
|
|
171
|
+
const conversationId = idFromNumber(idOrConversationId);
|
|
172
|
+
conversation = await this.readConversation(conversationId);
|
|
173
|
+
if (conversation === undefined)
|
|
174
|
+
throw new StorageRequestError(`Unknown conversation: ${conversationId}`);
|
|
175
|
+
}
|
|
176
|
+
const row = await this.db.get("SELECT record, commit_seq FROM entries WHERE id = ?", id);
|
|
177
|
+
if (row === undefined)
|
|
178
|
+
return undefined;
|
|
179
|
+
const entry = parseJson(row.record);
|
|
180
|
+
if (conversation !== undefined) {
|
|
181
|
+
let upperEntryId = Number.POSITIVE_INFINITY;
|
|
182
|
+
while (conversation.id !== entry.conversationId) {
|
|
183
|
+
if (conversation.parent === undefined)
|
|
184
|
+
return undefined;
|
|
185
|
+
upperEntryId = Math.min(upperEntryId, conversation.parent.at);
|
|
186
|
+
conversation = (await this.readConversation(conversation.parent.conversationId));
|
|
187
|
+
}
|
|
188
|
+
if (entry.id > upperEntryId)
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
return { entry, commitSeq: seqFromNumber(row.commit_seq) };
|
|
192
|
+
}
|
|
193
|
+
async readLatestHeadMarker(conversationId, atOrBeforeEntryId) {
|
|
194
|
+
let conversation = await this.readConversation(conversationId);
|
|
195
|
+
if (conversation === undefined)
|
|
196
|
+
throw new StorageRequestError(`Unknown conversation: ${conversationId}`);
|
|
197
|
+
let upper = atOrBeforeEntryId;
|
|
198
|
+
while (true) {
|
|
199
|
+
const row = upper === undefined
|
|
200
|
+
? await this.db.get("SELECT record FROM entries WHERE conversation_id = ? AND head IS NOT NULL ORDER BY id DESC LIMIT 1", conversation.id)
|
|
201
|
+
: await this.db.get("SELECT record FROM entries WHERE conversation_id = ? AND head IS NOT NULL AND id <= ? ORDER BY id DESC LIMIT 1", conversation.id, upper);
|
|
202
|
+
if (row !== undefined)
|
|
203
|
+
return parseJson(row.record);
|
|
204
|
+
if (conversation.parent === undefined)
|
|
205
|
+
return undefined;
|
|
206
|
+
upper = upper === undefined ? conversation.parent.at : Math.min(upper, conversation.parent.at);
|
|
207
|
+
conversation = (await this.readConversation(conversation.parent.conversationId));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async readEntries(query, limit, cursor) {
|
|
211
|
+
const { order, after } = scanStart(query.order, cursor, "descending");
|
|
212
|
+
if (order === "ascending")
|
|
213
|
+
return this.readEntriesAscending(query, limit, after);
|
|
214
|
+
let conversation = await this.readConversation(query.conversationId);
|
|
215
|
+
if (conversation === undefined)
|
|
216
|
+
throw new StorageRequestError(`Unknown conversation: ${query.conversationId}`);
|
|
217
|
+
let upper = query.maxEntryId;
|
|
218
|
+
if (after !== undefined)
|
|
219
|
+
upper = Math.min(upper ?? Number.MAX_SAFE_INTEGER, after - 1);
|
|
220
|
+
const values = [];
|
|
221
|
+
while (true) {
|
|
222
|
+
const clauses = ["conversation_id = ?"];
|
|
223
|
+
const params = [conversation.id];
|
|
224
|
+
if (query.minEntryId !== undefined) {
|
|
225
|
+
clauses.push("id >= ?");
|
|
226
|
+
params.push(query.minEntryId);
|
|
227
|
+
}
|
|
228
|
+
if (upper !== undefined) {
|
|
229
|
+
clauses.push("id <= ?");
|
|
230
|
+
params.push(upper);
|
|
231
|
+
}
|
|
232
|
+
params.push(limit + 1 - values.length);
|
|
233
|
+
const rows = await this.db.all(`SELECT record FROM entries WHERE ${clauses.join(" AND ")} ORDER BY id DESC LIMIT ?`, ...params);
|
|
234
|
+
values.push(...rows.map((row) => parseJson(row.record)));
|
|
235
|
+
if (values.length > limit || conversation.parent === undefined)
|
|
236
|
+
break;
|
|
237
|
+
upper = upper === undefined ? conversation.parent.at : Math.min(upper, conversation.parent.at);
|
|
238
|
+
if (query.minEntryId !== undefined && upper < query.minEntryId)
|
|
239
|
+
break;
|
|
240
|
+
conversation = (await this.readConversation(conversation.parent.conversationId));
|
|
241
|
+
}
|
|
242
|
+
return page(values, limit, "descending");
|
|
243
|
+
}
|
|
244
|
+
/** Oldest first: the fork chain's segments from the root conversation forward, each up to its fork point. */
|
|
245
|
+
async readEntriesAscending(query, limit, after) {
|
|
246
|
+
const segments = [];
|
|
247
|
+
let conversation = await this.readConversation(query.conversationId);
|
|
248
|
+
if (conversation === undefined)
|
|
249
|
+
throw new StorageRequestError(`Unknown conversation: ${query.conversationId}`);
|
|
250
|
+
let upper = query.maxEntryId;
|
|
251
|
+
while (true) {
|
|
252
|
+
segments.push({ conversationId: conversation.id, upper });
|
|
253
|
+
if (conversation.parent === undefined)
|
|
254
|
+
break;
|
|
255
|
+
upper = upper === undefined ? conversation.parent.at : Math.min(upper, conversation.parent.at);
|
|
256
|
+
if (query.minEntryId !== undefined && upper < query.minEntryId)
|
|
257
|
+
break;
|
|
258
|
+
conversation = (await this.readConversation(conversation.parent.conversationId));
|
|
259
|
+
}
|
|
260
|
+
let lower = query.minEntryId;
|
|
261
|
+
if (after !== undefined)
|
|
262
|
+
lower = Math.max(lower ?? after + 1, after + 1);
|
|
263
|
+
const values = [];
|
|
264
|
+
for (const segment of segments.reverse()) {
|
|
265
|
+
const clauses = ["conversation_id = ?"];
|
|
266
|
+
const params = [segment.conversationId];
|
|
267
|
+
if (lower !== undefined) {
|
|
268
|
+
clauses.push("id >= ?");
|
|
269
|
+
params.push(lower);
|
|
270
|
+
}
|
|
271
|
+
if (segment.upper !== undefined) {
|
|
272
|
+
clauses.push("id <= ?");
|
|
273
|
+
params.push(segment.upper);
|
|
274
|
+
}
|
|
275
|
+
params.push(limit + 1 - values.length);
|
|
276
|
+
const rows = await this.db.all(`SELECT record FROM entries WHERE ${clauses.join(" AND ")} ORDER BY id ASC LIMIT ?`, ...params);
|
|
277
|
+
values.push(...rows.map((row) => parseJson(row.record)));
|
|
278
|
+
if (values.length > limit)
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
return page(values, limit, "ascending");
|
|
282
|
+
}
|
|
283
|
+
async task(id, _context) {
|
|
284
|
+
this.assertOpen();
|
|
285
|
+
const row = await this.db.get("SELECT record FROM tasks WHERE id = ?", id);
|
|
286
|
+
return row === undefined ? undefined : parseJson(row.record);
|
|
287
|
+
}
|
|
288
|
+
async scanTasks(query, limit, cursor, _context) {
|
|
289
|
+
this.assertOpen();
|
|
290
|
+
const start = scanStart(query.order, cursor, "ascending");
|
|
291
|
+
const scan = scanSql(start);
|
|
292
|
+
const clauses = [scan.clause];
|
|
293
|
+
const params = [scan.param];
|
|
294
|
+
if (query.conversationId !== undefined) {
|
|
295
|
+
clauses.push("conversation_id = ?");
|
|
296
|
+
params.push(query.conversationId);
|
|
297
|
+
}
|
|
298
|
+
if (query.kind !== undefined) {
|
|
299
|
+
clauses.push("kind = ?");
|
|
300
|
+
params.push(encodeIndexedString(query.kind));
|
|
301
|
+
}
|
|
302
|
+
if (query.status !== undefined) {
|
|
303
|
+
clauses.push("status = ?");
|
|
304
|
+
params.push(query.status);
|
|
305
|
+
}
|
|
306
|
+
if (query.abortRequested !== undefined) {
|
|
307
|
+
clauses.push("abort_requested = ?");
|
|
308
|
+
params.push(query.abortRequested ? 1 : 0);
|
|
309
|
+
}
|
|
310
|
+
if (query.background !== undefined) {
|
|
311
|
+
clauses.push("background = ?");
|
|
312
|
+
params.push(query.background ? 1 : 0);
|
|
313
|
+
}
|
|
314
|
+
params.push(limit + 1);
|
|
315
|
+
const rows = await this.db.all(`SELECT record FROM tasks WHERE ${clauses.join(" AND ")} ORDER BY id ${scan.direction} LIMIT ?`, ...params);
|
|
316
|
+
return page(rows.map((row) => parseJson(row.record)), limit, start.order);
|
|
317
|
+
}
|
|
318
|
+
async submission(id, _context) {
|
|
319
|
+
this.assertOpen();
|
|
320
|
+
const row = await this.db.get("SELECT record FROM submissions WHERE id = ?", id);
|
|
321
|
+
return row === undefined ? undefined : parseJson(row.record);
|
|
322
|
+
}
|
|
323
|
+
async scanSubmissions(query, limit, cursor, _context) {
|
|
324
|
+
this.assertOpen();
|
|
325
|
+
const start = scanStart(query.order, cursor, "ascending");
|
|
326
|
+
const scan = scanSql(start);
|
|
327
|
+
const clauses = [scan.clause];
|
|
328
|
+
const params = [scan.param];
|
|
329
|
+
if (query.conversationId !== undefined) {
|
|
330
|
+
clauses.push("conversation_id = ?");
|
|
331
|
+
params.push(query.conversationId);
|
|
332
|
+
}
|
|
333
|
+
if (query.status !== undefined) {
|
|
334
|
+
clauses.push("status = ?");
|
|
335
|
+
params.push(query.status);
|
|
336
|
+
}
|
|
337
|
+
params.push(limit + 1);
|
|
338
|
+
const rows = await this.db.all(`SELECT record FROM submissions WHERE ${clauses.join(" AND ")} ORDER BY id ${scan.direction} LIMIT ?`, ...params);
|
|
339
|
+
return page(rows.map((row) => parseJson(row.record)), limit, start.order);
|
|
340
|
+
}
|
|
341
|
+
async submissionByRequest(conversationId, requestId, _context) {
|
|
342
|
+
this.assertOpen();
|
|
343
|
+
const row = await this.db.get("SELECT record FROM submissions WHERE conversation_id = ? AND request_id = ?", conversationId, encodeIndexedString(requestId));
|
|
344
|
+
return row === undefined ? undefined : parseJson(row.record);
|
|
345
|
+
}
|
|
346
|
+
async findDocument(address, at, _context) {
|
|
347
|
+
this.assertOpen();
|
|
348
|
+
const parts = addressParts(address);
|
|
349
|
+
const sql = at === "current"
|
|
350
|
+
? `SELECT record FROM documents
|
|
351
|
+
WHERE kind = ? AND scope_kind = ? AND owner_id = ? AND family = ? AND key_value = ?
|
|
352
|
+
AND retired_at IS NULL ORDER BY created_at DESC LIMIT 1`
|
|
353
|
+
: `SELECT record FROM documents
|
|
354
|
+
WHERE kind = ? AND scope_kind = ? AND owner_id = ? AND family = ? AND key_value = ?
|
|
355
|
+
AND created_at <= ? AND (retired_at IS NULL OR retired_at > ?)
|
|
356
|
+
ORDER BY created_at DESC LIMIT 1`;
|
|
357
|
+
const params = [parts.kind, parts.scopeKind, parts.ownerId, parts.family, parts.keyValue];
|
|
358
|
+
if (at !== "current")
|
|
359
|
+
params.push(at, at);
|
|
360
|
+
const row = await this.db.get(sql, ...params);
|
|
361
|
+
return row === undefined ? undefined : parseJson(row.record);
|
|
362
|
+
}
|
|
363
|
+
async document(id, at, _context) {
|
|
364
|
+
this.assertOpen();
|
|
365
|
+
// The record and revision queries must observe one committed state; a commit between them can replace the base.
|
|
366
|
+
return this.db.transaction((transaction) => this.materializeDocument(transaction, id, at));
|
|
367
|
+
}
|
|
368
|
+
async scanDocuments(query, limit, cursor, _context) {
|
|
369
|
+
this.assertOpen();
|
|
370
|
+
const scope = scopeColumns(query.scope);
|
|
371
|
+
const clauses = ["scope_kind = ?", "owner_id = ?", "id > ?"];
|
|
372
|
+
const params = [scope.scopeKind, scope.ownerId, cursorId(cursor) ?? -1];
|
|
373
|
+
if (query.kind !== undefined) {
|
|
374
|
+
clauses.push("kind = ?");
|
|
375
|
+
params.push(encodeIndexedString(query.kind));
|
|
376
|
+
}
|
|
377
|
+
if (query.at === "current") {
|
|
378
|
+
clauses.push("retired_at IS NULL");
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
clauses.push("created_at <= ?", "(retired_at IS NULL OR retired_at > ?)");
|
|
382
|
+
params.push(query.at, query.at);
|
|
383
|
+
}
|
|
384
|
+
params.push(limit + 1);
|
|
385
|
+
const rows = await this.db.all(`SELECT record FROM documents WHERE ${clauses.join(" AND ")} ORDER BY id LIMIT ?`, ...params);
|
|
386
|
+
return page(rows.map((row) => parseJson(row.record)), limit);
|
|
387
|
+
}
|
|
388
|
+
close(_context) {
|
|
389
|
+
if (this.closing === undefined) {
|
|
390
|
+
this.closed = true;
|
|
391
|
+
this.closing = this.closeDatabase();
|
|
392
|
+
}
|
|
393
|
+
return this.closing;
|
|
394
|
+
}
|
|
395
|
+
async closeDatabase() {
|
|
396
|
+
if (this.admittedReads > 0) {
|
|
397
|
+
await new Promise((resolve) => {
|
|
398
|
+
this.readsDrained = resolve;
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
await this.db.close();
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Run a read that issues several queries. Close waits for admitted reads, so their later queries never reach a
|
|
405
|
+
* closed database. Single-query reads and transactions are already ordered before close by the database.
|
|
406
|
+
*/
|
|
407
|
+
async admitRead(read) {
|
|
408
|
+
this.assertOpen();
|
|
409
|
+
this.admittedReads++;
|
|
410
|
+
try {
|
|
411
|
+
return await read();
|
|
412
|
+
}
|
|
413
|
+
finally {
|
|
414
|
+
if (--this.admittedReads === 0)
|
|
415
|
+
this.readsDrained?.();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async readConversation(id) {
|
|
419
|
+
const row = await this.db.get("SELECT record FROM conversations WHERE id = ?", id);
|
|
420
|
+
return row === undefined ? undefined : parseJson(row.record);
|
|
421
|
+
}
|
|
422
|
+
async materializeDocument(executor, id, at) {
|
|
423
|
+
const row = await executor.get("SELECT record FROM documents WHERE id = ?", id);
|
|
424
|
+
if (row === undefined)
|
|
425
|
+
return undefined;
|
|
426
|
+
const record = parseJson(row.record);
|
|
427
|
+
if (at !== "current" && isCurrentOnly(record)) {
|
|
428
|
+
throw new StorageRequestError(`Document ${id} does not retain historical content`);
|
|
429
|
+
}
|
|
430
|
+
if (!isAliveAt(record, at))
|
|
431
|
+
return undefined;
|
|
432
|
+
const upper = at === "current" ? Number.MAX_SAFE_INTEGER : at;
|
|
433
|
+
const base = await executor.get(`SELECT seq, kind, version, content FROM document_revisions
|
|
434
|
+
WHERE document_id = ? AND kind = 'base' AND seq <= ? ORDER BY seq DESC LIMIT 1`, id, upper);
|
|
435
|
+
if (base === undefined)
|
|
436
|
+
throw new Error(`Document ${id} is missing a required base`);
|
|
437
|
+
let value = parseJson(base.content);
|
|
438
|
+
const tail = await executor.all(`SELECT seq, kind, version, content FROM document_revisions
|
|
439
|
+
WHERE document_id = ? AND seq > ? AND seq <= ? ORDER BY seq`, id, base.seq, upper);
|
|
440
|
+
for (const revision of tail) {
|
|
441
|
+
if (revision.kind !== "delta" || revision.version !== base.version) {
|
|
442
|
+
throw new Error(`Document ${id} crosses a stored version boundary without a base`);
|
|
443
|
+
}
|
|
444
|
+
value = apply(value, parseJson(revision.content));
|
|
445
|
+
}
|
|
446
|
+
return { record, version: base.version, value, deltasSinceBase: tail.length };
|
|
447
|
+
}
|
|
448
|
+
candidateNextId(writes) {
|
|
449
|
+
let nextId = this.nextId;
|
|
450
|
+
for (const write of writes) {
|
|
451
|
+
const id = writeId(write);
|
|
452
|
+
if (id !== undefined)
|
|
453
|
+
nextId = Math.max(nextId, id + 1);
|
|
454
|
+
}
|
|
455
|
+
return nextId;
|
|
456
|
+
}
|
|
457
|
+
async checkGlobalIds(executor, writes) {
|
|
458
|
+
const claimed = new Map();
|
|
459
|
+
for (const write of writes) {
|
|
460
|
+
if (write.type === "document.change" || write.type === "document.retire")
|
|
461
|
+
continue;
|
|
462
|
+
const document = write.type === "document.create" || write.type === "document.copy";
|
|
463
|
+
const table = document ? "document" : write.type;
|
|
464
|
+
const id = document ? write.record.id : write.value.id;
|
|
465
|
+
const existing = (await executor.get("SELECT record_type FROM record_ids WHERE id = ?", id))
|
|
466
|
+
?.record_type;
|
|
467
|
+
const earlier = claimed.get(id);
|
|
468
|
+
if (table === "conversation" || table === "entry" || table === "document") {
|
|
469
|
+
if (existing !== undefined)
|
|
470
|
+
throw new Error(`ID ${id} already belongs to ${existing}`);
|
|
471
|
+
if (earlier !== undefined)
|
|
472
|
+
throw new Error(`ID ${id} is written more than once`);
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
if (existing !== undefined && existing !== table)
|
|
476
|
+
throw new Error(`ID ${id} already belongs to ${existing}`);
|
|
477
|
+
if (earlier !== undefined && earlier !== table)
|
|
478
|
+
throw new Error(`ID ${id} is written as two record types`);
|
|
479
|
+
}
|
|
480
|
+
claimed.set(id, table);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
prepareDocumentActions(writes) {
|
|
484
|
+
const actions = new Map();
|
|
485
|
+
for (const write of writes) {
|
|
486
|
+
if (write.type !== "document.create" &&
|
|
487
|
+
write.type !== "document.copy" &&
|
|
488
|
+
write.type !== "document.change" &&
|
|
489
|
+
write.type !== "document.retire") {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const id = write.type === "document.create" || write.type === "document.copy" ? write.record.id : write.id;
|
|
493
|
+
let action = actions.get(id);
|
|
494
|
+
if (action === undefined) {
|
|
495
|
+
action = { retire: false };
|
|
496
|
+
actions.set(id, action);
|
|
497
|
+
}
|
|
498
|
+
switch (write.type) {
|
|
499
|
+
case "document.create":
|
|
500
|
+
if (action.create !== undefined || action.content !== undefined || action.copy !== undefined) {
|
|
501
|
+
throw new Error(`Document ${id} has more than one content command`);
|
|
502
|
+
}
|
|
503
|
+
action.create = write.record;
|
|
504
|
+
action.content = write.content;
|
|
505
|
+
break;
|
|
506
|
+
case "document.copy":
|
|
507
|
+
if (action.create !== undefined || action.content !== undefined || action.copy !== undefined) {
|
|
508
|
+
throw new Error(`Document ${id} has more than one content command`);
|
|
509
|
+
}
|
|
510
|
+
action.create = write.record;
|
|
511
|
+
action.copy = write.source;
|
|
512
|
+
break;
|
|
513
|
+
case "document.change":
|
|
514
|
+
if (action.content !== undefined || action.copy !== undefined) {
|
|
515
|
+
throw new Error(`Document ${id} has more than one content command`);
|
|
516
|
+
}
|
|
517
|
+
action.content = write.content;
|
|
518
|
+
break;
|
|
519
|
+
case "document.retire":
|
|
520
|
+
if (action.retire)
|
|
521
|
+
throw new Error(`Document ${id} is retired more than once`);
|
|
522
|
+
action.retire = true;
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return actions;
|
|
527
|
+
}
|
|
528
|
+
async checkDocumentActions(executor, actions) {
|
|
529
|
+
const liveCounts = new Map();
|
|
530
|
+
for (const [id, action] of actions) {
|
|
531
|
+
if (action.copy !== undefined && actions.has(action.copy.id)) {
|
|
532
|
+
throw new Error(`Document copy ${id} source is changed in the copy batch`);
|
|
533
|
+
}
|
|
534
|
+
const row = await executor.get("SELECT record FROM documents WHERE id = ?", id);
|
|
535
|
+
const existing = row === undefined ? undefined : parseJson(row.record);
|
|
536
|
+
if (action.create === undefined && existing === undefined)
|
|
537
|
+
throw new Error(`Unknown document: ${id}`);
|
|
538
|
+
if (action.create !== undefined && existing !== undefined)
|
|
539
|
+
throw new Error(`Document ${id} already exists`);
|
|
540
|
+
if (existing?.retiredAt !== undefined)
|
|
541
|
+
throw new Error(`Document ${id} is retired`);
|
|
542
|
+
if (action.content?.kind === "delta") {
|
|
543
|
+
const previous = await executor.get("SELECT version FROM document_revisions WHERE document_id = ? ORDER BY seq DESC LIMIT 1", id);
|
|
544
|
+
if (previous === undefined)
|
|
545
|
+
throw new Error(`Document ${id} delta has no base`);
|
|
546
|
+
if (previous.version !== action.content.version) {
|
|
547
|
+
throw new Error(`Document ${id} version transition requires a base`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const record = action.create ?? existing;
|
|
551
|
+
const key = addressKey(record);
|
|
552
|
+
let live = liveCounts.get(key);
|
|
553
|
+
if (live === undefined)
|
|
554
|
+
live = (await this.currentDocumentId(executor, record)) === undefined ? 0 : 1;
|
|
555
|
+
if (action.retire && existing !== undefined)
|
|
556
|
+
live--;
|
|
557
|
+
if (action.create !== undefined && !action.retire)
|
|
558
|
+
live++;
|
|
559
|
+
liveCounts.set(key, live);
|
|
560
|
+
}
|
|
561
|
+
for (const live of liveCounts.values()) {
|
|
562
|
+
if (live > 1)
|
|
563
|
+
throw new Error("Document address already has a current incarnation");
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
async currentDocumentId(executor, address) {
|
|
567
|
+
const parts = addressParts(address);
|
|
568
|
+
const id = (await executor.get(`SELECT id FROM documents
|
|
569
|
+
WHERE kind = ? AND scope_kind = ? AND owner_id = ? AND family = ? AND key_value = ? AND retired_at IS NULL
|
|
570
|
+
LIMIT 1`, parts.kind, parts.scopeKind, parts.ownerId, parts.family, parts.keyValue))?.id;
|
|
571
|
+
return id === undefined ? undefined : idFromNumber(id);
|
|
572
|
+
}
|
|
573
|
+
async applyTableWrite(executor, write, seq) {
|
|
574
|
+
switch (write.type) {
|
|
575
|
+
case "conversation":
|
|
576
|
+
await this.claimId(executor, write.value.id, "conversation");
|
|
577
|
+
await executor.run("INSERT INTO conversations (id, owner_conversation_id, owner_task_id, record) VALUES (?, ?, ?, ?)", write.value.id, write.value.owner?.conversationId ?? null, write.value.owner?.taskId ?? null, encodeJson(write.value));
|
|
578
|
+
break;
|
|
579
|
+
case "entry":
|
|
580
|
+
await this.claimId(executor, write.value.id, "entry");
|
|
581
|
+
await executor.run("INSERT INTO entries (id, conversation_id, head, commit_seq, record) VALUES (?, ?, ?, ?, ?)", write.value.id, write.value.conversationId, write.value.head ?? null, seq, encodeJson(write.value));
|
|
582
|
+
break;
|
|
583
|
+
case "task":
|
|
584
|
+
await this.claimId(executor, write.value.id, "task");
|
|
585
|
+
await executor.run(`INSERT INTO tasks (id, conversation_id, kind, status, abort_requested, background, record)
|
|
586
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
587
|
+
ON CONFLICT(id) DO UPDATE SET conversation_id = excluded.conversation_id, kind = excluded.kind,
|
|
588
|
+
status = excluded.status, abort_requested = excluded.abort_requested,
|
|
589
|
+
background = excluded.background, record = excluded.record`, write.value.id, write.value.conversationId, encodeIndexedString(write.value.kind), write.value.state.status, write.value.abortRequested ? 1 : 0, write.value.background ? 1 : 0, encodeJson(write.value));
|
|
590
|
+
break;
|
|
591
|
+
case "submission":
|
|
592
|
+
await this.claimId(executor, write.value.id, "submission");
|
|
593
|
+
await executor.run(`INSERT INTO submissions (id, conversation_id, request_id, status, record) VALUES (?, ?, ?, ?, ?)
|
|
594
|
+
ON CONFLICT(id) DO UPDATE SET conversation_id = excluded.conversation_id,
|
|
595
|
+
request_id = excluded.request_id, status = excluded.status, record = excluded.record`, write.value.id, write.value.conversationId, write.value.requestId === undefined ? null : encodeIndexedString(write.value.requestId), write.value.status, encodeJson(write.value));
|
|
596
|
+
break;
|
|
597
|
+
case "document.create":
|
|
598
|
+
case "document.copy":
|
|
599
|
+
case "document.change":
|
|
600
|
+
case "document.retire":
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
async claimId(executor, id, table) {
|
|
605
|
+
await executor.run("INSERT OR IGNORE INTO record_ids (id, record_type) VALUES (?, ?)", id, table);
|
|
606
|
+
}
|
|
607
|
+
async applyDocumentActions(executor, actions, seq) {
|
|
608
|
+
for (const [id, action] of actions) {
|
|
609
|
+
let content = action.content;
|
|
610
|
+
if (action.copy !== undefined) {
|
|
611
|
+
try {
|
|
612
|
+
const stored = await this.materializeDocument(executor, action.copy.id, action.copy.at);
|
|
613
|
+
if (stored === undefined)
|
|
614
|
+
throw new Error(`Fork source document ${action.copy.id} cannot be read`);
|
|
615
|
+
const create = action.create;
|
|
616
|
+
if (stored.record.scope.kind !== "conversation" ||
|
|
617
|
+
create.scope.kind !== "conversation" ||
|
|
618
|
+
stored.record.kind !== create.kind ||
|
|
619
|
+
stored.record.key !== create.key ||
|
|
620
|
+
stored.record.history !== create.history ||
|
|
621
|
+
stored.record.fork !== create.fork) {
|
|
622
|
+
throw new Error(`Fork source document ${action.copy.id} does not match the copied record`);
|
|
623
|
+
}
|
|
624
|
+
content = { kind: "base", version: stored.version, value: stored.value };
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
throw new Error(`Document copy ${id} was rejected`, { cause: error });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
let record;
|
|
631
|
+
if (action.create !== undefined) {
|
|
632
|
+
record = {
|
|
633
|
+
...action.create,
|
|
634
|
+
createdAt: seq,
|
|
635
|
+
...(action.retire ? { retiredAt: seq } : {}),
|
|
636
|
+
};
|
|
637
|
+
const parts = addressParts(record);
|
|
638
|
+
await this.claimId(executor, id, "document");
|
|
639
|
+
await executor.run(`INSERT INTO documents
|
|
640
|
+
(id, kind, family, key_value, scope_kind, owner_id, created_at, retired_at, record)
|
|
641
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, parts.kind, parts.family, parts.keyValue, parts.scopeKind, parts.ownerId, seq, action.retire ? seq : null, encodeJson(record));
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
const row = (await executor.get("SELECT record FROM documents WHERE id = ?", id));
|
|
645
|
+
record = parseJson(row.record);
|
|
646
|
+
}
|
|
647
|
+
if (content !== undefined) {
|
|
648
|
+
if (content.kind === "base" && isCurrentOnly(record)) {
|
|
649
|
+
await executor.run("DELETE FROM document_revisions WHERE document_id = ?", id);
|
|
650
|
+
}
|
|
651
|
+
const encodedContent = content.kind === "base" ? encodeJson(content.value) : encodeJson(content.ops);
|
|
652
|
+
await executor.run("INSERT INTO document_revisions (document_id, seq, kind, version, content) VALUES (?, ?, ?, ?, ?)", id, seq, content.kind, content.version, encodedContent);
|
|
653
|
+
}
|
|
654
|
+
if (action.retire) {
|
|
655
|
+
if (action.create === undefined) {
|
|
656
|
+
record = { ...record, retiredAt: seq };
|
|
657
|
+
await executor.run("UPDATE documents SET retired_at = ?, record = ? WHERE id = ?", seq, encodeJson(record), id);
|
|
658
|
+
}
|
|
659
|
+
if (isCurrentOnly(record)) {
|
|
660
|
+
await executor.run("DELETE FROM document_revisions WHERE document_id = ?", id);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
assertOpen() {
|
|
666
|
+
if (this.closed)
|
|
667
|
+
throw new Error("SqliteStorage is closed");
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
//# sourceMappingURL=storage.js.map
|