@earendil-works/pi-durable 0.99.2 → 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.
- package/CHANGELOG.md +7 -0
- package/README.md +196 -74
- package/dist/env/index.d.ts +5 -0
- package/dist/env/index.d.ts.map +1 -1
- package/dist/env/index.js.map +1 -1
- package/dist/env/node.d.ts +2 -0
- package/dist/env/node.d.ts.map +1 -1
- package/dist/env/node.js +2 -0
- package/dist/env/node.js.map +1 -1
- package/dist/harness/agent.d.ts +31 -0
- package/dist/harness/agent.d.ts.map +1 -0
- package/dist/harness/agent.js +209 -0
- package/dist/harness/agent.js.map +1 -0
- package/dist/harness/compaction.d.ts.map +1 -1
- package/dist/harness/compaction.js +8 -11
- package/dist/harness/compaction.js.map +1 -1
- package/dist/harness/define.d.ts +18 -0
- package/dist/harness/define.d.ts.map +1 -0
- package/dist/harness/define.js +25 -0
- package/dist/harness/define.js.map +1 -0
- package/dist/harness/events.d.ts +5 -5
- package/dist/harness/events.d.ts.map +1 -1
- package/dist/harness/events.js +6 -6
- package/dist/harness/events.js.map +1 -1
- package/dist/harness/generation.d.ts +2 -4
- package/dist/harness/generation.d.ts.map +1 -1
- package/dist/harness/generation.js +39 -43
- package/dist/harness/generation.js.map +1 -1
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +68 -154
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/inbox.d.ts +4 -2
- package/dist/harness/inbox.d.ts.map +1 -1
- package/dist/harness/inbox.js +3 -5
- package/dist/harness/inbox.js.map +1 -1
- package/dist/harness/prompt.d.ts +1 -3
- package/dist/harness/prompt.d.ts.map +1 -1
- package/dist/harness/prompt.js +1 -11
- package/dist/harness/prompt.js.map +1 -1
- package/dist/harness/registry.d.ts +2 -3
- package/dist/harness/registry.d.ts.map +1 -1
- package/dist/harness/registry.js +66 -283
- package/dist/harness/registry.js.map +1 -1
- package/dist/harness/scheduler.d.ts +7 -2
- package/dist/harness/scheduler.d.ts.map +1 -1
- package/dist/harness/scheduler.js +41 -27
- package/dist/harness/scheduler.js.map +1 -1
- package/dist/harness/submissions.d.ts +4 -4
- package/dist/harness/submissions.d.ts.map +1 -1
- package/dist/harness/submissions.js +7 -4
- package/dist/harness/submissions.js.map +1 -1
- package/dist/harness/task-graph.d.ts +49 -0
- package/dist/harness/task-graph.d.ts.map +1 -0
- package/dist/harness/task-graph.js +163 -0
- package/dist/harness/task-graph.js.map +1 -0
- package/dist/harness/tool.d.ts +1 -1
- package/dist/harness/tool.d.ts.map +1 -1
- package/dist/harness/tool.js +9 -6
- package/dist/harness/tool.js.map +1 -1
- package/dist/harness/types.d.ts +171 -140
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/harness/view.d.ts +1 -1
- package/dist/harness/view.d.ts.map +1 -1
- package/dist/harness/view.js +2 -7
- package/dist/harness/view.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/session/session.d.ts.map +1 -1
- package/dist/session/session.js +5 -1
- package/dist/session/session.js.map +1 -1
- package/dist/session/transaction.d.ts +2 -2
- package/dist/session/transaction.d.ts.map +1 -1
- package/dist/session/transaction.js +7 -4
- package/dist/session/transaction.js.map +1 -1
- package/dist/storage/sqlite/database.d.ts +26 -19
- package/dist/storage/sqlite/database.d.ts.map +1 -1
- package/dist/storage/sqlite/database.js.map +1 -1
- package/dist/storage/sqlite/index.d.ts +1 -1
- package/dist/storage/sqlite/index.d.ts.map +1 -1
- package/dist/storage/sqlite/index.js.map +1 -1
- package/dist/storage/sqlite/migrations.js +6 -6
- package/dist/storage/sqlite/migrations.js.map +1 -1
- package/dist/storage/sqlite/node.d.ts +23 -7
- package/dist/storage/sqlite/node.d.ts.map +1 -1
- package/dist/storage/sqlite/node.js +129 -47
- package/dist/storage/sqlite/node.js.map +1 -1
- package/dist/storage/sqlite/storage.d.ts +12 -0
- package/dist/storage/sqlite/storage.d.ts.map +1 -1
- package/dist/storage/sqlite/storage.js +121 -129
- package/dist/storage/sqlite/storage.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 +3 -2
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +3 -3
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +5 -4
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/file-mutation-queue.d.ts +5 -1
- package/dist/tools/file-mutation-queue.d.ts.map +1 -1
- package/dist/tools/file-mutation-queue.js +39 -33
- package/dist/tools/file-mutation-queue.js.map +1 -1
- package/dist/tools/index.d.ts +18 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +10 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/read.d.ts +3 -3
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +4 -3
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/write.d.ts +1 -1
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js +3 -2
- package/dist/tools/write.js.map +1 -1
- package/dist/types.d.ts +15 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +25 -15
- package/dist/harness/config.d.ts +0 -29
- package/dist/harness/config.d.ts.map +0 -1
- package/dist/harness/config.js +0 -24
- package/dist/harness/config.js.map +0 -1
- package/docs/chord-delta-findings.md +0 -358
- package/docs/pico-v5-chord-usage.md +0 -373
- package/docs/pico-v5-handoff.md +0 -861
- package/docs/pico-v5.md +0 -4174
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
172
|
-
this.
|
|
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 =
|
|
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
|
|
205
|
-
this.
|
|
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
|
-
?
|
|
213
|
-
:
|
|
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
|
|
223
|
-
this.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
318
|
-
?
|
|
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
|
-
:
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
356
|
-
if (this.
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
362
|
-
|
|
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 =
|
|
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 =
|
|
377
|
-
WHERE document_id = ? AND kind = 'base' AND seq <= ? ORDER BY seq DESC LIMIT 1
|
|
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 =
|
|
382
|
-
WHERE document_id = ? AND seq > ? AND seq <= ? ORDER BY seq
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
}
|