@earendil-works/pi-durable 0.99.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +557 -46
  3. package/dist/entries.d.ts +20 -2
  4. package/dist/entries.d.ts.map +1 -1
  5. package/dist/entries.js +14 -1
  6. package/dist/entries.js.map +1 -1
  7. package/dist/env/index.d.ts +18 -38
  8. package/dist/env/index.d.ts.map +1 -1
  9. package/dist/env/index.js +2 -0
  10. package/dist/env/index.js.map +1 -1
  11. package/dist/env/node.d.ts +2 -0
  12. package/dist/env/node.d.ts.map +1 -1
  13. package/dist/env/node.js +53 -58
  14. package/dist/env/node.js.map +1 -1
  15. package/dist/harness/agent.d.ts +31 -0
  16. package/dist/harness/agent.d.ts.map +1 -0
  17. package/dist/harness/agent.js +209 -0
  18. package/dist/harness/agent.js.map +1 -0
  19. package/dist/harness/compaction.d.ts +56 -0
  20. package/dist/harness/compaction.d.ts.map +1 -0
  21. package/dist/harness/compaction.js +382 -0
  22. package/dist/harness/compaction.js.map +1 -0
  23. package/dist/harness/context.d.ts +8 -0
  24. package/dist/harness/context.d.ts.map +1 -1
  25. package/dist/harness/context.js +32 -21
  26. package/dist/harness/context.js.map +1 -1
  27. package/dist/harness/define.d.ts +18 -0
  28. package/dist/harness/define.d.ts.map +1 -0
  29. package/dist/harness/define.js +25 -0
  30. package/dist/harness/define.js.map +1 -0
  31. package/dist/harness/events.d.ts +166 -0
  32. package/dist/harness/events.d.ts.map +1 -0
  33. package/dist/harness/events.js +319 -0
  34. package/dist/harness/events.js.map +1 -0
  35. package/dist/harness/generation.d.ts +36 -7
  36. package/dist/harness/generation.d.ts.map +1 -1
  37. package/dist/harness/generation.js +356 -82
  38. package/dist/harness/generation.js.map +1 -1
  39. package/dist/harness/harness.d.ts.map +1 -1
  40. package/dist/harness/harness.js +141 -106
  41. package/dist/harness/harness.js.map +1 -1
  42. package/dist/harness/inbox.d.ts +59 -0
  43. package/dist/harness/inbox.d.ts.map +1 -0
  44. package/dist/harness/inbox.js +97 -0
  45. package/dist/harness/inbox.js.map +1 -0
  46. package/dist/harness/json.d.ts +10 -0
  47. package/dist/harness/json.d.ts.map +1 -0
  48. package/dist/harness/json.js +33 -0
  49. package/dist/harness/json.js.map +1 -0
  50. package/dist/harness/live.d.ts +66 -7
  51. package/dist/harness/live.d.ts.map +1 -1
  52. package/dist/harness/live.js +71 -7
  53. package/dist/harness/live.js.map +1 -1
  54. package/dist/harness/output.d.ts +62 -0
  55. package/dist/harness/output.d.ts.map +1 -0
  56. package/dist/harness/output.js +261 -0
  57. package/dist/harness/output.js.map +1 -0
  58. package/dist/harness/prompt.d.ts +9 -6
  59. package/dist/harness/prompt.d.ts.map +1 -1
  60. package/dist/harness/prompt.js +57 -14
  61. package/dist/harness/prompt.js.map +1 -1
  62. package/dist/harness/registry.d.ts +2 -3
  63. package/dist/harness/registry.d.ts.map +1 -1
  64. package/dist/harness/registry.js +69 -277
  65. package/dist/harness/registry.js.map +1 -1
  66. package/dist/harness/scheduler.d.ts +42 -12
  67. package/dist/harness/scheduler.d.ts.map +1 -1
  68. package/dist/harness/scheduler.js +649 -75
  69. package/dist/harness/scheduler.js.map +1 -1
  70. package/dist/harness/submissions.d.ts +14 -9
  71. package/dist/harness/submissions.d.ts.map +1 -1
  72. package/dist/harness/submissions.js +76 -34
  73. package/dist/harness/submissions.js.map +1 -1
  74. package/dist/harness/task-graph.d.ts +49 -0
  75. package/dist/harness/task-graph.d.ts.map +1 -0
  76. package/dist/harness/task-graph.js +163 -0
  77. package/dist/harness/task-graph.js.map +1 -0
  78. package/dist/harness/tool.d.ts +36 -0
  79. package/dist/harness/tool.d.ts.map +1 -0
  80. package/dist/harness/tool.js +415 -0
  81. package/dist/harness/tool.js.map +1 -0
  82. package/dist/harness/types.d.ts +330 -125
  83. package/dist/harness/types.d.ts.map +1 -1
  84. package/dist/harness/types.js.map +1 -1
  85. package/dist/harness/usage.d.ts +18 -0
  86. package/dist/harness/usage.d.ts.map +1 -0
  87. package/dist/harness/usage.js +57 -0
  88. package/dist/harness/usage.js.map +1 -0
  89. package/dist/harness/view.d.ts +43 -0
  90. package/dist/harness/view.d.ts.map +1 -0
  91. package/dist/harness/view.js +189 -0
  92. package/dist/harness/view.js.map +1 -0
  93. package/dist/index.d.ts +13 -5
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +8 -2
  96. package/dist/index.js.map +1 -1
  97. package/dist/session/observation.d.ts +18 -11
  98. package/dist/session/observation.d.ts.map +1 -1
  99. package/dist/session/observation.js +24 -13
  100. package/dist/session/observation.js.map +1 -1
  101. package/dist/session/session.d.ts +10 -1
  102. package/dist/session/session.d.ts.map +1 -1
  103. package/dist/session/session.js +23 -5
  104. package/dist/session/session.js.map +1 -1
  105. package/dist/session/transaction.d.ts +12 -3
  106. package/dist/session/transaction.d.ts.map +1 -1
  107. package/dist/session/transaction.js +90 -31
  108. package/dist/session/transaction.js.map +1 -1
  109. package/dist/storage/jsonl/storage.d.ts +22 -2
  110. package/dist/storage/jsonl/storage.d.ts.map +1 -1
  111. package/dist/storage/jsonl/storage.js +1 -1
  112. package/dist/storage/jsonl/storage.js.map +1 -1
  113. package/dist/storage/memory.js +1 -1
  114. package/dist/storage/memory.js.map +1 -1
  115. package/dist/storage/sqlite/database.d.ts +26 -19
  116. package/dist/storage/sqlite/database.d.ts.map +1 -1
  117. package/dist/storage/sqlite/database.js.map +1 -1
  118. package/dist/storage/sqlite/index.d.ts +1 -1
  119. package/dist/storage/sqlite/index.d.ts.map +1 -1
  120. package/dist/storage/sqlite/index.js.map +1 -1
  121. package/dist/storage/sqlite/migrations.js +7 -7
  122. package/dist/storage/sqlite/migrations.js.map +1 -1
  123. package/dist/storage/sqlite/node.d.ts +23 -7
  124. package/dist/storage/sqlite/node.d.ts.map +1 -1
  125. package/dist/storage/sqlite/node.js +129 -47
  126. package/dist/storage/sqlite/node.js.map +1 -1
  127. package/dist/storage/sqlite/storage.d.ts +12 -0
  128. package/dist/storage/sqlite/storage.d.ts.map +1 -1
  129. package/dist/storage/sqlite/storage.js +121 -129
  130. package/dist/storage/sqlite/storage.js.map +1 -1
  131. package/dist/testing/storage-benchmark.d.ts.map +1 -1
  132. package/dist/testing/storage-benchmark.js +0 -1
  133. package/dist/testing/storage-benchmark.js.map +1 -1
  134. package/dist/testing/storage-conformance.d.ts.map +1 -1
  135. package/dist/testing/storage-conformance.js +34 -2
  136. package/dist/testing/storage-conformance.js.map +1 -1
  137. package/dist/tools/bash.d.ts +28 -0
  138. package/dist/tools/bash.d.ts.map +1 -0
  139. package/dist/tools/bash.js +70 -0
  140. package/dist/tools/bash.js.map +1 -0
  141. package/dist/tools/edit-diff.d.ts +89 -0
  142. package/dist/tools/edit-diff.d.ts.map +1 -0
  143. package/dist/tools/edit-diff.js +386 -0
  144. package/dist/tools/edit-diff.js.map +1 -0
  145. package/dist/tools/edit.d.ts +18 -0
  146. package/dist/tools/edit.d.ts.map +1 -0
  147. package/dist/tools/edit.js +117 -0
  148. package/dist/tools/edit.js.map +1 -0
  149. package/dist/tools/env.d.ts +5 -0
  150. package/dist/tools/env.d.ts.map +1 -0
  151. package/dist/tools/env.js +7 -0
  152. package/dist/tools/env.js.map +1 -0
  153. package/dist/tools/file-mutation-queue.d.ts +9 -0
  154. package/dist/tools/file-mutation-queue.d.ts.map +1 -0
  155. package/dist/tools/file-mutation-queue.js +52 -0
  156. package/dist/tools/file-mutation-queue.js.map +1 -0
  157. package/dist/tools/image.d.ts +2 -0
  158. package/dist/tools/image.d.ts.map +1 -0
  159. package/dist/tools/image.js +92 -0
  160. package/dist/tools/image.js.map +1 -0
  161. package/dist/tools/index.d.ts +23 -0
  162. package/dist/tools/index.d.ts.map +1 -0
  163. package/dist/tools/index.js +15 -0
  164. package/dist/tools/index.js.map +1 -0
  165. package/dist/tools/path-utils.d.ts +5 -0
  166. package/dist/tools/path-utils.d.ts.map +1 -0
  167. package/dist/tools/path-utils.js +26 -0
  168. package/dist/tools/path-utils.js.map +1 -0
  169. package/dist/tools/read.d.ts +17 -0
  170. package/dist/tools/read.d.ts.map +1 -0
  171. package/dist/tools/read.js +101 -0
  172. package/dist/tools/read.js.map +1 -0
  173. package/dist/tools/write.d.ts +10 -0
  174. package/dist/tools/write.d.ts.map +1 -0
  175. package/dist/tools/write.js +31 -0
  176. package/dist/tools/write.js.map +1 -0
  177. package/dist/{env/utils/truncate.d.ts → truncate.d.ts} +1 -17
  178. package/dist/truncate.d.ts.map +1 -0
  179. package/dist/truncate.js +146 -0
  180. package/dist/truncate.js.map +1 -0
  181. package/dist/types.d.ts +97 -19
  182. package/dist/types.d.ts.map +1 -1
  183. package/dist/types.js.map +1 -1
  184. package/package.json +32 -14
  185. package/dist/env/utils/adaptive-publisher.d.ts +0 -24
  186. package/dist/env/utils/adaptive-publisher.d.ts.map +0 -1
  187. package/dist/env/utils/adaptive-publisher.js +0 -79
  188. package/dist/env/utils/adaptive-publisher.js.map +0 -1
  189. package/dist/env/utils/output-capture.d.ts +0 -31
  190. package/dist/env/utils/output-capture.d.ts.map +0 -1
  191. package/dist/env/utils/output-capture.js +0 -238
  192. package/dist/env/utils/output-capture.js.map +0 -1
  193. package/dist/env/utils/truncate.d.ts.map +0 -1
  194. package/dist/env/utils/truncate.js +0 -294
  195. package/dist/env/utils/truncate.js.map +0 -1
  196. package/dist/harness/config.d.ts +0 -20
  197. package/dist/harness/config.d.ts.map +0 -1
  198. package/dist/harness/config.js +0 -18
  199. package/dist/harness/config.js.map +0 -1
  200. package/docs/chord-delta-findings.md +0 -358
  201. package/docs/pico-v5-chord-usage.md +0 -373
  202. package/docs/pico-v5-handoff.md +0 -581
  203. package/docs/pico-v5.md +0 -3084
