@earendil-works/pi-durable 1.5.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 +18 -0
- package/README.md +17 -4
- package/dist/codemode/codemode.d.ts +2 -1
- package/dist/codemode/codemode.d.ts.map +1 -1
- package/dist/codemode/codemode.js +1 -37
- 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.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 +1 -1
- package/dist/env/index.d.ts.map +1 -1
- package/dist/env/index.js.map +1 -1
- package/dist/env/node-watch.d.ts +1 -1
- package/dist/env/node-watch.d.ts.map +1 -1
- package/dist/env/node-watch.js.map +1 -1
- package/dist/env/node.d.ts +1 -1
- package/dist/env/node.d.ts.map +1 -1
- package/dist/env/node.js.map +1 -1
- package/dist/harness/agent.d.ts.map +1 -1
- package/dist/harness/agent.js +27 -1
- 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 +21 -9
- package/dist/harness/compaction.js.map +1 -1
- package/dist/harness/context.d.ts +1 -1
- package/dist/harness/context.d.ts.map +1 -1
- package/dist/harness/context.js.map +1 -1
- package/dist/harness/define.d.ts +1 -1
- package/dist/harness/define.d.ts.map +1 -1
- package/dist/harness/define.js.map +1 -1
- package/dist/harness/generation.d.ts +1 -1
- package/dist/harness/generation.d.ts.map +1 -1
- package/dist/harness/generation.js +6 -4
- package/dist/harness/generation.js.map +1 -1
- package/dist/harness/harness.d.ts +1 -1
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/inbox.d.ts +2 -1
- package/dist/harness/inbox.d.ts.map +1 -1
- package/dist/harness/inbox.js +1 -1
- package/dist/harness/inbox.js.map +1 -1
- package/dist/harness/json.d.ts +1 -1
- package/dist/harness/json.d.ts.map +1 -1
- package/dist/harness/json.js.map +1 -1
- package/dist/harness/live.d.ts +2 -1
- package/dist/harness/live.d.ts.map +1 -1
- package/dist/harness/live.js.map +1 -1
- package/dist/harness/partial-changes.d.ts +1 -1
- package/dist/harness/partial-changes.d.ts.map +1 -1
- package/dist/harness/partial-changes.js +1 -0
- package/dist/harness/partial-changes.js.map +1 -1
- 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 +1 -1
- package/dist/harness/registry-reader.d.ts.map +1 -1
- package/dist/harness/registry-reader.js.map +1 -1
- package/dist/harness/scheduler.d.ts +2 -1
- package/dist/harness/scheduler.d.ts.map +1 -1
- package/dist/harness/scheduler.js +42 -13
- package/dist/harness/scheduler.js.map +1 -1
- package/dist/harness/submissions.d.ts +1 -1
- package/dist/harness/submissions.d.ts.map +1 -1
- package/dist/harness/submissions.js +1 -1
- package/dist/harness/submissions.js.map +1 -1
- package/dist/harness/task-graph.d.ts +3 -1
- package/dist/harness/task-graph.d.ts.map +1 -1
- package/dist/harness/task-graph.js +1 -1
- package/dist/harness/task-graph.js.map +1 -1
- package/dist/harness/tool.d.ts +1 -1
- package/dist/harness/tool.d.ts.map +1 -1
- package/dist/harness/tool.js +1 -1
- package/dist/harness/tool.js.map +1 -1
- package/dist/harness/transcript.d.ts +1 -1
- package/dist/harness/transcript.d.ts.map +1 -1
- package/dist/harness/transcript.js.map +1 -1
- package/dist/harness/types.d.ts +21 -4
- 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 +1 -1
- package/dist/harness/util.d.ts.map +1 -1
- package/dist/harness/util.js.map +1 -1
- package/dist/harness/view.d.ts +2 -1
- package/dist/harness/view.d.ts.map +1 -1
- package/dist/harness/view.js +1 -1
- package/dist/harness/view.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/session/cached-storage.d.ts +2 -1
- package/dist/session/cached-storage.d.ts.map +1 -1
- package/dist/session/cached-storage.js.map +1 -1
- 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 +2 -1
- package/dist/session/keys.d.ts.map +1 -1
- package/dist/session/keys.js.map +1 -1
- package/dist/session/session.d.ts +3 -1
- package/dist/session/session.d.ts.map +1 -1
- package/dist/session/session.js +1 -1
- package/dist/session/session.js.map +1 -1
- package/dist/session/transaction.d.ts +4 -3
- package/dist/session/transaction.d.ts.map +1 -1
- package/dist/session/transaction.js +3 -3
- package/dist/session/transaction.js.map +1 -1
- package/dist/session/writer.d.ts +1 -1
- package/dist/session/writer.d.ts.map +1 -1
- package/dist/session/writer.js.map +1 -1
- package/dist/storage/indexeddb.d.ts +2 -1
- package/dist/storage/indexeddb.d.ts.map +1 -1
- 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 +32 -3
- package/dist/storage/jsonl/storage.d.ts.map +1 -1
- package/dist/storage/jsonl/storage.js +70 -19
- package/dist/storage/jsonl/storage.js.map +1 -1
- package/dist/storage/memory.d.ts +2 -1
- package/dist/storage/memory.d.ts.map +1 -1
- 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 +12 -11
- package/dist/storage/sqlite/storage.d.ts.map +1 -1
- package/dist/storage/sqlite/storage.js +44 -259
- package/dist/storage/sqlite/storage.js.map +1 -1
- package/dist/storage/store/commit.d.ts +2 -2
- package/dist/storage/store/commit.d.ts.map +1 -1
- package/dist/storage/store/commit.js +1 -1
- package/dist/storage/store/commit.js.map +1 -1
- package/dist/storage/store/documents.d.ts +1 -1
- package/dist/storage/store/documents.d.ts.map +1 -1
- package/dist/storage/store/documents.js +1 -1
- package/dist/storage/store/documents.js.map +1 -1
- package/dist/storage/store/queries.d.ts +2 -2
- package/dist/storage/store/queries.d.ts.map +1 -1
- package/dist/storage/store/queries.js.map +1 -1
- package/dist/storage/store/storage.d.ts +3 -2
- package/dist/storage/store/storage.d.ts.map +1 -1
- package/dist/storage/store/storage.js +1 -1
- package/dist/storage/store/storage.js.map +1 -1
- package/dist/storage/store/tables.d.ts +31 -31
- package/dist/storage/store/tables.d.ts.map +1 -1
- package/dist/storage/store/tables.js +1 -1
- package/dist/storage/store/tables.js.map +1 -1
- package/dist/testing/commit-writes.d.ts +1 -1
- package/dist/testing/commit-writes.d.ts.map +1 -1
- package/dist/testing/commit-writes.js.map +1 -1
- package/dist/testing/env-conformance.js.map +1 -1
- package/dist/testing/storage-benchmark.js.map +1 -1
- package/dist/testing/storage-conformance.js.map +1 -1
- package/dist/tool-search/tool-search.d.ts +2 -2
- package/dist/tool-search/tool-search.d.ts.map +1 -1
- package/dist/tool-search/tool-search.js +18 -6
- package/dist/tool-search/tool-search.js.map +1 -1
- package/dist/tools/bash.d.ts +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.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/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.js.map +1 -1
- package/dist/types.d.ts +6 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +6 -6
|
@@ -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,4 +1,5 @@
|
|
|
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
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>;
|
|
@@ -24,14 +25,21 @@ export declare class SqliteStorage implements Storage {
|
|
|
24
25
|
/**
|
|
25
26
|
* Commit `batches` in one SQL transaction, each with its own seq. Each batch is checked before it writes anything,
|
|
26
27
|
* against the state the earlier ones left, and the first one refused ends the batch: the transaction commits the ones
|
|
27
|
-
* before it. No savepoints, which Durable Objects do not allow: the checks run before the writes instead.
|
|
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.
|
|
28
33
|
*/
|
|
29
34
|
commit(batches: readonly (readonly StorageWrite[])[], receive: (index: number, seq: Seq) => void, _context: Context): Promise<CommitResult>;
|
|
30
35
|
private readMetadata;
|
|
31
36
|
private writeMetadata;
|
|
32
|
-
/**
|
|
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
|
+
*/
|
|
33
42
|
private checkBatch;
|
|
34
|
-
private applyBatch;
|
|
35
43
|
mintId<I extends Id<string>>(): Promise<I>;
|
|
36
44
|
conversation(id: ConversationId, _context: Context): Promise<ConversationRecord | undefined>;
|
|
37
45
|
scanConversations(query: ConversationQuery, limit: number, cursor: Cursor | undefined, _context: Context): Promise<Page<ConversationRecord, Cursor>>;
|
|
@@ -70,15 +78,8 @@ export declare class SqliteStorage implements Storage {
|
|
|
70
78
|
private readConversation;
|
|
71
79
|
private materializeDocument;
|
|
72
80
|
private candidateNextId;
|
|
73
|
-
private checkGlobalIds;
|
|
74
|
-
private prepareDocumentActions;
|
|
75
|
-
private checkDocumentActions;
|
|
76
|
-
private currentDocumentId;
|
|
77
|
-
private applyTableWrite;
|
|
78
|
-
private claimId;
|
|
79
81
|
/** Resolve each copy to a base of its source's state before this batch, which may not change the source. */
|
|
80
82
|
private resolveCopies;
|
|
81
|
-
private applyDocumentActions;
|
|
82
83
|
private assertOpen;
|
|
83
84
|
}
|
|
84
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,11 @@ 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
|
-
const isCurrentOnly = (record) => record.scope.kind !== "conversation" || record.history === "latest";
|
|
58
|
-
const writeId = (write) => {
|
|
59
|
-
switch (write.type) {
|
|
60
|
-
case "conversation":
|
|
61
|
-
case "entry":
|
|
62
|
-
case "task":
|
|
63
|
-
case "submission":
|
|
64
|
-
return write.value.id;
|
|
65
|
-
case "document.create":
|
|
66
|
-
case "document.copy":
|
|
67
|
-
return write.record.id;
|
|
68
|
-
case "document.change":
|
|
69
|
-
case "document.retire":
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
32
|
/**
|
|
74
33
|
* `executor`, setting `calls.failed` when one of its calls throws, synchronously (a synchronous driver) or by
|
|
75
34
|
* rejecting, with any value: then a failing check is the database's, not one of Durable's own.
|
|
@@ -129,7 +88,11 @@ export class SqliteStorage {
|
|
|
129
88
|
/**
|
|
130
89
|
* Commit `batches` in one SQL transaction, each with its own seq. Each batch is checked before it writes anything,
|
|
131
90
|
* against the state the earlier ones left, and the first one refused ends the batch: the transaction commits the ones
|
|
132
|
-
* before it. No savepoints, which Durable Objects do not allow: the checks run before the writes instead.
|
|
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.
|
|
133
96
|
*/
|
|
134
97
|
async commit(batches, receive, _context) {
|
|
135
98
|
this.assertOpen();
|
|
@@ -139,29 +102,22 @@ export class SqliteStorage {
|
|
|
139
102
|
await this.db.transaction(async (transaction) => {
|
|
140
103
|
const metadata = await this.readMetadata(transaction);
|
|
141
104
|
let next = metadata.next_seq;
|
|
142
|
-
//
|
|
143
|
-
const
|
|
144
|
-
const
|
|
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();
|
|
145
108
|
for (const writes of batches) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
calls.failed = false;
|
|
150
|
-
try {
|
|
151
|
-
documentActions = this.prepareDocumentActions(writes);
|
|
152
|
-
await this.checkBatch(checked, writes, documentActions);
|
|
153
|
-
}
|
|
154
|
-
catch (error) {
|
|
155
|
-
if (calls.failed)
|
|
156
|
-
throw error;
|
|
157
|
-
refused = { error };
|
|
109
|
+
const checked = await this.checkBatch(transaction, state, pending, writes);
|
|
110
|
+
if ("refused" in checked) {
|
|
111
|
+
refused = { error: checked.refused };
|
|
158
112
|
break;
|
|
159
113
|
}
|
|
160
114
|
const seq = seqFromNumber(next++);
|
|
161
|
-
|
|
115
|
+
state.apply(writes, checked.actions, seq);
|
|
116
|
+
pending.add(writes, checked.actions, seq, state);
|
|
162
117
|
seqs.push(seq);
|
|
163
118
|
candidateNextId = Math.max(candidateNextId, this.candidateNextId(writes));
|
|
164
119
|
}
|
|
120
|
+
await pending.flush(transaction, this.schema.tables);
|
|
165
121
|
if (seqs.length > 0) {
|
|
166
122
|
await this.writeMetadata(transaction, Math.max(Number(metadata.next_id), candidateNextId), next);
|
|
167
123
|
}
|
|
@@ -181,16 +137,33 @@ export class SqliteStorage {
|
|
|
181
137
|
async writeMetadata(executor, nextId, nextSeq) {
|
|
182
138
|
await executor.run(`UPDATE ${this.schema.tables.durableMetadata} SET next_id = ?, next_seq = ? WHERE singleton = 1`, String(nextId), nextSeq);
|
|
183
139
|
}
|
|
184
|
-
/**
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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 };
|
|
194
167
|
}
|
|
195
168
|
async mintId() {
|
|
196
169
|
this.assertOpen();
|
|
@@ -520,161 +493,12 @@ export class SqliteStorage {
|
|
|
520
493
|
candidateNextId(writes) {
|
|
521
494
|
let nextId = this.nextId;
|
|
522
495
|
for (const write of writes) {
|
|
523
|
-
const id =
|
|
496
|
+
const id = claimOf(write)?.id;
|
|
524
497
|
if (id !== undefined)
|
|
525
498
|
nextId = Math.max(nextId, id + 1);
|
|
526
499
|
}
|
|
527
500
|
return nextId;
|
|
528
501
|
}
|
|
529
|
-
async checkGlobalIds(executor, writes) {
|
|
530
|
-
const claimed = new Map();
|
|
531
|
-
for (const write of writes) {
|
|
532
|
-
if (write.type === "document.change" || write.type === "document.retire")
|
|
533
|
-
continue;
|
|
534
|
-
const document = write.type === "document.create" || write.type === "document.copy";
|
|
535
|
-
const table = document ? "document" : write.type;
|
|
536
|
-
const id = document ? write.record.id : write.value.id;
|
|
537
|
-
const existing = (await executor.get(`SELECT record_type FROM ${this.schema.tables.recordIds} WHERE id = ?`, id))?.record_type;
|
|
538
|
-
const earlier = claimed.get(id);
|
|
539
|
-
if (table === "conversation" || table === "entry" || table === "document") {
|
|
540
|
-
if (existing !== undefined)
|
|
541
|
-
throw new Error(`ID ${id} already belongs to ${existing}`);
|
|
542
|
-
if (earlier !== undefined)
|
|
543
|
-
throw new Error(`ID ${id} is written more than once`);
|
|
544
|
-
}
|
|
545
|
-
else {
|
|
546
|
-
if (existing !== undefined && existing !== table)
|
|
547
|
-
throw new Error(`ID ${id} already belongs to ${existing}`);
|
|
548
|
-
if (earlier !== undefined && earlier !== table)
|
|
549
|
-
throw new Error(`ID ${id} is written as two record types`);
|
|
550
|
-
}
|
|
551
|
-
claimed.set(id, table);
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
prepareDocumentActions(writes) {
|
|
555
|
-
const actions = new Map();
|
|
556
|
-
for (const write of writes) {
|
|
557
|
-
if (write.type !== "document.create" &&
|
|
558
|
-
write.type !== "document.copy" &&
|
|
559
|
-
write.type !== "document.change" &&
|
|
560
|
-
write.type !== "document.retire") {
|
|
561
|
-
continue;
|
|
562
|
-
}
|
|
563
|
-
const id = write.type === "document.create" || write.type === "document.copy" ? write.record.id : write.id;
|
|
564
|
-
let action = actions.get(id);
|
|
565
|
-
if (action === undefined) {
|
|
566
|
-
action = { retire: false };
|
|
567
|
-
actions.set(id, action);
|
|
568
|
-
}
|
|
569
|
-
switch (write.type) {
|
|
570
|
-
case "document.create":
|
|
571
|
-
if (action.create !== undefined || action.content !== undefined || action.copy !== undefined) {
|
|
572
|
-
throw new Error(`Document ${id} has more than one content command`);
|
|
573
|
-
}
|
|
574
|
-
action.create = write.record;
|
|
575
|
-
action.content = write.content;
|
|
576
|
-
break;
|
|
577
|
-
case "document.copy":
|
|
578
|
-
if (action.create !== undefined || action.content !== undefined || action.copy !== undefined) {
|
|
579
|
-
throw new Error(`Document ${id} has more than one content command`);
|
|
580
|
-
}
|
|
581
|
-
action.create = write.record;
|
|
582
|
-
action.copy = write.source;
|
|
583
|
-
break;
|
|
584
|
-
case "document.change":
|
|
585
|
-
if (action.content !== undefined || action.copy !== undefined) {
|
|
586
|
-
throw new Error(`Document ${id} has more than one content command`);
|
|
587
|
-
}
|
|
588
|
-
action.content = write.content;
|
|
589
|
-
break;
|
|
590
|
-
case "document.retire":
|
|
591
|
-
if (action.retire)
|
|
592
|
-
throw new Error(`Document ${id} is retired more than once`);
|
|
593
|
-
action.retire = true;
|
|
594
|
-
break;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
return actions;
|
|
598
|
-
}
|
|
599
|
-
async checkDocumentActions(executor, actions) {
|
|
600
|
-
const liveCounts = new Map();
|
|
601
|
-
for (const [id, action] of actions) {
|
|
602
|
-
if (action.copy !== undefined && actions.has(action.copy.id)) {
|
|
603
|
-
throw new Error(`Document copy ${id} source is changed in the copy batch`);
|
|
604
|
-
}
|
|
605
|
-
const row = await executor.get(`SELECT record FROM ${this.schema.tables.documents} WHERE id = ?`, id);
|
|
606
|
-
const existing = row === undefined ? undefined : parseJson(row.record);
|
|
607
|
-
if (action.create === undefined && existing === undefined)
|
|
608
|
-
throw new Error(`Unknown document: ${id}`);
|
|
609
|
-
if (action.create !== undefined && existing !== undefined)
|
|
610
|
-
throw new Error(`Document ${id} already exists`);
|
|
611
|
-
if (existing?.retiredAt !== undefined)
|
|
612
|
-
throw new Error(`Document ${id} is retired`);
|
|
613
|
-
if (action.content?.kind === "delta") {
|
|
614
|
-
const previous = await executor.get(`SELECT version FROM ${this.schema.tables.documentRevisions} WHERE document_id = ? ORDER BY seq DESC LIMIT 1`, id);
|
|
615
|
-
if (previous === undefined)
|
|
616
|
-
throw new Error(`Document ${id} delta has no base`);
|
|
617
|
-
if (previous.version !== action.content.version) {
|
|
618
|
-
throw new Error(`Document ${id} version transition requires a base`);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
const record = action.create ?? existing;
|
|
622
|
-
const key = addressKey(record);
|
|
623
|
-
let live = liveCounts.get(key);
|
|
624
|
-
if (live === undefined)
|
|
625
|
-
live = (await this.currentDocumentId(executor, record)) === undefined ? 0 : 1;
|
|
626
|
-
if (action.retire && existing !== undefined)
|
|
627
|
-
live--;
|
|
628
|
-
if (action.create !== undefined && !action.retire)
|
|
629
|
-
live++;
|
|
630
|
-
liveCounts.set(key, live);
|
|
631
|
-
}
|
|
632
|
-
for (const live of liveCounts.values()) {
|
|
633
|
-
if (live > 1)
|
|
634
|
-
throw new Error("Document address already has a current incarnation");
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
async currentDocumentId(executor, address) {
|
|
638
|
-
const parts = addressParts(address);
|
|
639
|
-
const id = (await executor.get(`SELECT id FROM ${this.schema.tables.documents}
|
|
640
|
-
WHERE kind = ? AND scope_kind = ? AND owner_id = ? AND family = ? AND key_value = ? AND retired_at IS NULL
|
|
641
|
-
LIMIT 1`, parts.kind, parts.scopeKind, parts.ownerId, parts.family, parts.keyValue))?.id;
|
|
642
|
-
return id === undefined ? undefined : idFromNumber(id);
|
|
643
|
-
}
|
|
644
|
-
async applyTableWrite(executor, write, seq) {
|
|
645
|
-
switch (write.type) {
|
|
646
|
-
case "conversation":
|
|
647
|
-
await this.claimId(executor, write.value.id, "conversation");
|
|
648
|
-
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));
|
|
649
|
-
break;
|
|
650
|
-
case "entry":
|
|
651
|
-
await this.claimId(executor, write.value.id, "entry");
|
|
652
|
-
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));
|
|
653
|
-
break;
|
|
654
|
-
case "task":
|
|
655
|
-
await this.claimId(executor, write.value.id, "task");
|
|
656
|
-
await executor.run(`INSERT INTO ${this.schema.tables.tasks} (id, conversation_id, kind, status, abort_requested, background, record)
|
|
657
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
658
|
-
ON CONFLICT(id) DO UPDATE SET conversation_id = excluded.conversation_id, kind = excluded.kind,
|
|
659
|
-
status = excluded.status, abort_requested = excluded.abort_requested,
|
|
660
|
-
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));
|
|
661
|
-
break;
|
|
662
|
-
case "submission":
|
|
663
|
-
await this.claimId(executor, write.value.id, "submission");
|
|
664
|
-
await executor.run(`INSERT INTO ${this.schema.tables.submissions} (id, conversation_id, request_id, status, record) VALUES (?, ?, ?, ?, ?)
|
|
665
|
-
ON CONFLICT(id) DO UPDATE SET conversation_id = excluded.conversation_id,
|
|
666
|
-
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));
|
|
667
|
-
break;
|
|
668
|
-
case "document.create":
|
|
669
|
-
case "document.copy":
|
|
670
|
-
case "document.change":
|
|
671
|
-
case "document.retire":
|
|
672
|
-
break;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
async claimId(executor, id, table) {
|
|
676
|
-
await executor.run(`INSERT OR IGNORE INTO ${this.schema.tables.recordIds} (id, record_type) VALUES (?, ?)`, id, table);
|
|
677
|
-
}
|
|
678
502
|
/** Resolve each copy to a base of its source's state before this batch, which may not change the source. */
|
|
679
503
|
async resolveCopies(executor, actions) {
|
|
680
504
|
for (const [id, action] of actions) {
|
|
@@ -700,45 +524,6 @@ export class SqliteStorage {
|
|
|
700
524
|
}
|
|
701
525
|
}
|
|
702
526
|
}
|
|
703
|
-
async applyDocumentActions(executor, actions, seq) {
|
|
704
|
-
for (const [id, action] of actions) {
|
|
705
|
-
// Copies are resolved to their content by `resolveCopies()`.
|
|
706
|
-
const content = action.content;
|
|
707
|
-
let record;
|
|
708
|
-
if (action.create !== undefined) {
|
|
709
|
-
record = {
|
|
710
|
-
...action.create,
|
|
711
|
-
createdAt: seq,
|
|
712
|
-
...(action.retire ? { retiredAt: seq } : {}),
|
|
713
|
-
};
|
|
714
|
-
const parts = addressParts(record);
|
|
715
|
-
await this.claimId(executor, id, "document");
|
|
716
|
-
await executor.run(`INSERT INTO ${this.schema.tables.documents}
|
|
717
|
-
(id, kind, family, key_value, scope_kind, owner_id, created_at, retired_at, record)
|
|
718
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, parts.kind, parts.family, parts.keyValue, parts.scopeKind, parts.ownerId, seq, action.retire ? seq : null, encodeJson(record));
|
|
719
|
-
}
|
|
720
|
-
else {
|
|
721
|
-
const row = (await executor.get(`SELECT record FROM ${this.schema.tables.documents} WHERE id = ?`, id));
|
|
722
|
-
record = parseJson(row.record);
|
|
723
|
-
}
|
|
724
|
-
if (content !== undefined) {
|
|
725
|
-
if (content.kind === "base" && isCurrentOnly(record)) {
|
|
726
|
-
await executor.run(`DELETE FROM ${this.schema.tables.documentRevisions} WHERE document_id = ?`, id);
|
|
727
|
-
}
|
|
728
|
-
const encodedContent = content.kind === "base" ? encodeJson(content.value) : encodeJson(content.ops);
|
|
729
|
-
await executor.run(`INSERT INTO ${this.schema.tables.documentRevisions} (document_id, seq, kind, version, content) VALUES (?, ?, ?, ?, ?)`, id, seq, content.kind, content.version, encodedContent);
|
|
730
|
-
}
|
|
731
|
-
if (action.retire) {
|
|
732
|
-
if (action.create === undefined) {
|
|
733
|
-
record = { ...record, retiredAt: seq };
|
|
734
|
-
await executor.run(`UPDATE ${this.schema.tables.documents} SET retired_at = ?, record = ? WHERE id = ?`, seq, encodeJson(record), id);
|
|
735
|
-
}
|
|
736
|
-
if (isCurrentOnly(record)) {
|
|
737
|
-
await executor.run(`DELETE FROM ${this.schema.tables.documentRevisions} WHERE document_id = ?`, id);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
527
|
assertOpen() {
|
|
743
528
|
if (this.closed)
|
|
744
529
|
throw new Error("SqliteStorage is closed");
|