@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.
Files changed (201) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +17 -4
  3. package/dist/codemode/codemode.d.ts +2 -1
  4. package/dist/codemode/codemode.d.ts.map +1 -1
  5. package/dist/codemode/codemode.js +1 -37
  6. package/dist/codemode/codemode.js.map +1 -1
  7. package/dist/codemode/models.d.ts +2 -1
  8. package/dist/codemode/models.d.ts.map +1 -1
  9. package/dist/codemode/models.js.map +1 -1
  10. package/dist/documents.d.ts +1 -1
  11. package/dist/documents.d.ts.map +1 -1
  12. package/dist/documents.js +1 -1
  13. package/dist/documents.js.map +1 -1
  14. package/dist/entries.d.ts +1 -1
  15. package/dist/entries.d.ts.map +1 -1
  16. package/dist/entries.js.map +1 -1
  17. package/dist/env/index.d.ts +1 -1
  18. package/dist/env/index.d.ts.map +1 -1
  19. package/dist/env/index.js.map +1 -1
  20. package/dist/env/node-watch.d.ts +1 -1
  21. package/dist/env/node-watch.d.ts.map +1 -1
  22. package/dist/env/node-watch.js.map +1 -1
  23. package/dist/env/node.d.ts +1 -1
  24. package/dist/env/node.d.ts.map +1 -1
  25. package/dist/env/node.js.map +1 -1
  26. package/dist/harness/agent.d.ts.map +1 -1
  27. package/dist/harness/agent.js +27 -1
  28. package/dist/harness/agent.js.map +1 -1
  29. package/dist/harness/compaction.d.ts +8 -3
  30. package/dist/harness/compaction.d.ts.map +1 -1
  31. package/dist/harness/compaction.js +21 -9
  32. package/dist/harness/compaction.js.map +1 -1
  33. package/dist/harness/context.d.ts +1 -1
  34. package/dist/harness/context.d.ts.map +1 -1
  35. package/dist/harness/context.js.map +1 -1
  36. package/dist/harness/define.d.ts +1 -1
  37. package/dist/harness/define.d.ts.map +1 -1
  38. package/dist/harness/define.js.map +1 -1
  39. package/dist/harness/generation.d.ts +1 -1
  40. package/dist/harness/generation.d.ts.map +1 -1
  41. package/dist/harness/generation.js +6 -4
  42. package/dist/harness/generation.js.map +1 -1
  43. package/dist/harness/harness.d.ts +1 -1
  44. package/dist/harness/harness.d.ts.map +1 -1
  45. package/dist/harness/harness.js.map +1 -1
  46. package/dist/harness/inbox.d.ts +2 -1
  47. package/dist/harness/inbox.d.ts.map +1 -1
  48. package/dist/harness/inbox.js +1 -1
  49. package/dist/harness/inbox.js.map +1 -1
  50. package/dist/harness/json.d.ts +1 -1
  51. package/dist/harness/json.d.ts.map +1 -1
  52. package/dist/harness/json.js.map +1 -1
  53. package/dist/harness/live.d.ts +2 -1
  54. package/dist/harness/live.d.ts.map +1 -1
  55. package/dist/harness/live.js.map +1 -1
  56. package/dist/harness/partial-changes.d.ts +1 -1
  57. package/dist/harness/partial-changes.d.ts.map +1 -1
  58. package/dist/harness/partial-changes.js +1 -0
  59. package/dist/harness/partial-changes.js.map +1 -1
  60. package/dist/harness/prompt.d.ts +1 -1
  61. package/dist/harness/prompt.d.ts.map +1 -1
  62. package/dist/harness/prompt.js.map +1 -1
  63. package/dist/harness/provider.d.ts +1 -1
  64. package/dist/harness/provider.d.ts.map +1 -1
  65. package/dist/harness/provider.js.map +1 -1
  66. package/dist/harness/registry-reader.d.ts +1 -1
  67. package/dist/harness/registry-reader.d.ts.map +1 -1
  68. package/dist/harness/registry-reader.js.map +1 -1
  69. package/dist/harness/scheduler.d.ts +2 -1
  70. package/dist/harness/scheduler.d.ts.map +1 -1
  71. package/dist/harness/scheduler.js +42 -13
  72. package/dist/harness/scheduler.js.map +1 -1
  73. package/dist/harness/submissions.d.ts +1 -1
  74. package/dist/harness/submissions.d.ts.map +1 -1
  75. package/dist/harness/submissions.js +1 -1
  76. package/dist/harness/submissions.js.map +1 -1
  77. package/dist/harness/task-graph.d.ts +3 -1
  78. package/dist/harness/task-graph.d.ts.map +1 -1
  79. package/dist/harness/task-graph.js +1 -1
  80. package/dist/harness/task-graph.js.map +1 -1
  81. package/dist/harness/tool.d.ts +1 -1
  82. package/dist/harness/tool.d.ts.map +1 -1
  83. package/dist/harness/tool.js +1 -1
  84. package/dist/harness/tool.js.map +1 -1
  85. package/dist/harness/transcript.d.ts +1 -1
  86. package/dist/harness/transcript.d.ts.map +1 -1
  87. package/dist/harness/transcript.js.map +1 -1
  88. package/dist/harness/types.d.ts +21 -4
  89. package/dist/harness/types.d.ts.map +1 -1
  90. package/dist/harness/types.js.map +1 -1
  91. package/dist/harness/usage.d.ts +2 -1
  92. package/dist/harness/usage.d.ts.map +1 -1
  93. package/dist/harness/usage.js +1 -1
  94. package/dist/harness/usage.js.map +1 -1
  95. package/dist/harness/util.d.ts +1 -1
  96. package/dist/harness/util.d.ts.map +1 -1
  97. package/dist/harness/util.js.map +1 -1
  98. package/dist/harness/view.d.ts +2 -1
  99. package/dist/harness/view.d.ts.map +1 -1
  100. package/dist/harness/view.js +1 -1
  101. package/dist/harness/view.js.map +1 -1
  102. package/dist/index.d.ts +1 -1
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.js.map +1 -1
  105. package/dist/session/cached-storage.d.ts +2 -1
  106. package/dist/session/cached-storage.d.ts.map +1 -1
  107. package/dist/session/cached-storage.js.map +1 -1
  108. package/dist/session/forks.d.ts +1 -1
  109. package/dist/session/forks.d.ts.map +1 -1
  110. package/dist/session/forks.js.map +1 -1
  111. package/dist/session/keys.d.ts +2 -1
  112. package/dist/session/keys.d.ts.map +1 -1
  113. package/dist/session/keys.js.map +1 -1
  114. package/dist/session/session.d.ts +3 -1
  115. package/dist/session/session.d.ts.map +1 -1
  116. package/dist/session/session.js +1 -1
  117. package/dist/session/session.js.map +1 -1
  118. package/dist/session/transaction.d.ts +4 -3
  119. package/dist/session/transaction.d.ts.map +1 -1
  120. package/dist/session/transaction.js +3 -3
  121. package/dist/session/transaction.js.map +1 -1
  122. package/dist/session/writer.d.ts +1 -1
  123. package/dist/session/writer.d.ts.map +1 -1
  124. package/dist/session/writer.js.map +1 -1
  125. package/dist/storage/indexeddb.d.ts +2 -1
  126. package/dist/storage/indexeddb.d.ts.map +1 -1
  127. package/dist/storage/indexeddb.js.map +1 -1
  128. package/dist/storage/jsonl/index.d.ts +1 -1
  129. package/dist/storage/jsonl/index.d.ts.map +1 -1
  130. package/dist/storage/jsonl/index.js +1 -1
  131. package/dist/storage/jsonl/index.js.map +1 -1
  132. package/dist/storage/jsonl/node.d.ts +3 -3
  133. package/dist/storage/jsonl/node.d.ts.map +1 -1
  134. package/dist/storage/jsonl/node.js +2 -2
  135. package/dist/storage/jsonl/node.js.map +1 -1
  136. package/dist/storage/jsonl/storage.d.ts +32 -3
  137. package/dist/storage/jsonl/storage.d.ts.map +1 -1
  138. package/dist/storage/jsonl/storage.js +70 -19
  139. package/dist/storage/jsonl/storage.js.map +1 -1
  140. package/dist/storage/memory.d.ts +2 -1
  141. package/dist/storage/memory.d.ts.map +1 -1
  142. package/dist/storage/memory.js.map +1 -1
  143. package/dist/storage/sqlite/commit.d.ts +52 -0
  144. package/dist/storage/sqlite/commit.d.ts.map +1 -0
  145. package/dist/storage/sqlite/commit.js +371 -0
  146. package/dist/storage/sqlite/commit.js.map +1 -0
  147. package/dist/storage/sqlite/rows.d.ts +33 -0
  148. package/dist/storage/sqlite/rows.d.ts.map +1 -0
  149. package/dist/storage/sqlite/rows.js +53 -0
  150. package/dist/storage/sqlite/rows.js.map +1 -0
  151. package/dist/storage/sqlite/statements.d.ts +13 -0
  152. package/dist/storage/sqlite/statements.d.ts.map +1 -0
  153. package/dist/storage/sqlite/statements.js +88 -0
  154. package/dist/storage/sqlite/statements.js.map +1 -0
  155. package/dist/storage/sqlite/storage.d.ts +12 -11
  156. package/dist/storage/sqlite/storage.d.ts.map +1 -1
  157. package/dist/storage/sqlite/storage.js +44 -259
  158. package/dist/storage/sqlite/storage.js.map +1 -1
  159. package/dist/storage/store/commit.d.ts +2 -2
  160. package/dist/storage/store/commit.d.ts.map +1 -1
  161. package/dist/storage/store/commit.js +1 -1
  162. package/dist/storage/store/commit.js.map +1 -1
  163. package/dist/storage/store/documents.d.ts +1 -1
  164. package/dist/storage/store/documents.d.ts.map +1 -1
  165. package/dist/storage/store/documents.js +1 -1
  166. package/dist/storage/store/documents.js.map +1 -1
  167. package/dist/storage/store/queries.d.ts +2 -2
  168. package/dist/storage/store/queries.d.ts.map +1 -1
  169. package/dist/storage/store/queries.js.map +1 -1
  170. package/dist/storage/store/storage.d.ts +3 -2
  171. package/dist/storage/store/storage.d.ts.map +1 -1
  172. package/dist/storage/store/storage.js +1 -1
  173. package/dist/storage/store/storage.js.map +1 -1
  174. package/dist/storage/store/tables.d.ts +31 -31
  175. package/dist/storage/store/tables.d.ts.map +1 -1
  176. package/dist/storage/store/tables.js +1 -1
  177. package/dist/storage/store/tables.js.map +1 -1
  178. package/dist/testing/commit-writes.d.ts +1 -1
  179. package/dist/testing/commit-writes.d.ts.map +1 -1
  180. package/dist/testing/commit-writes.js.map +1 -1
  181. package/dist/testing/env-conformance.js.map +1 -1
  182. package/dist/testing/storage-benchmark.js.map +1 -1
  183. package/dist/testing/storage-conformance.js.map +1 -1
  184. package/dist/tool-search/tool-search.d.ts +2 -2
  185. package/dist/tool-search/tool-search.d.ts.map +1 -1
  186. package/dist/tool-search/tool-search.js +18 -6
  187. package/dist/tool-search/tool-search.js.map +1 -1
  188. package/dist/tools/bash.d.ts +1 -1
  189. package/dist/tools/bash.d.ts.map +1 -1
  190. package/dist/tools/bash.js.map +1 -1
  191. package/dist/tools/file-mutation-queue.d.ts +1 -1
  192. package/dist/tools/file-mutation-queue.d.ts.map +1 -1
  193. package/dist/tools/file-mutation-queue.js.map +1 -1
  194. package/dist/tools/path-utils.d.ts +1 -1
  195. package/dist/tools/path-utils.d.ts.map +1 -1
  196. package/dist/tools/path-utils.js.map +1 -1
  197. package/dist/tools/read.js.map +1 -1
  198. package/dist/types.d.ts +6 -2
  199. package/dist/types.d.ts.map +1 -1
  200. package/dist/types.js.map +1 -1
  201. 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, JsonValue } from "@earendil-works/chord";
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
- /** Durable's own checks of one batch, copy resolution included, before it writes anything. */
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":"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"}
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
- // Whether a database call of a batch's checks failed: then the failure is an unknown outcome, not a refusal.
143
- const calls = { failed: false };
144
- const checked = watchCalls(transaction, calls);
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
- let documentActions;
147
- // Only Durable's own checks refuse a batch, before it wrote anything: the ones before it stand. A failure of
148
- // the database rejects the call, and rolls back the batches before it with the transaction.
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
- await this.applyBatch(transaction, writes, documentActions, seq);
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
- /** Durable's own checks of one batch, copy resolution included, before it writes anything. */
185
- async checkBatch(executor, writes, documentActions) {
186
- await this.checkGlobalIds(executor, writes);
187
- await this.checkDocumentActions(executor, documentActions);
188
- await this.resolveCopies(executor, documentActions);
189
- }
190
- async applyBatch(executor, writes, documentActions, seq) {
191
- for (const write of writes)
192
- await this.applyTableWrite(executor, write, seq);
193
- await this.applyDocumentActions(executor, documentActions, seq);
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 = writeId(write);
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");