@@ -6,8 +6,6 @@ const parseJson = (value) => JSON.parse(value);
6
6
  const encodeJson = (value) => JSON.stringify(value);
7
7
  // Some SQLite bindings replace lone UTF-16 surrogates. JSON encoding keeps indexed identities lossless.
8
8
  const encodeIndexedString = (value) => JSON.stringify(value);
9
- const getRow = (statement, ...params) => statement.get(...params);
10
- const allRows = (statement, ...params) => statement.all(...params);
11
9
  const cursorId = (cursor) => {
12
10
  const after = cursor?.after;
13
11
  if (after === undefined)
@@ -66,45 +64,23 @@ const writeId = (write) => {
66
64
  return undefined;
67
65
  }
68
66
  };
69
- class StatementCachingDatabase {
70
- database;
71
- statements = new Map();
72
- constructor(database) {
73
- this.database = database;
74
- }
75
- exec(sql) {
76
- this.database.exec(sql);
77
- }
78
- prepare(sql) {
79
- let statement = this.statements.get(sql);
80
- if (statement === undefined) {
81
- statement = this.database.prepare(sql);
82
- this.statements.set(sql, statement);
83
- }
84
- return statement;
85
- }
86
- transaction(callback) {
87
- return this.database.transaction(callback);
88
- }
89
- close() {
90
- this.statements.clear();
91
- return this.database.close();
92
- }
93
- }
94
67
  /** Portable SQLite implementation of the Pico storage contract. */
