@earendil-works/pi-durable 1.4.0 → 1.5.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/CHANGELOG.md +204 -0
- package/README.md +195 -85
- package/dist/codemode/codemode.d.ts +6 -5
- package/dist/codemode/codemode.d.ts.map +1 -1
- package/dist/codemode/codemode.js +10 -49
- package/dist/codemode/codemode.js.map +1 -1
- package/dist/codemode/models.d.ts +2 -1
- package/dist/codemode/models.d.ts.map +1 -1
- package/dist/codemode/models.js +4 -4
- package/dist/codemode/models.js.map +1 -1
- package/dist/documents.d.ts +1 -1
- package/dist/documents.d.ts.map +1 -1
- package/dist/documents.js +1 -1
- package/dist/documents.js.map +1 -1
- package/dist/entries.d.ts +1 -1
- package/dist/entries.d.ts.map +1 -1
- package/dist/entries.js.map +1 -1
- package/dist/env/index.d.ts +13 -11
- package/dist/env/index.d.ts.map +1 -1
- package/dist/env/index.js.map +1 -1
- package/dist/env/node-watch.d.ts +4 -4
- package/dist/env/node-watch.d.ts.map +1 -1
- package/dist/env/node-watch.js +18 -16
- package/dist/env/node-watch.js.map +1 -1
- package/dist/env/node.d.ts +3 -3
- package/dist/env/node.d.ts.map +1 -1
- package/dist/env/node.js +1 -1
- package/dist/env/node.js.map +1 -1
- package/dist/errors.d.ts +18 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +26 -3
- package/dist/errors.js.map +1 -1
- package/dist/harness/agent.d.ts.map +1 -1
- package/dist/harness/agent.js +37 -3
- package/dist/harness/agent.js.map +1 -1
- package/dist/harness/compaction.d.ts +8 -3
- package/dist/harness/compaction.d.ts.map +1 -1
- package/dist/harness/compaction.js +40 -11
- package/dist/harness/compaction.js.map +1 -1
- package/dist/harness/context.d.ts +24 -8
- package/dist/harness/context.d.ts.map +1 -1
- package/dist/harness/context.js +71 -25
- package/dist/harness/context.js.map +1 -1
- package/dist/harness/define.d.ts +8 -4
- package/dist/harness/define.d.ts.map +1 -1
- package/dist/harness/define.js +4 -0
- package/dist/harness/define.js.map +1 -1
- package/dist/harness/generation.d.ts +39 -6
- package/dist/harness/generation.d.ts.map +1 -1
- package/dist/harness/generation.js +451 -100
- package/dist/harness/generation.js.map +1 -1
- package/dist/harness/harness.d.ts +3 -1
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +87 -62
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/inbox.d.ts +7 -2
- package/dist/harness/inbox.d.ts.map +1 -1
- package/dist/harness/inbox.js +5 -2
- package/dist/harness/inbox.js.map +1 -1
- package/dist/harness/json.d.ts +21 -5
- package/dist/harness/json.d.ts.map +1 -1
- package/dist/harness/json.js +23 -5
- package/dist/harness/json.js.map +1 -1
- package/dist/harness/live.d.ts +124 -17
- package/dist/harness/live.d.ts.map +1 -1
- package/dist/harness/live.js +93 -12
- package/dist/harness/live.js.map +1 -1
- package/dist/harness/partial-changes.d.ts +54 -0
- package/dist/harness/partial-changes.d.ts.map +1 -0
- package/dist/harness/partial-changes.js +163 -0
- package/dist/harness/partial-changes.js.map +1 -0
- package/dist/harness/prompt.d.ts +1 -1
- package/dist/harness/prompt.d.ts.map +1 -1
- package/dist/harness/prompt.js.map +1 -1
- package/dist/harness/provider.d.ts +1 -1
- package/dist/harness/provider.d.ts.map +1 -1
- package/dist/harness/provider.js.map +1 -1
- package/dist/harness/registry-reader.d.ts +11 -0
- package/dist/harness/registry-reader.d.ts.map +1 -0
- package/dist/harness/registry-reader.js +25 -0
- package/dist/harness/registry-reader.js.map +1 -0
- package/dist/harness/scheduler.d.ts +48 -20
- package/dist/harness/scheduler.d.ts.map +1 -1
- package/dist/harness/scheduler.js +1006 -380
- package/dist/harness/scheduler.js.map +1 -1
- package/dist/harness/submissions.d.ts +18 -11
- package/dist/harness/submissions.d.ts.map +1 -1
- package/dist/harness/submissions.js +63 -26
- package/dist/harness/submissions.js.map +1 -1
- package/dist/harness/task-graph.d.ts +14 -9
- package/dist/harness/task-graph.d.ts.map +1 -1
- package/dist/harness/task-graph.js +38 -28
- package/dist/harness/task-graph.js.map +1 -1
- package/dist/harness/telemetry.d.ts +11 -11
- package/dist/harness/telemetry.d.ts.map +1 -1
- package/dist/harness/telemetry.js +7 -7
- package/dist/harness/telemetry.js.map +1 -1
- package/dist/harness/tool.d.ts +3 -1
- package/dist/harness/tool.d.ts.map +1 -1
- package/dist/harness/tool.js +172 -56
- package/dist/harness/tool.js.map +1 -1
- package/dist/harness/transcript.d.ts +72 -0
- package/dist/harness/transcript.d.ts.map +1 -0
- package/dist/harness/transcript.js +264 -0
- package/dist/harness/transcript.js.map +1 -0
- package/dist/harness/types.d.ts +237 -61
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/harness/usage.d.ts +2 -1
- package/dist/harness/usage.d.ts.map +1 -1
- package/dist/harness/usage.js +1 -1
- package/dist/harness/usage.js.map +1 -1
- package/dist/harness/util.d.ts +4 -2
- package/dist/harness/util.d.ts.map +1 -1
- package/dist/harness/util.js +9 -0
- package/dist/harness/util.js.map +1 -1
- package/dist/harness/view.d.ts +44 -22
- package/dist/harness/view.d.ts.map +1 -1
- package/dist/harness/view.js +130 -71
- package/dist/harness/view.js.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/search.d.ts +19 -3
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +107 -44
- package/dist/search.js.map +1 -1
- package/dist/session/cached-storage.d.ts +72 -0
- package/dist/session/cached-storage.d.ts.map +1 -0
- package/dist/session/cached-storage.js +269 -0
- package/dist/session/cached-storage.js.map +1 -0
- package/dist/session/conversation-tails.d.ts +33 -0
- package/dist/session/conversation-tails.d.ts.map +1 -0
- package/dist/session/conversation-tails.js +101 -0
- package/dist/session/conversation-tails.js.map +1 -0
- package/dist/session/forks.d.ts +1 -1
- package/dist/session/forks.d.ts.map +1 -1
- package/dist/session/forks.js.map +1 -1
- package/dist/session/keys.d.ts +69 -0
- package/dist/session/keys.d.ts.map +1 -0
- package/dist/session/keys.js +161 -0
- package/dist/session/keys.js.map +1 -0
- package/dist/session/session.d.ts +169 -26
- package/dist/session/session.d.ts.map +1 -1
- package/dist/session/session.js +1029 -171
- package/dist/session/session.js.map +1 -1
- package/dist/session/transaction.d.ts +84 -5
- package/dist/session/transaction.d.ts.map +1 -1
- package/dist/session/transaction.js +227 -40
- package/dist/session/transaction.js.map +1 -1
- package/dist/session/writer.d.ts +74 -0
- package/dist/session/writer.d.ts.map +1 -0
- package/dist/session/writer.js +294 -0
- package/dist/session/writer.js.map +1 -0
- package/dist/storage/indexeddb.d.ts +12 -3
- package/dist/storage/indexeddb.d.ts.map +1 -1
- package/dist/storage/indexeddb.js +91 -41
- package/dist/storage/indexeddb.js.map +1 -1
- package/dist/storage/jsonl/index.d.ts +1 -1
- package/dist/storage/jsonl/index.d.ts.map +1 -1
- package/dist/storage/jsonl/index.js +1 -1
- package/dist/storage/jsonl/index.js.map +1 -1
- package/dist/storage/jsonl/node.d.ts +3 -3
- package/dist/storage/jsonl/node.d.ts.map +1 -1
- package/dist/storage/jsonl/node.js +2 -2
- package/dist/storage/jsonl/node.js.map +1 -1
- package/dist/storage/jsonl/storage.d.ts +46 -5
- package/dist/storage/jsonl/storage.d.ts.map +1 -1
- package/dist/storage/jsonl/storage.js +158 -43
- package/dist/storage/jsonl/storage.js.map +1 -1
- package/dist/storage/memory.d.ts +9 -3
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +28 -2
- package/dist/storage/memory.js.map +1 -1
- package/dist/storage/sqlite/commit.d.ts +52 -0
- package/dist/storage/sqlite/commit.d.ts.map +1 -0
- package/dist/storage/sqlite/commit.js +371 -0
- package/dist/storage/sqlite/commit.js.map +1 -0
- package/dist/storage/sqlite/rows.d.ts +33 -0
- package/dist/storage/sqlite/rows.d.ts.map +1 -0
- package/dist/storage/sqlite/rows.js +53 -0
- package/dist/storage/sqlite/rows.js.map +1 -0
- package/dist/storage/sqlite/statements.d.ts +13 -0
- package/dist/storage/sqlite/statements.d.ts.map +1 -0
- package/dist/storage/sqlite/statements.js +88 -0
- package/dist/storage/sqlite/statements.js.map +1 -0
- package/dist/storage/sqlite/storage.d.ts +23 -10
- package/dist/storage/sqlite/storage.d.ts.map +1 -1
- package/dist/storage/sqlite/storage.js +120 -261
- package/dist/storage/sqlite/storage.js.map +1 -1
- package/dist/storage/store/commit.d.ts +33 -0
- package/dist/storage/store/commit.d.ts.map +1 -0
- package/dist/storage/store/commit.js +286 -0
- package/dist/storage/store/commit.js.map +1 -0
- package/dist/storage/store/documents.d.ts +43 -0
- package/dist/storage/store/documents.d.ts.map +1 -0
- package/dist/storage/store/documents.js +102 -0
- package/dist/storage/store/documents.js.map +1 -0
- package/dist/storage/store/index.d.ts +2 -0
- package/dist/storage/store/index.d.ts.map +1 -0
- package/dist/storage/store/index.js +2 -0
- package/dist/storage/store/index.js.map +1 -0
- package/dist/storage/store/queries.d.ts +49 -0
- package/dist/storage/store/queries.d.ts.map +1 -0
- package/dist/storage/store/queries.js +93 -0
- package/dist/storage/store/queries.js.map +1 -0
- package/dist/storage/store/storage.d.ts +60 -0
- package/dist/storage/store/storage.d.ts.map +1 -0
- package/dist/storage/store/storage.js +438 -0
- package/dist/storage/store/storage.js.map +1 -0
- package/dist/storage/store/tables.d.ts +54 -0
- package/dist/storage/store/tables.d.ts.map +1 -0
- package/dist/storage/store/tables.js +108 -0
- package/dist/storage/store/tables.js.map +1 -0
- package/dist/testing/commit-writes.d.ts +5 -0
- package/dist/testing/commit-writes.d.ts.map +1 -0
- package/dist/testing/commit-writes.js +12 -0
- package/dist/testing/commit-writes.js.map +1 -0
- package/dist/testing/env-conformance.js +7 -7
- package/dist/testing/env-conformance.js.map +1 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/storage-benchmark.d.ts.map +1 -1
- package/dist/testing/storage-benchmark.js +18 -17
- package/dist/testing/storage-benchmark.js.map +1 -1
- package/dist/testing/storage-conformance.d.ts.map +1 -1
- package/dist/testing/storage-conformance.js +354 -69
- package/dist/testing/storage-conformance.js.map +1 -1
- package/dist/tool-search/tool-search.d.ts +8 -14
- package/dist/tool-search/tool-search.d.ts.map +1 -1
- package/dist/tool-search/tool-search.js +25 -13
- package/dist/tool-search/tool-search.js.map +1 -1
- package/dist/tools/bash.d.ts +13 -13
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +16 -15
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +7 -7
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +9 -9
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/file-mutation-queue.d.ts +1 -1
- package/dist/tools/file-mutation-queue.d.ts.map +1 -1
- package/dist/tools/file-mutation-queue.js.map +1 -1
- package/dist/tools/index.d.ts +32 -32
- package/dist/tools/path-utils.d.ts +1 -1
- package/dist/tools/path-utils.d.ts.map +1 -1
- package/dist/tools/path-utils.js.map +1 -1
- package/dist/tools/read.d.ts +6 -6
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +19 -8
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/write.d.ts +4 -4
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js +5 -5
- package/dist/tools/write.js.map +1 -1
- package/dist/types.d.ts +228 -78
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +19 -12
- package/dist/harness/events.d.ts +0 -186
- package/dist/harness/events.d.ts.map +0 -1
- package/dist/harness/events.js +0 -373
- package/dist/harness/events.js.map +0 -1
- package/dist/session/observation.d.ts +0 -42
- package/dist/session/observation.d.ts.map +0 -1
- package/dist/session/observation.js +0 -281
- package/dist/session/observation.js.map +0 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// The multi-row statements of a commit call, few whatever the number of rows: rows as one JSON array parameter read
|
|
2
|
+
// through `json_each(?)` (one SQL text per table, no bound-parameter limit), and large rows bound column by column in
|
|
3
|
+
// multi-row `VALUES` of a few fixed sizes, so their already JSON-encoded records are not escaped a second time.
|
|
4
|
+
/** The rows `sql` reads for `values`, given as its one JSON array parameter (`json_each(?)`): one statement per chunk. */
|
|
5
|
+
export async function readRows(executor, sql, values) {
|
|
6
|
+
const rows = [];
|
|
7
|
+
for (const array of jsonArrays(values.map((value) => JSON.stringify(value)))) {
|
|
8
|
+
rows.push(...(await executor.all(sql, array)));
|
|
9
|
+
}
|
|
10
|
+
return rows;
|
|
11
|
+
}
|
|
12
|
+
/** Runs `sql` (a deletion) for `values`, given as its one JSON array parameter: one statement per chunk. */
|
|
13
|
+
export async function runForEach(executor, sql, values) {
|
|
14
|
+
for (const array of jsonArrays(values.map((value) => JSON.stringify(value))))
|
|
15
|
+
await executor.run(sql, array);
|
|
16
|
+
}
|
|
17
|
+
// Rows whose string cells hold more characters than this (before escaping) go bound: a second JSON escaping of their
|
|
18
|
+
// records costs more than the statements they need.
|
|
19
|
+
const LARGE_ROW_CHARACTERS = 8 * 1024;
|
|
20
|
+
/**
|
|
21
|
+
* Inserts `rows` (one cell per column, in order) into `table`, then `conflict` (an upsert's clause): the rows of little
|
|
22
|
+
* text from one JSON array parameter per chunk, the same SQL text whatever their number; the large ones bound column by
|
|
23
|
+
* column.
|
|
24
|
+
*/
|
|
25
|
+
export async function insertRows(executor, table, columns, rows, conflict = "") {
|
|
26
|
+
const small = [];
|
|
27
|
+
const large = [];
|
|
28
|
+
for (const row of rows) {
|
|
29
|
+
let text = 0;
|
|
30
|
+
for (const cell of row)
|
|
31
|
+
if (typeof cell === "string")
|
|
32
|
+
text += cell.length;
|
|
33
|
+
if (text > LARGE_ROW_CHARACTERS)
|
|
34
|
+
large.push(row);
|
|
35
|
+
else
|
|
36
|
+
small.push(JSON.stringify(row));
|
|
37
|
+
}
|
|
38
|
+
const names = columns.join(", ");
|
|
39
|
+
if (small.length > 0) {
|
|
40
|
+
const cells = columns.map((_, index) => `json_extract(item.value, '$[${index}]')`).join(", ");
|
|
41
|
+
// `WHERE true` keeps SQLite from reading an upsert's `ON` as a join constraint of the `SELECT`.
|
|
42
|
+
const sql = `INSERT INTO ${table} (${names}) SELECT ${cells} FROM json_each(?) AS item WHERE true ${conflict}`;
|
|
43
|
+
for (const array of jsonArrays(small))
|
|
44
|
+
await executor.run(sql, array);
|
|
45
|
+
}
|
|
46
|
+
await insertBound(executor, `INSERT INTO ${table} (${names}) VALUES`, columns.length, large, conflict);
|
|
47
|
+
}
|
|
48
|
+
// Durable Objects bind at most 100 parameters per statement.
|
|
49
|
+
const MAX_PARAMETERS = 100;
|
|
50
|
+
/**
|
|
51
|
+
* Inserts `rows` with `head` (`INSERT INTO t (columns) VALUES`), `width` cells each, in statements of 1, 2, 4, ... rows
|
|
52
|
+
* up to `MAX_PARAMETERS` cells: few statements, and at most a handful of SQL texts per table for prepared statements.
|
|
53
|
+
*/
|
|
54
|
+
async function insertBound(executor, head, width, rows, conflict) {
|
|
55
|
+
const row = `(${new Array(width).fill("?").join(", ")})`;
|
|
56
|
+
let size = 1;
|
|
57
|
+
while (size * 2 * width <= MAX_PARAMETERS)
|
|
58
|
+
size *= 2;
|
|
59
|
+
// `size` only shrinks: halved before a chunk to fit the rows left, it is also what the loop then steps over.
|
|
60
|
+
for (let start = 0; start < rows.length; start += size) {
|
|
61
|
+
while (size > rows.length - start)
|
|
62
|
+
size /= 2;
|
|
63
|
+
const chunk = rows.slice(start, start + size);
|
|
64
|
+
await executor.run(`${head} ${new Array(size).fill(row).join(", ")} ${conflict}`, ...chunk.flat());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Durable Objects refuse a string over 2 MB, a bound parameter too; 512 Ki characters are at most 1.5 MB of UTF-8.
|
|
68
|
+
const MAX_ARRAY_CHARACTERS = 512 * 1024;
|
|
69
|
+
/** JSON values `parts`, in order, as JSON arrays of at most `MAX_ARRAY_CHARACTERS` each (a longer value alone). */
|
|
70
|
+
function jsonArrays(parts) {
|
|
71
|
+
const arrays = [];
|
|
72
|
+
let chunk = [];
|
|
73
|
+
// The array's text: its brackets, its parts and the commas between them.
|
|
74
|
+
let length = 2;
|
|
75
|
+
for (const part of parts) {
|
|
76
|
+
if (chunk.length > 0 && length + 1 + part.length > MAX_ARRAY_CHARACTERS) {
|
|
77
|
+
arrays.push(`[${chunk.join(",")}]`);
|
|
78
|
+
chunk = [];
|
|
79
|
+
length = 2;
|
|
80
|
+
}
|
|
81
|
+
length += (chunk.length > 0 ? 1 : 0) + part.length;
|
|
82
|
+
chunk.push(part);
|
|
83
|
+
}
|
|
84
|
+
if (chunk.length > 0)
|
|
85
|
+
arrays.push(`[${chunk.join(",")}]`);
|
|
86
|
+
return arrays;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=statements.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statements.js","sourceRoot":"","sources":["../../../src/storage/sqlite/statements.ts"],"names":[],"mappings":"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","sourcesContent":["// The multi-row statements of a commit call, few whatever the number of rows: rows as one JSON array parameter read\n// through `json_each(?)` (one SQL text per table, no bound-parameter limit), and large rows bound column by column in\n// multi-row `VALUES` of a few fixed sizes, so their already JSON-encoded records are not escaped a second time.\n\nimport type { SqliteExecutor } from \"@earendil-works/chord/sqlite\";\nimport type { Cell } from \"./rows.ts\";\n\n/** The rows `sql` reads for `values`, given as its one JSON array parameter (`json_each(?)`): one statement per chunk. */\nexport async function readRows<T extends object>(\n\texecutor: SqliteExecutor,\n\tsql: string,\n\tvalues: readonly (Cell | readonly Cell[])[],\n): Promise<T[]> {\n\tconst rows: T[] = [];\n\tfor (const array of jsonArrays(values.map((value) => JSON.stringify(value)))) {\n\t\trows.push(...(await executor.all<T>(sql, array)));\n\t}\n\treturn rows;\n}\n\n/** Runs `sql` (a deletion) for `values`, given as its one JSON array parameter: one statement per chunk. */\nexport async function runForEach(executor: SqliteExecutor, sql: string, values: readonly Cell[]): Promise<void> {\n\tfor (const array of jsonArrays(values.map((value) => JSON.stringify(value)))) await executor.run(sql, array);\n}\n\n// Rows whose string cells hold more characters than this (before escaping) go bound: a second JSON escaping of their\n// records costs more than the statements they need.\nconst LARGE_ROW_CHARACTERS = 8 * 1024;\n\n/**\n * Inserts `rows` (one cell per column, in order) into `table`, then `conflict` (an upsert's clause): the rows of little\n * text from one JSON array parameter per chunk, the same SQL text whatever their number; the large ones bound column by\n * column.\n */\nexport async function insertRows(\n\texecutor: SqliteExecutor,\n\ttable: string,\n\tcolumns: readonly string[],\n\trows: readonly (readonly Cell[])[],\n\tconflict = \"\",\n): Promise<void> {\n\tconst small: string[] = [];\n\tconst large: (readonly Cell[])[] = [];\n\tfor (const row of rows) {\n\t\tlet text = 0;\n\t\tfor (const cell of row) if (typeof cell === \"string\") text += cell.length;\n\t\tif (text > LARGE_ROW_CHARACTERS) large.push(row);\n\t\telse small.push(JSON.stringify(row));\n\t}\n\tconst names = columns.join(\", \");\n\tif (small.length > 0) {\n\t\tconst cells = columns.map((_, index) => `json_extract(item.value, '$[${index}]')`).join(\", \");\n\t\t// `WHERE true` keeps SQLite from reading an upsert's `ON` as a join constraint of the `SELECT`.\n\t\tconst sql = `INSERT INTO ${table} (${names}) SELECT ${cells} FROM json_each(?) AS item WHERE true ${conflict}`;\n\t\tfor (const array of jsonArrays(small)) await executor.run(sql, array);\n\t}\n\tawait insertBound(executor, `INSERT INTO ${table} (${names}) VALUES`, columns.length, large, conflict);\n}\n\n// Durable Objects bind at most 100 parameters per statement.\nconst MAX_PARAMETERS = 100;\n\n/**\n * Inserts `rows` with `head` (`INSERT INTO t (columns) VALUES`), `width` cells each, in statements of 1, 2, 4, ... rows\n * up to `MAX_PARAMETERS` cells: few statements, and at most a handful of SQL texts per table for prepared statements.\n */\nasync function insertBound(\n\texecutor: SqliteExecutor,\n\thead: string,\n\twidth: number,\n\trows: readonly (readonly Cell[])[],\n\tconflict: string,\n): Promise<void> {\n\tconst row = `(${new Array(width).fill(\"?\").join(\", \")})`;\n\tlet size = 1;\n\twhile (size * 2 * width <= MAX_PARAMETERS) size *= 2;\n\t// `size` only shrinks: halved before a chunk to fit the rows left, it is also what the loop then steps over.\n\tfor (let start = 0; start < rows.length; start += size) {\n\t\twhile (size > rows.length - start) size /= 2;\n\t\tconst chunk = rows.slice(start, start + size);\n\t\tawait executor.run(`${head} ${new Array(size).fill(row).join(\", \")} ${conflict}`, ...chunk.flat());\n\t}\n}\n\n// Durable Objects refuse a string over 2 MB, a bound parameter too; 512 Ki characters are at most 1.5 MB of UTF-8.\nconst MAX_ARRAY_CHARACTERS = 512 * 1024;\n\n/** JSON values `parts`, in order, as JSON arrays of at most `MAX_ARRAY_CHARACTERS` each (a longer value alone). */\nfunction jsonArrays(parts: readonly string[]): string[] {\n\tconst arrays: string[] = [];\n\tlet chunk: string[] = [];\n\t// The array's text: its brackets, its parts and the commas between them.\n\tlet length = 2;\n\tfor (const part of parts) {\n\t\tif (chunk.length > 0 && length + 1 + part.length > MAX_ARRAY_CHARACTERS) {\n\t\t\tarrays.push(`[${chunk.join(\",\")}]`);\n\t\t\tchunk = [];\n\t\t\tlength = 2;\n\t\t}\n\t\tlength += (chunk.length > 0 ? 1 : 0) + part.length;\n\t\tchunk.push(part);\n\t}\n\tif (chunk.length > 0) arrays.push(`[${chunk.join(\",\")}]`);\n\treturn arrays;\n}\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { Context
|
|
1
|
+
import type { Context } from "@earendil-works/chord/context";
|
|
2
|
+
import type { JsonValue } from "@earendil-works/chord/json";
|
|
2
3
|
import type { SqliteDatabase } from "@earendil-works/chord/sqlite";
|
|
3
|
-
import type { ConversationId, ConversationQuery, ConversationRecord, Cursor, DocumentAddress, DocumentId, DocumentPoint, DocumentQuery, DocumentRecord, EntryId, EntryQuery, EntryRecord, Id, Page, Seq, Storage, StorageWrite, StoredDocument, SubmissionId, SubmissionQuery, SubmissionRecord, TaskId, TaskQuery, TaskRecord } from "../../types.ts";
|
|
4
|
+
import type { CommitResult, ConversationId, ConversationQuery, ConversationRecord, Cursor, DocumentAddress, DocumentId, DocumentPoint, DocumentQuery, DocumentRecord, EntryId, EntryQuery, EntryRecord, Id, Page, Seq, Storage, StorageWrite, StoredDocument, SubmissionId, SubmissionQuery, SubmissionRecord, TaskId, TaskQuery, TaskRecord } from "../../types.ts";
|
|
4
5
|
type StoredTask = TaskRecord<JsonValue, JsonValue, JsonValue>;
|
|
5
6
|
export type SqliteStorageOptions = {
|
|
6
7
|
/**
|
|
@@ -21,7 +22,24 @@ export declare class SqliteStorage implements Storage {
|
|
|
21
22
|
private constructor();
|
|
22
23
|
/** Initialize storage over an owned SQLite database facade. */
|
|
23
24
|
static open(db: SqliteDatabase, options?: SqliteStorageOptions): Promise<SqliteStorage>;
|
|
24
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Commit `batches` in one SQL transaction, each with its own seq. Each batch is checked before it writes anything,
|
|
27
|
+
* against the state the earlier ones left, and the first one refused ends the batch: the transaction commits the ones
|
|
28
|
+
* before it. No savepoints, which Durable Objects do not allow: the checks run before the writes instead. The checks
|
|
29
|
+
* read the stored state of all batches up front and run in memory, and the accepted batches are written per table in
|
|
30
|
+
* multi-row statements (`commit.ts`), so a call costs a few statements whatever its number of batches. A batch with a
|
|
31
|
+
* copy reads the copy's source from the database: the batches before it are written first, so a call of many fork
|
|
32
|
+
* batches costs a few statements per fork.
|
|
33
|
+
*/
|
|
34
|
+
commit(batches: readonly (readonly StorageWrite[])[], receive: (index: number, seq: Seq) => void, _context: Context): Promise<CommitResult>;
|
|
35
|
+
private readMetadata;
|
|
36
|
+
private writeMetadata;
|
|
37
|
+
/**
|
|
38
|
+
* Durable's own checks of one batch, against what the batches before it left: its document actions, or its refusal.
|
|
39
|
+
* Only these checks refuse; a failure of a database call rejects. A copy reads its source from the database, so the
|
|
40
|
+
* batches before it are written first.
|
|
41
|
+
*/
|
|
42
|
+
private checkBatch;
|
|
25
43
|
mintId<I extends Id<string>>(): Promise<I>;
|
|
26
44
|
conversation(id: ConversationId, _context: Context): Promise<ConversationRecord | undefined>;
|
|
27
45
|
scanConversations(query: ConversationQuery, limit: number, cursor: Cursor | undefined, _context: Context): Promise<Page<ConversationRecord, Cursor>>;
|
|
@@ -60,13 +78,8 @@ export declare class SqliteStorage implements Storage {
|
|
|
60
78
|
private readConversation;
|
|
61
79
|
private materializeDocument;
|
|
62
80
|
private candidateNextId;
|
|
63
|
-
|
|
64
|
-
private
|
|
65
|
-
private checkDocumentActions;
|
|
66
|
-
private currentDocumentId;
|
|
67
|
-
private applyTableWrite;
|
|
68
|
-
private claimId;
|
|
69
|
-
private applyDocumentActions;
|
|
81
|
+
/** Resolve each copy to a base of its source's state before this batch, which may not change the source. */
|
|
82
|
+
private resolveCopies;
|
|
70
83
|
private assertOpen;
|
|
71
84
|
}
|
|
72
85
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../../src/storage/sqlite/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../../src/storage/sqlite/storage.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,12 +2,10 @@ import { apply } from "@earendil-works/chord/delta";
|
|
|
2
2
|
import { StorageRequestError } from "../../errors.js";
|
|
3
3
|
import { idFromNumber, seqFromNumber } from "../../ids.js";
|
|
4
4
|
import { nextCursor, scanStart } from "../scan.js";
|
|
5
|
+
import { CommitState, claimOf, PendingWrites, prepareDocumentActions } from "./commit.js";
|
|
5
6
|
import { applySqliteMigrationsForSchema } from "./migrations.js";
|
|
7
|
+
import { addressParts, encodeIndexedString, isCurrentOnly, parseJson, scopeColumns } from "./rows.js";
|
|
6
8
|
import { createSqliteSchema } from "./schema.js";
|
|
7
|
-
const parseJson = (value) => JSON.parse(value);
|
|
8
|
-
const encodeJson = (value) => JSON.stringify(value);
|
|
9
|
-
// Some SQLite bindings replace lone UTF-16 surrogates. JSON encoding keeps indexed identities lossless.
|
|
10
|
-
const encodeIndexedString = (value) => JSON.stringify(value);
|
|
11
9
|
const cursorId = (cursor) => {
|
|
12
10
|
const after = cursor?.after;
|
|
13
11
|
if (after === undefined)
|
|
@@ -26,50 +24,33 @@ const page = (values, limit, order = "ascending") => {
|
|
|
26
24
|
const scanSql = (start) => start.order === "ascending"
|
|
27
25
|
? { clause: "id > ?", param: start.after ?? -1, direction: "ASC" }
|
|
28
26
|
: { clause: "id < ?", param: start.after ?? Number.MAX_SAFE_INTEGER, direction: "DESC" };
|
|
29
|
-
const scopeColumns = (scope) => {
|
|
30
|
-
switch (scope.kind) {
|
|
31
|
-
case "session":
|
|
32
|
-
return { scopeKind: "session", ownerId: 0 };
|
|
33
|
-
case "conversation":
|
|
34
|
-
return { scopeKind: "conversation", ownerId: scope.conversationId };
|
|
35
|
-
case "task":
|
|
36
|
-
return { scopeKind: "task", ownerId: scope.taskId };
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
const addressParts = (address) => {
|
|
40
|
-
const scope = scopeColumns(address.scope);
|
|
41
|
-
return {
|
|
42
|
-
kind: encodeIndexedString(address.kind),
|
|
43
|
-
...scope,
|
|
44
|
-
family: address.key === undefined ? 0 : 1,
|
|
45
|
-
keyValue: encodeIndexedString(address.key ?? ""),
|
|
46
|
-
};
|
|
47
|
-
};
|
|
48
|
-
const addressKey = (address) => {
|
|
49
|
-
const parts = addressParts(address);
|
|
50
|
-
return JSON.stringify([parts.kind, parts.scopeKind, parts.ownerId, parts.family, parts.keyValue]);
|
|
51
|
-
};
|
|
52
27
|
const isAliveAt = (record, at) => {
|
|
53
28
|
if (at === "current")
|
|
54
29
|
return record.retiredAt === undefined;
|
|
55
30
|
return record.createdAt <= at && (record.retiredAt === undefined || at < record.retiredAt);
|
|
56
31
|
};
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
32
|
+
/**
|
|
33
|
+
* `executor`, setting `calls.failed` when one of its calls throws, synchronously (a synchronous driver) or by
|
|
34
|
+
* rejecting, with any value: then a failing check is the database's, not one of Durable's own.
|
|
35
|
+
*/
|
|
36
|
+
function watchCalls(executor, calls) {
|
|
37
|
+
const watched = async (call) => {
|
|
38
|
+
try {
|
|
39
|
+
return await call();
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
calls.failed = true;
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
exec: (sql) => watched(() => executor.exec(sql)),
|
|
48
|
+
run: (sql, ...params) => watched(() => executor.run(sql, ...params)),
|
|
49
|
+
runCounted: (sql, ...params) => watched(() => executor.runCounted(sql, ...params)),
|
|
50
|
+
get: (sql, ...params) => watched(() => executor.get(sql, ...params)),
|
|
51
|
+
all: (sql, ...params) => watched(() => executor.all(sql, ...params)),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
73
54
|
/** Portable SQLite implementation of the Pico storage contract. */
|
|
74
55
|
export class SqliteStorage {
|
|
75
56
|
db;
|
|
@@ -104,25 +85,85 @@ export class SqliteStorage {
|
|
|
104
85
|
throw error;
|
|
105
86
|
}
|
|
106
87
|
}
|
|
107
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Commit `batches` in one SQL transaction, each with its own seq. Each batch is checked before it writes anything,
|
|
90
|
+
* against the state the earlier ones left, and the first one refused ends the batch: the transaction commits the ones
|
|
91
|
+
* before it. No savepoints, which Durable Objects do not allow: the checks run before the writes instead. The checks
|
|
92
|
+
* read the stored state of all batches up front and run in memory, and the accepted batches are written per table in
|
|
93
|
+
* multi-row statements (`commit.ts`), so a call costs a few statements whatever its number of batches. A batch with a
|
|
94
|
+
* copy reads the copy's source from the database: the batches before it are written first, so a call of many fork
|
|
95
|
+
* batches costs a few statements per fork.
|
|
96
|
+
*/
|
|
97
|
+
async commit(batches, receive, _context) {
|
|
108
98
|
this.assertOpen();
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
await
|
|
117
|
-
|
|
118
|
-
for (const
|
|
119
|
-
await this.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
99
|
+
const seqs = [];
|
|
100
|
+
let refused;
|
|
101
|
+
let candidateNextId = this.nextId;
|
|
102
|
+
await this.db.transaction(async (transaction) => {
|
|
103
|
+
const metadata = await this.readMetadata(transaction);
|
|
104
|
+
let next = metadata.next_seq;
|
|
105
|
+
// A failure of the database rejects the call and rolls back the batches before it with the transaction.
|
|
106
|
+
const state = await CommitState.read(transaction, this.schema.tables, batches);
|
|
107
|
+
const pending = new PendingWrites();
|
|
108
|
+
for (const writes of batches) {
|
|
109
|
+
const checked = await this.checkBatch(transaction, state, pending, writes);
|
|
110
|
+
if ("refused" in checked) {
|
|
111
|
+
refused = { error: checked.refused };
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
const seq = seqFromNumber(next++);
|
|
115
|
+
state.apply(writes, checked.actions, seq);
|
|
116
|
+
pending.add(writes, checked.actions, seq, state);
|
|
117
|
+
seqs.push(seq);
|
|
118
|
+
candidateNextId = Math.max(candidateNextId, this.candidateNextId(writes));
|
|
119
|
+
}
|
|
120
|
+
await pending.flush(transaction, this.schema.tables);
|
|
121
|
+
if (seqs.length > 0) {
|
|
122
|
+
await this.writeMetadata(transaction, Math.max(Number(metadata.next_id), candidateNextId), next);
|
|
123
|
+
}
|
|
123
124
|
});
|
|
124
125
|
this.nextId = Math.max(this.nextId, candidateNextId);
|
|
125
|
-
|
|
126
|
+
// `COMMIT` returned: the stored batches are durable.
|
|
127
|
+
for (const [index, seq] of seqs.entries())
|
|
128
|
+
receive(index, seq);
|
|
129
|
+
return refused === undefined ? { stored: seqs.length } : { stored: seqs.length, refused: refused.error };
|
|
130
|
+
}
|
|
131
|
+
async readMetadata(executor) {
|
|
132
|
+
const metadata = await executor.get(`SELECT next_id, next_seq FROM ${this.schema.tables.durableMetadata} WHERE singleton = 1`);
|
|
133
|
+
if (metadata === undefined)
|
|
134
|
+
throw new Error("Durable SQLite metadata is missing");
|
|
135
|
+
return metadata;
|
|
136
|
+
}
|
|
137
|
+
async writeMetadata(executor, nextId, nextSeq) {
|
|
138
|
+
await executor.run(`UPDATE ${this.schema.tables.durableMetadata} SET next_id = ?, next_seq = ? WHERE singleton = 1`, String(nextId), nextSeq);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Durable's own checks of one batch, against what the batches before it left: its document actions, or its refusal.
|
|
142
|
+
* Only these checks refuse; a failure of a database call rejects. A copy reads its source from the database, so the
|
|
143
|
+
* batches before it are written first.
|
|
144
|
+
*/
|
|
145
|
+
async checkBatch(transaction, state, pending, writes) {
|
|
146
|
+
let actions;
|
|
147
|
+
try {
|
|
148
|
+
actions = prepareDocumentActions(writes);
|
|
149
|
+
state.check(writes, actions);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
return { refused: error };
|
|
153
|
+
}
|
|
154
|
+
if (![...actions.values()].some((action) => action.copy !== undefined))
|
|
155
|
+
return { actions };
|
|
156
|
+
await pending.flush(transaction, this.schema.tables);
|
|
157
|
+
const calls = { failed: false };
|
|
158
|
+
try {
|
|
159
|
+
await this.resolveCopies(watchCalls(transaction, calls), actions);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (calls.failed)
|
|
163
|
+
throw error;
|
|
164
|
+
return { refused: error };
|
|
165
|
+
}
|
|
166
|
+
return { actions };
|
|
126
167
|
}
|
|
127
168
|
async mintId() {
|
|
128
169
|
this.assertOpen();
|
|
@@ -452,216 +493,34 @@ export class SqliteStorage {
|
|
|
452
493
|
candidateNextId(writes) {
|
|
453
494
|
let nextId = this.nextId;
|
|
454
495
|
for (const write of writes) {
|
|
455
|
-
const id =
|
|
496
|
+
const id = claimOf(write)?.id;
|
|
456
497
|
if (id !== undefined)
|
|
457
498
|
nextId = Math.max(nextId, id + 1);
|
|
458
499
|
}
|
|
459
500
|
return nextId;
|
|
460
501
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
for (const write of writes) {
|
|
464
|
-
if (write.type === "document.change" || write.type === "document.retire")
|
|
465
|
-
continue;
|
|
466
|
-
const document = write.type === "document.create" || write.type === "document.copy";
|
|
467
|
-
const table = document ? "document" : write.type;
|
|
468
|
-
const id = document ? write.record.id : write.value.id;
|
|
469
|
-
const existing = (await executor.get(`SELECT record_type FROM ${this.schema.tables.recordIds} WHERE id = ?`, id))?.record_type;
|
|
470
|
-
const earlier = claimed.get(id);
|
|
471
|
-
if (table === "conversation" || table === "entry" || table === "document") {
|
|
472
|
-
if (existing !== undefined)
|
|
473
|
-
throw new Error(`ID ${id} already belongs to ${existing}`);
|
|
474
|
-
if (earlier !== undefined)
|
|
475
|
-
throw new Error(`ID ${id} is written more than once`);
|
|
476
|
-
}
|
|
477
|
-
else {
|
|
478
|
-
if (existing !== undefined && existing !== table)
|
|
479
|
-
throw new Error(`ID ${id} already belongs to ${existing}`);
|
|
480
|
-
if (earlier !== undefined && earlier !== table)
|
|
481
|
-
throw new Error(`ID ${id} is written as two record types`);
|
|
482
|
-
}
|
|
483
|
-
claimed.set(id, table);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
prepareDocumentActions(writes) {
|
|
487
|
-
const actions = new Map();
|
|
488
|
-
for (const write of writes) {
|
|
489
|
-
if (write.type !== "document.create" &&
|
|
490
|
-
write.type !== "document.copy" &&
|
|
491
|
-
write.type !== "document.change" &&
|
|
492
|
-
write.type !== "document.retire") {
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
const id = write.type === "document.create" || write.type === "document.copy" ? write.record.id : write.id;
|
|
496
|
-
let action = actions.get(id);
|
|
497
|
-
if (action === undefined) {
|
|
498
|
-
action = { retire: false };
|
|
499
|
-
actions.set(id, action);
|
|
500
|
-
}
|
|
501
|
-
switch (write.type) {
|
|
502
|
-
case "document.create":
|
|
503
|
-
if (action.create !== undefined || action.content !== undefined || action.copy !== undefined) {
|
|
504
|
-
throw new Error(`Document ${id} has more than one content command`);
|
|
505
|
-
}
|
|
506
|
-
action.create = write.record;
|
|
507
|
-
action.content = write.content;
|
|
508
|
-
break;
|
|
509
|
-
case "document.copy":
|
|
510
|
-
if (action.create !== undefined || action.content !== undefined || action.copy !== undefined) {
|
|
511
|
-
throw new Error(`Document ${id} has more than one content command`);
|
|
512
|
-
}
|
|
513
|
-
action.create = write.record;
|
|
514
|
-
action.copy = write.source;
|
|
515
|
-
break;
|
|
516
|
-
case "document.change":
|
|
517
|
-
if (action.content !== undefined || action.copy !== undefined) {
|
|
518
|
-
throw new Error(`Document ${id} has more than one content command`);
|
|
519
|
-
}
|
|
520
|
-
action.content = write.content;
|
|
521
|
-
break;
|
|
522
|
-
case "document.retire":
|
|
523
|
-
if (action.retire)
|
|
524
|
-
throw new Error(`Document ${id} is retired more than once`);
|
|
525
|
-
action.retire = true;
|
|
526
|
-
break;
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
return actions;
|
|
530
|
-
}
|
|
531
|
-
async checkDocumentActions(executor, actions) {
|
|
532
|
-
const liveCounts = new Map();
|
|
502
|
+
/** Resolve each copy to a base of its source's state before this batch, which may not change the source. */
|
|
503
|
+
async resolveCopies(executor, actions) {
|
|
533
504
|
for (const [id, action] of actions) {
|
|
534
|
-
if (action.copy
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
throw new Error(`Document ${id} delta has no base`);
|
|
549
|
-
if (previous.version !== action.content.version) {
|
|
550
|
-
throw new Error(`Document ${id} version transition requires a base`);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
const record = action.create ?? existing;
|
|
554
|
-
const key = addressKey(record);
|
|
555
|
-
let live = liveCounts.get(key);
|
|
556
|
-
if (live === undefined)
|
|
557
|
-
live = (await this.currentDocumentId(executor, record)) === undefined ? 0 : 1;
|
|
558
|
-
if (action.retire && existing !== undefined)
|
|
559
|
-
live--;
|
|
560
|
-
if (action.create !== undefined && !action.retire)
|
|
561
|
-
live++;
|
|
562
|
-
liveCounts.set(key, live);
|
|
563
|
-
}
|
|
564
|
-
for (const live of liveCounts.values()) {
|
|
565
|
-
if (live > 1)
|
|
566
|
-
throw new Error("Document address already has a current incarnation");
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
async currentDocumentId(executor, address) {
|
|
570
|
-
const parts = addressParts(address);
|
|
571
|
-
const id = (await executor.get(`SELECT id FROM ${this.schema.tables.documents}
|
|
572
|
-
WHERE kind = ? AND scope_kind = ? AND owner_id = ? AND family = ? AND key_value = ? AND retired_at IS NULL
|
|
573
|
-
LIMIT 1`, parts.kind, parts.scopeKind, parts.ownerId, parts.family, parts.keyValue))?.id;
|
|
574
|
-
return id === undefined ? undefined : idFromNumber(id);
|
|
575
|
-
}
|
|
576
|
-
async applyTableWrite(executor, write, seq) {
|
|
577
|
-
switch (write.type) {
|
|
578
|
-
case "conversation":
|
|
579
|
-
await this.claimId(executor, write.value.id, "conversation");
|
|
580
|
-
await executor.run(`INSERT INTO ${this.schema.tables.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));
|
|
581
|
-
break;
|
|
582
|
-
case "entry":
|
|
583
|
-
await this.claimId(executor, write.value.id, "entry");
|
|
584
|
-
await executor.run(`INSERT INTO ${this.schema.tables.entries} (id, conversation_id, head, commit_seq, record) VALUES (?, ?, ?, ?, ?)`, write.value.id, write.value.conversationId, write.value.head ?? null, seq, encodeJson(write.value));
|
|
585
|
-
break;
|
|
586
|
-
case "task":
|
|
587
|
-
await this.claimId(executor, write.value.id, "task");
|
|
588
|
-
await executor.run(`INSERT INTO ${this.schema.tables.tasks} (id, conversation_id, kind, status, abort_requested, background, record)
|
|
589
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
590
|
-
ON CONFLICT(id) DO UPDATE SET conversation_id = excluded.conversation_id, kind = excluded.kind,
|
|
591
|
-
status = excluded.status, abort_requested = excluded.abort_requested,
|
|
592
|
-
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));
|
|
593
|
-
break;
|
|
594
|
-
case "submission":
|
|
595
|
-
await this.claimId(executor, write.value.id, "submission");
|
|
596
|
-
await executor.run(`INSERT INTO ${this.schema.tables.submissions} (id, conversation_id, request_id, status, record) VALUES (?, ?, ?, ?, ?)
|
|
597
|
-
ON CONFLICT(id) DO UPDATE SET conversation_id = excluded.conversation_id,
|
|
598
|
-
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));
|
|
599
|
-
break;
|
|
600
|
-
case "document.create":
|
|
601
|
-
case "document.copy":
|
|
602
|
-
case "document.change":
|
|
603
|
-
case "document.retire":
|
|
604
|
-
break;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
async claimId(executor, id, table) {
|
|
608
|
-
await executor.run(`INSERT OR IGNORE INTO ${this.schema.tables.recordIds} (id, record_type) VALUES (?, ?)`, id, table);
|
|
609
|
-
}
|
|
610
|
-
async applyDocumentActions(executor, actions, seq) {
|
|
611
|
-
for (const [id, action] of actions) {
|
|
612
|
-
let content = action.content;
|
|
613
|
-
if (action.copy !== undefined) {
|
|
614
|
-
try {
|
|
615
|
-
const stored = await this.materializeDocument(executor, action.copy.id, action.copy.at);
|
|
616
|
-
if (stored === undefined)
|
|
617
|
-
throw new Error(`Fork source document ${action.copy.id} cannot be read`);
|
|
618
|
-
const create = action.create;
|
|
619
|
-
if (stored.record.scope.kind !== "conversation" ||
|
|
620
|
-
create.scope.kind !== "conversation" ||
|
|
621
|
-
stored.record.kind !== create.kind ||
|
|
622
|
-
stored.record.key !== create.key ||
|
|
623
|
-
stored.record.history !== create.history ||
|
|
624
|
-
stored.record.fork !== create.fork) {
|
|
625
|
-
throw new Error(`Fork source document ${action.copy.id} does not match the copied record`);
|
|
626
|
-
}
|
|
627
|
-
content = { kind: "base", version: stored.version, value: stored.value };
|
|
628
|
-
}
|
|
629
|
-
catch (error) {
|
|
630
|
-
throw new Error(`Document copy ${id} was rejected`, { cause: error });
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
let record;
|
|
634
|
-
if (action.create !== undefined) {
|
|
635
|
-
record = {
|
|
636
|
-
...action.create,
|
|
637
|
-
createdAt: seq,
|
|
638
|
-
...(action.retire ? { retiredAt: seq } : {}),
|
|
639
|
-
};
|
|
640
|
-
const parts = addressParts(record);
|
|
641
|
-
await this.claimId(executor, id, "document");
|
|
642
|
-
await executor.run(`INSERT INTO ${this.schema.tables.documents}
|
|
643
|
-
(id, kind, family, key_value, scope_kind, owner_id, created_at, retired_at, record)
|
|
644
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, parts.kind, parts.family, parts.keyValue, parts.scopeKind, parts.ownerId, seq, action.retire ? seq : null, encodeJson(record));
|
|
645
|
-
}
|
|
646
|
-
else {
|
|
647
|
-
const row = (await executor.get(`SELECT record FROM ${this.schema.tables.documents} WHERE id = ?`, id));
|
|
648
|
-
record = parseJson(row.record);
|
|
649
|
-
}
|
|
650
|
-
if (content !== undefined) {
|
|
651
|
-
if (content.kind === "base" && isCurrentOnly(record)) {
|
|
652
|
-
await executor.run(`DELETE FROM ${this.schema.tables.documentRevisions} WHERE document_id = ?`, id);
|
|
505
|
+
if (action.copy === undefined)
|
|
506
|
+
continue;
|
|
507
|
+
try {
|
|
508
|
+
const stored = await this.materializeDocument(executor, action.copy.id, action.copy.at);
|
|
509
|
+
if (stored === undefined)
|
|
510
|
+
throw new Error(`Fork source document ${action.copy.id} cannot be read`);
|
|
511
|
+
const create = action.create;
|
|
512
|
+
if (stored.record.scope.kind !== "conversation" ||
|
|
513
|
+
create.scope.kind !== "conversation" ||
|
|
514
|
+
stored.record.kind !== create.kind ||
|
|
515
|
+
stored.record.key !== create.key ||
|
|
516
|
+
stored.record.history !== create.history ||
|
|
517
|
+
stored.record.fork !== create.fork) {
|
|
518
|
+
throw new Error(`Fork source document ${action.copy.id} does not match the copied record`);
|
|
653
519
|
}
|
|
654
|
-
|
|
655
|
-
await executor.run(`INSERT INTO ${this.schema.tables.documentRevisions} (document_id, seq, kind, version, content) VALUES (?, ?, ?, ?, ?)`, id, seq, content.kind, content.version, encodedContent);
|
|
520
|
+
action.content = { kind: "base", version: stored.version, value: stored.value };
|
|
656
521
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
record = { ...record, retiredAt: seq };
|
|
660
|
-
await executor.run(`UPDATE ${this.schema.tables.documents} SET retired_at = ?, record = ? WHERE id = ?`, seq, encodeJson(record), id);
|
|
661
|
-
}
|
|
662
|
-
if (isCurrentOnly(record)) {
|
|
663
|
-
await executor.run(`DELETE FROM ${this.schema.tables.documentRevisions} WHERE document_id = ?`, id);
|
|
664
|
-
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
throw new Error(`Document copy ${id} was rejected`, { cause: error });
|
|
665
524
|
}
|
|
666
525
|
}
|
|
667
526
|
}
|