95
68
  export class SqliteStorage {
96
69
  db;
97
70
  nextId;
98
71
  closed = false;
72
+ closing;
73
+ admittedReads = 0;
74
+ readsDrained;
99
75
  constructor(db, nextId) {
100
- this.db = new StatementCachingDatabase(db);
76
+ this.db = db;
101
77
  this.nextId = nextId;
102
78
  }
103
79
  /** Initialize storage over an owned SQLite database facade. */
104
80
  static async open(db) {
105
81
  try {
106
82
  await applySqliteMigrations(db);
107
- const metadata = getRow(db.prepare("SELECT next_id, next_seq FROM durable_metadata WHERE singleton = 1"));
83
+ const metadata = await db.get("SELECT next_id, next_seq FROM durable_metadata WHERE singleton = 1");
108
84
  if (metadata === undefined)
109
85
  throw new Error("Durable SQLite metadata is missing");
110
86
  return new SqliteStorage(db, Number(metadata.next_id));
@@ -123,19 +99,17 @@ export class SqliteStorage {
123
99
  this.assertOpen();
124
100
  const documentActions = this.prepareDocumentActions(writes);
125
101
  const candidateNextId = this.candidateNextId(writes);
126
- const seq = await this.db.transaction(() => {
127
- const metadata = getRow(this.db.prepare("SELECT next_id, next_seq FROM durable_metadata WHERE singleton = 1"));
102
+ const seq = await this.db.transaction(async (transaction) => {
103
+ const metadata = await transaction.get("SELECT next_id, next_seq FROM durable_metadata WHERE singleton = 1");
128
104
  if (metadata === undefined)
129
105
  throw new Error("Durable SQLite metadata is missing");
130
106
  const committedSeq = seqFromNumber(metadata.next_seq);
131
- this.checkGlobalIds(writes);
132
- this.checkDocumentActions(documentActions);
107
+ await this.checkGlobalIds(transaction, writes);
108
+ await this.checkDocumentActions(transaction, documentActions);
133
109
  for (const write of writes)
134
- this.applyTableWrite(write, committedSeq);
135
- this.applyDocumentActions(documentActions, committedSeq);
136
- this.db
137
- .prepare("UPDATE durable_metadata SET next_id = ?, next_seq = ? WHERE singleton = 1")
138
- .run(String(Math.max(Number(metadata.next_id), candidateNextId)), committedSeq + 1);
110
+ await this.applyTableWrite(transaction, write, committedSeq);
111
+ await this.applyDocumentActions(transaction, documentActions, committedSeq);
112
+ await transaction.run("UPDATE durable_metadata SET next_id = ?, next_seq = ? WHERE singleton = 1", String(Math.max(Number(metadata.next_id), candidateNextId)), committedSeq + 1);
139
113
  return committedSeq;
140
114
  });
141
115
  this.nextId = Math.max(this.nextId, candidateNextId);
@@ -149,7 +123,7 @@ export class SqliteStorage {
149
123
  }
150
124
  async conversation(id, _context) {
151
125
  this.assertOpen();
152
- const row = getRow(this.db.prepare("SELECT record FROM conversations WHERE id = ?"), id);
126
+ const row = await this.db.get("SELECT record FROM conversations WHERE id = ?", id);
153
127
  return row === undefined ? undefined : parseJson(row.record);
154
128
  }
155
129
  async scanConversations(query, limit, cursor, _context) {
@@ -165,11 +139,19 @@ export class SqliteStorage {
165
139
  params.push(query.ownerTaskId);
166
140
  }
167
141
  params.push(limit + 1);
168
- const rows = allRows(this.db.prepare(`SELECT record FROM conversations WHERE ${clauses.join(" AND ")} ORDER BY id LIMIT ?`), ...params);
142
+ const rows = await this.db.all(`SELECT record FROM conversations WHERE ${clauses.join(" AND ")} ORDER BY id LIMIT ?`, ...params);
169
143
  return page(rows.map((row) => parseJson(row.record)), limit);
170
144
  }
171
- async entry(idOrConversationId, idOrContext, context) {
172
- this.assertOpen();
145
+ entry(idOrConversationId, idOrContext, context) {
146
+ return this.admitRead(() => this.readEntry(idOrConversationId, idOrContext, context));
147
+ }
148
+ findLatestHeadMarker(conversationId, atOrBeforeEntryId, _context) {
149
+ return this.admitRead(() => this.readLatestHeadMarker(conversationId, atOrBeforeEntryId));
150
+ }
151
+ scanEntries(query, limit, cursor, _context) {
152
+ return this.admitRead(() => this.readEntries(query, limit, cursor));
153
+ }
154
+ async readEntry(idOrConversationId, idOrContext, context) {
173
155
  const id = context === undefined
174
156
  ? idFromNumber(idOrConversationId)
175
157
  : typeof idOrContext === "number"
@@ -180,11 +162,11 @@ export class SqliteStorage {
180
162
  let conversation;
181
163
  if (context !== undefined) {
182
164
  const conversationId = idFromNumber(idOrConversationId);
183
- conversation = this.readConversation(conversationId);
165
+ conversation = await this.readConversation(conversationId);
184
166
  if (conversation === undefined)
185
167
  throw new Error(`Unknown conversation: ${conversationId}`);
186
168
  }
187
- const row = getRow(this.db.prepare("SELECT record, commit_seq FROM entries WHERE id = ?"), id);
169
+ const row = await this.db.get("SELECT record, commit_seq FROM entries WHERE id = ?", id);
188
170
  if (row === undefined)
189
171
  return undefined;
190
172
  const entry = parseJson(row.record);
@@ -194,34 +176,32 @@ export class SqliteStorage {
194
176
  if (conversation.parent === undefined)
195
177
  return undefined;
196
178
  upperEntryId = Math.min(upperEntryId, conversation.parent.at);
197
- conversation = this.readConversation(conversation.parent.conversationId);
179
+ conversation = (await this.readConversation(conversation.parent.conversationId));
198
180
  }
199
181
  if (entry.id > upperEntryId)
200
182
  return undefined;
201
183
  }
202
184
  return { entry, commitSeq: seqFromNumber(row.commit_seq) };
203
185
  }
204
- async findLatestHeadMarker(conversationId, atOrBeforeEntryId, _context) {
205
- this.assertOpen();
206
- let conversation = this.readConversation(conversationId);
186
+ async readLatestHeadMarker(conversationId, atOrBeforeEntryId) {
187
+ let conversation = await this.readConversation(conversationId);
207
188
  if (conversation === undefined)
208
189
  throw new Error(`Unknown conversation: ${conversationId}`);
209
190
  let upper = atOrBeforeEntryId;
210
191
  while (true) {
211
192
  const row = upper === undefined
212
- ? getRow(this.db.prepare("SELECT record FROM entries WHERE conversation_id = ? AND head IS NOT NULL ORDER BY id DESC LIMIT 1"), conversation.id)
213
- : getRow(this.db.prepare("SELECT record FROM entries WHERE conversation_id = ? AND head IS NOT NULL AND id <= ? ORDER BY id DESC LIMIT 1"), conversation.id, upper);
193
+ ? await this.db.get("SELECT record FROM entries WHERE conversation_id = ? AND head IS NOT NULL ORDER BY id DESC LIMIT 1", conversation.id)
194
+ : await this.db.get("SELECT record FROM entries WHERE conversation_id = ? AND head IS NOT NULL AND id <= ? ORDER BY id DESC LIMIT 1", conversation.id, upper);
214
195
  if (row !== undefined)
215
196
  return parseJson(row.record);
216
197
  if (conversation.parent === undefined)
217
198
  return undefined;
218
199
  upper = upper === undefined ? conversation.parent.at : Math.min(upper, conversation.parent.at);
219
- conversation = this.readConversation(conversation.parent.conversationId);
200
+ conversation = (await this.readConversation(conversation.parent.conversationId));
220
201
  }
221
202
  }
222
- async scanEntries(query, limit, cursor, _context) {
223
- this.assertOpen();
224
- let conversation = this.readConversation(query.conversationId);
203
+ async readEntries(query, limit, cursor) {
204
+ let conversation = await this.readConversation(query.conversationId);
225
205
  if (conversation === undefined)
226
206
  throw new Error(`Unknown conversation: ${query.conversationId}`);
227
207
  const after = cursorId(cursor);
@@ -241,20 +221,20 @@ export class SqliteStorage {
241
221
  params.push(upper);
242
222
  }
243
223
  params.push(limit + 1 - values.length);
244
- const rows = allRows(this.db.prepare(`SELECT record FROM entries WHERE ${clauses.join(" AND ")} ORDER BY id DESC LIMIT ?`), ...params);
224
+ const rows = await this.db.all(`SELECT record FROM entries WHERE ${clauses.join(" AND ")} ORDER BY id DESC LIMIT ?`, ...params);
245
225
  values.push(...rows.map((row) => parseJson(row.record)));
246
226
  if (values.length > limit || conversation.parent === undefined)
247
227
  break;
248
228
  upper = upper === undefined ? conversation.parent.at : Math.min(upper, conversation.parent.at);
249
229
  if (query.minEntryId !== undefined && upper < query.minEntryId)
250
230
  break;
251
- conversation = this.readConversation(conversation.parent.conversationId);
231
+ conversation = (await this.readConversation(conversation.parent.conversationId));
252
232
  }
253
233
  return page(values, limit);
254
234
  }
255
235
  async task(id, _context) {
256
236
  this.assertOpen();
257
- const row = getRow(this.db.prepare("SELECT record FROM tasks WHERE id = ?"), id);
237
+ const row = await this.db.get("SELECT record FROM tasks WHERE id = ?", id);
258
238
  return row === undefined ? undefined : parseJson(row.record);
259
239
  }
260
240
  async scanTasks(query, limit, cursor, _context) {
@@ -282,12 +262,12 @@ export class SqliteStorage {
282
262
  params.push(query.background ? 1 : 0);
283
263
  }
284
264
  params.push(limit + 1);
285
- const rows = allRows(this.db.prepare(`SELECT record FROM tasks WHERE ${clauses.join(" AND ")} ORDER BY id LIMIT ?`), ...params);
265
+ const rows = await this.db.all(`SELECT record FROM tasks WHERE ${clauses.join(" AND ")} ORDER BY id LIMIT ?`, ...params);
286
266
  return page(rows.map((row) => parseJson(row.record)), limit);
287
267
  }
288
268
  async submission(id, _context) {
289
269
  this.assertOpen();
290
- const row = getRow(this.db.prepare("SELECT record FROM submissions WHERE id = ?"), id);
270
+ const row = await this.db.get("SELECT record FROM submissions WHERE id = ?", id);
291
271
  return row === undefined ? undefined : parseJson(row.record);
292
272
  }
293
273
  async scanSubmissions(query, limit, cursor, _context) {
@@ -303,34 +283,35 @@ export class SqliteStorage {
303
283
  params.push(query.status);
304
284
  }
305
285
  params.push(limit + 1);
306
- const rows = allRows(this.db.prepare(`SELECT record FROM submissions WHERE ${clauses.join(" AND ")} ORDER BY id LIMIT ?`), ...params);
286
+ const rows = await this.db.all(`SELECT record FROM submissions WHERE ${clauses.join(" AND ")} ORDER BY id LIMIT ?`, ...params);
307
287
  return page(rows.map((row) => parseJson(row.record)), limit);
308
288
  }
309
289
  async submissionByRequest(conversationId, requestId, _context) {
310
290
  this.assertOpen();
311
- const row = getRow(this.db.prepare("SELECT record FROM submissions WHERE conversation_id = ? AND request_id = ?"), conversationId, encodeIndexedString(requestId));
291
+ const row = await this.db.get("SELECT record FROM submissions WHERE conversation_id = ? AND request_id = ?", conversationId, encodeIndexedString(requestId));
312
292
  return row === undefined ? undefined : parseJson(row.record);
313
293
  }
314
294
  async findDocument(address, at, _context) {
315
295
  this.assertOpen();
316
296
  const parts = addressParts(address);
317
- const statement = at === "current"
318
- ? this.db.prepare(`SELECT record FROM documents
297
+ const sql = at === "current"
298
+ ? `SELECT record FROM documents
319
299
  WHERE kind = ? AND scope_kind = ? AND owner_id = ? AND family = ? AND key_value = ?
320
- AND retired_at IS NULL ORDER BY created_at DESC LIMIT 1`)
321
- : this.db.prepare(`SELECT record FROM documents
300
+ AND retired_at IS NULL ORDER BY created_at DESC LIMIT 1`
301
+ : `SELECT record FROM documents
322
302
  WHERE kind = ? AND scope_kind = ? AND owner_id = ? AND family = ? AND key_value = ?
323
303
  AND created_at <= ? AND (retired_at IS NULL OR retired_at > ?)
324
- ORDER BY created_at DESC LIMIT 1`);
304
+ ORDER BY created_at DESC LIMIT 1`;
325
305
  const params = [parts.kind, parts.scopeKind, parts.ownerId, parts.family, parts.keyValue];
326
306
  if (at !== "current")
327
307
  params.push(at, at);
328
- const row = getRow(statement, ...params);
308
+ const row = await this.db.get(sql, ...params);
329
309
  return row === undefined ? undefined : parseJson(row.record);
330
310
  }
331
311
  async document(id, at, _context) {
332
312
  this.assertOpen();
333
- return this.materializeDocument(id, at);
313
+ // The record and revision queries must observe one committed state; a commit between them can replace the base.
314
+ return this.db.transaction((transaction) => this.materializeDocument(transaction, id, at));
334
315
  }
335
316
  async scanDocuments(query, limit, cursor, _context) {
336
317
  this.assertOpen();
@@ -349,21 +330,45 @@ export class SqliteStorage {
349
330
  params.push(query.at, query.at);
350
331
  }
351
332
  params.push(limit + 1);
352
- const rows = allRows(this.db.prepare(`SELECT record FROM documents WHERE ${clauses.join(" AND ")} ORDER BY id LIMIT ?`), ...params);
333
+ const rows = await this.db.all(`SELECT record FROM documents WHERE ${clauses.join(" AND ")} ORDER BY id LIMIT ?`, ...params);
353
334
  return page(rows.map((row) => parseJson(row.record)), limit);
354
335
  }
355
- async close(_context) {
356
- if (this.closed)
357
- return;
358
- this.closed = true;
336
+ close(_context) {
337
+ if (this.closing === undefined) {
338
+ this.closed = true;
339
+ this.closing = this.closeDatabase();
340
+ }
341
+ return this.closing;
342
+ }
343
+ async closeDatabase() {
344
+ if (this.admittedReads > 0) {
345
+ await new Promise((resolve) => {
346
+ this.readsDrained = resolve;
347
+ });
348
+ }
359
349
  await this.db.close();
360
350
  }
361
- readConversation(id) {
362
- const row = getRow(this.db.prepare("SELECT record FROM conversations WHERE id = ?"), id);
351
+ /**
352
+ * Run a read that issues several queries. Close waits for admitted reads, so their later queries never reach a
353
+ * closed database. Single-query reads and transactions are already ordered before close by the database.
354
+ */
355
+ async admitRead(read) {
356
+ this.assertOpen();
357
+ this.admittedReads++;
358
+ try {
359
+ return await read();
360
+ }
361
+ finally {
362
+ if (--this.admittedReads === 0)
363
+ this.readsDrained?.();
364
+ }
365
+ }
366
+ async readConversation(id) {
367
+ const row = await this.db.get("SELECT record FROM conversations WHERE id = ?", id);
363
368
  return row === undefined ? undefined : parseJson(row.record);
364
369
  }
365
- materializeDocument(id, at) {
366
- const row = getRow(this.db.prepare("SELECT record FROM documents WHERE id = ?"), id);
370
+ async materializeDocument(executor, id, at) {
371
+ const row = await executor.get("SELECT record FROM documents WHERE id = ?", id);
367
372
  if (row === undefined)
368
373
  return undefined;
369
374
  const record = parseJson(row.record);
@@ -373,13 +378,13 @@ export class SqliteStorage {
373
378
  if (!isAliveAt(record, at))
374
379
  return undefined;
375
380
  const upper = at === "current" ? Number.MAX_SAFE_INTEGER : at;
376
- const base = getRow(this.db.prepare(`SELECT seq, kind, version, content FROM document_revisions
377
- WHERE document_id = ? AND kind = 'base' AND seq <= ? ORDER BY seq DESC LIMIT 1`), id, upper);
381
+ const base = await executor.get(`SELECT seq, kind, version, content FROM document_revisions
382
+ WHERE document_id = ? AND kind = 'base' AND seq <= ? ORDER BY seq DESC LIMIT 1`, id, upper);
378
383
  if (base === undefined)
379
384
  throw new Error(`Document ${id} is missing a required base`);
380
385
  let value = parseJson(base.content);
381
- const tail = allRows(this.db.prepare(`SELECT seq, kind, version, content FROM document_revisions
382
- WHERE document_id = ? AND seq > ? AND seq <= ? ORDER BY seq`), id, base.seq, upper);
386
+ const tail = await executor.all(`SELECT seq, kind, version, content FROM document_revisions
387
+ WHERE document_id = ? AND seq > ? AND seq <= ? ORDER BY seq`, id, base.seq, upper);
383
388
  for (const revision of tail) {
384
389
  if (revision.kind !== "delta" || revision.version !== base.version) {
385
390
  throw new Error(`Document ${id} crosses a stored version boundary without a base`);
@@ -397,16 +402,16 @@ export class SqliteStorage {
397
402
  }
398
403
  return nextId;
399
404
  }
400
- checkGlobalIds(writes) {
405
+ async checkGlobalIds(executor, writes) {
401
406
  const claimed = new Map();
402
- const lookup = this.db.prepare("SELECT record_type FROM record_ids WHERE id = ?");
403
407
  for (const write of writes) {
404
408
  if (write.type === "document.change" || write.type === "document.retire")
405
409
  continue;
406
410
  const document = write.type === "document.create" || write.type === "document.copy";
407
411
  const table = document ? "document" : write.type;
408
412
  const id = document ? write.record.id : write.value.id;
409
- const existing = getRow(lookup, id)?.record_type;
413
+ const existing = (await executor.get("SELECT record_type FROM record_ids WHERE id = ?", id))
414
+ ?.record_type;
410
415
  const earlier = claimed.get(id);
411
416
  if (table === "conversation" || table === "entry" || table === "document") {
412
417
  if (existing !== undefined)
@@ -468,13 +473,13 @@ export class SqliteStorage {
468
473
  }
469
474
  return actions;
470
475
  }
471
- checkDocumentActions(actions) {
476
+ async checkDocumentActions(executor, actions) {
472
477
  const liveCounts = new Map();
473
478
  for (const [id, action] of actions) {
474
479
  if (action.copy !== undefined && actions.has(action.copy.id)) {
475
480
  throw new StorageRejected(`Document copy ${id} source is changed in the copy batch`);
476
481
  }
477
- const row = getRow(this.db.prepare("SELECT record FROM documents WHERE id = ?"), id);
482
+ const row = await executor.get("SELECT record FROM documents WHERE id = ?", id);
478
483
  const existing = row === undefined ? undefined : parseJson(row.record);
479
484
  if (action.create === undefined && existing === undefined)
480
485
  throw new Error(`Unknown document: ${id}`);
@@ -483,7 +488,7 @@ export class SqliteStorage {
483
488
  if (existing?.retiredAt !== undefined)
484
489
  throw new Error(`Document ${id} is retired`);
485
490
  if (action.content?.kind === "delta") {
486
- const previous = getRow(this.db.prepare("SELECT version FROM document_revisions WHERE document_id = ? ORDER BY seq DESC LIMIT 1"), id);
491
+ const previous = await executor.get("SELECT version FROM document_revisions WHERE document_id = ? ORDER BY seq DESC LIMIT 1", id);
487
492
  if (previous === undefined)
488
493
  throw new Error(`Document ${id} delta has no base`);
489
494
  if (previous.version !== action.content.version) {
@@ -494,7 +499,7 @@ export class SqliteStorage {
494
499
  const key = addressKey(record);
495
500
  let live = liveCounts.get(key);
496
501
  if (live === undefined)
497
- live = this.currentDocumentId(record) === undefined ? 0 : 1;
502
+ live = (await this.currentDocumentId(executor, record)) === undefined ? 0 : 1;
498
503
  if (action.retire && existing !== undefined)
499
504
  live--;
500
505
  if (action.create !== undefined && !action.retire)
@@ -506,44 +511,36 @@ export class SqliteStorage {
506
511
  throw new Error("Document address already has a current incarnation");
507
512
  }
508
513
  }
509
- currentDocumentId(address) {
514
+ async currentDocumentId(executor, address) {
510
515
  const parts = addressParts(address);
511
- const id = getRow(this.db.prepare(`SELECT id FROM documents
516
+ const id = (await executor.get(`SELECT id FROM documents
512
517
  WHERE kind = ? AND scope_kind = ? AND owner_id = ? AND family = ? AND key_value = ? AND retired_at IS NULL
513
- LIMIT 1`), parts.kind, parts.scopeKind, parts.ownerId, parts.family, parts.keyValue)?.id;
518
+ LIMIT 1`, parts.kind, parts.scopeKind, parts.ownerId, parts.family, parts.keyValue))?.id;
514
519
  return id === undefined ? undefined : idFromNumber(id);
515
520
  }
516
- applyTableWrite(write, seq) {
521
+ async applyTableWrite(executor, write, seq) {
517
522
  switch (write.type) {
518
523
  case "conversation":
519
- this.claimId(write.value.id, "conversation");
520
- this.db
521
- .prepare("INSERT INTO conversations (id, owner_conversation_id, owner_task_id, record) VALUES (?, ?, ?, ?)")
522
- .run(write.value.id, write.value.owner?.conversationId ?? null, write.value.owner?.taskId ?? null, encodeJson(write.value));
524
+ await this.claimId(executor, write.value.id, "conversation");
525
+ await executor.run("INSERT INTO conversations (id, owner_conversation_id, owner_task_id, record) VALUES (?, ?, ?, ?)", write.value.id, write.value.owner?.conversationId ?? null, write.value.owner?.taskId ?? null, encodeJson(write.value));
523
526
  break;
524
527
  case "entry":
525
- this.claimId(write.value.id, "entry");
526
- this.db
527
- .prepare("INSERT INTO entries (id, conversation_id, head, commit_seq, record) VALUES (?, ?, ?, ?, ?)")
528
- .run(write.value.id, write.value.conversationId, write.value.head ?? null, seq, encodeJson(write.value));
528
+ await this.claimId(executor, write.value.id, "entry");
529
+ await executor.run("INSERT INTO entries (id, conversation_id, head, commit_seq, record) VALUES (?, ?, ?, ?, ?)", write.value.id, write.value.conversationId, write.value.head ?? null, seq, encodeJson(write.value));
529
530
  break;
530
531
  case "task":
531
- this.claimId(write.value.id, "task");
532
- this.db
533
- .prepare(`INSERT INTO tasks (id, conversation_id, kind, status, abort_requested, background, record)
532
+ await this.claimId(executor, write.value.id, "task");
533
+ await executor.run(`INSERT INTO tasks (id, conversation_id, kind, status, abort_requested, background, record)
534
534
  VALUES (?, ?, ?, ?, ?, ?, ?)
535
535
  ON CONFLICT(id) DO UPDATE SET conversation_id = excluded.conversation_id, kind = excluded.kind,
536
536
  status = excluded.status, abort_requested = excluded.abort_requested,
537
- background = excluded.background, record = excluded.record`)
538
- .run(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));
537
+ 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));
539
538
  break;
540
539
  case "submission":
541
- this.claimId(write.value.id, "submission");
542
- this.db
543
- .prepare(`INSERT INTO submissions (id, conversation_id, request_id, status, record) VALUES (?, ?, ?, ?, ?)
540
+ await this.claimId(executor, write.value.id, "submission");
541
+ await executor.run(`INSERT INTO submissions (id, conversation_id, request_id, status, record) VALUES (?, ?, ?, ?, ?)
544
542
  ON CONFLICT(id) DO UPDATE SET conversation_id = excluded.conversation_id,
545
- request_id = excluded.request_id, status = excluded.status, record = excluded.record`)
546
- .run(write.value.id, write.value.conversationId, write.value.requestId === undefined ? null : encodeIndexedString(write.value.requestId), write.value.status, encodeJson(write.value));
543
+ 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));
547
544
  break;
548
545
  case "document.create":
549
546
  case "document.copy":
@@ -552,15 +549,15 @@ export class SqliteStorage {
552
549
  break;
553
550
  }
554
551
  }
555
- claimId(id, table) {
556
- this.db.prepare("INSERT OR IGNORE INTO record_ids (id, record_type) VALUES (?, ?)").run(id, table);
552
+ async claimId(executor, id, table) {
553
+ await executor.run("INSERT OR IGNORE INTO record_ids (id, record_type) VALUES (?, ?)", id, table);
557
554
  }
558
- applyDocumentActions(actions, seq) {
555
+ async applyDocumentActions(executor, actions, seq) {
559
556
  for (const [id, action] of actions) {
560
557
  let content = action.content;
561
558
  if (action.copy !== undefined) {
562
559
  try {
563
- const stored = this.materializeDocument(action.copy.id, action.copy.at);
560
+ const stored = await this.materializeDocument(executor, action.copy.id, action.copy.at);
564
561
  if (stored === undefined)
565
562
  throw new Error(`Fork source document ${action.copy.id} cannot be read`);
566
563
  const create = action.create;
@@ -588,35 +585,30 @@ export class SqliteStorage {
588
585
  ...(action.retire ? { retiredAt: seq } : {}),
589
586
  };
590
587
  const parts = addressParts(record);
591
- this.claimId(id, "document");
592
- this.db
593
- .prepare(`INSERT INTO documents
588
+ await this.claimId(executor, id, "document");
589
+ await executor.run(`INSERT INTO documents
594
590
  (id, kind, family, key_value, scope_kind, owner_id, created_at, retired_at, record)
595
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
596
- .run(id, parts.kind, parts.family, parts.keyValue, parts.scopeKind, parts.ownerId, seq, action.retire ? seq : null, encodeJson(record));
591
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, parts.kind, parts.family, parts.keyValue, parts.scopeKind, parts.ownerId, seq, action.retire ? seq : null, encodeJson(record));
597
592
  }
598
593
  else {
599
- const row = getRow(this.db.prepare("SELECT record FROM documents WHERE id = ?"), id);
594
+ const row = (await executor.get("SELECT record FROM documents WHERE id = ?", id));
600
595
  record = parseJson(row.record);
601
596
  }
602
597
  if (content !== undefined) {
603
598
  if (content.kind === "base" && isCurrentOnly(record)) {
604
- this.db.prepare("DELETE FROM document_revisions WHERE document_id = ?").run(id);
599
+ await executor.run("DELETE FROM document_revisions WHERE document_id = ?", id);
605
600
  }
606
601
  const encodedContent = content.kind === "base" ? encodeJson(content.value) : encodeJson(content.ops);
607
- this.db
608
- .prepare("INSERT INTO document_revisions (document_id, seq, kind, version, content) VALUES (?, ?, ?, ?, ?)")
609
- .run(id, seq, content.kind, content.version, encodedContent);
602
+ await executor.run("INSERT INTO document_revisions (document_id, seq, kind, version, content) VALUES (?, ?, ?, ?, ?)", id, seq, content.kind, content.version, encodedContent);
610
603
  }
611
604
  if (action.retire) {
612
605
  if (action.create === undefined) {
613
606
  record = { ...record, retiredAt: seq };
614
- this.db
615
- .prepare("UPDATE documents SET retired_at = ?, record = ? WHERE id = ?")
616
- .run(seq, encodeJson(record), id);
607
+ await executor.run("UPDATE documents SET retired_at = ?, record = ? WHERE id = ?", seq, encodeJson(record), id);
608
+ }
609
+ if (isCurrentOnly(record)) {
610
+ await executor.run("DELETE FROM document_revisions WHERE document_id = ?", id);
617
611
  }
618
- if (isCurrentOnly(record))
619
- this.db.prepare("DELETE FROM document_revisions WHERE document_id = ?").run(id);
620
612
  }
621
613
  }
622
614
  }