@earendil-works/pi-durable 0.87.0 → 0.99.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/README.md +72 -1
- package/dist/documents.d.ts +66 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +98 -0
- package/dist/documents.js.map +1 -0
- package/dist/entries.d.ts +13 -0
- package/dist/entries.d.ts.map +1 -0
- package/dist/entries.js +18 -0
- package/dist/entries.js.map +1 -0
- package/dist/env/index.d.ts +131 -0
- package/dist/env/index.d.ts.map +1 -0
- package/dist/env/index.js +45 -0
- package/dist/env/index.js.map +1 -0
- package/dist/env/node.d.ts +45 -0
- package/dist/env/node.d.ts.map +1 -0
- package/dist/env/node.js +908 -0
- package/dist/env/node.js.map +1 -0
- package/dist/env/utils/adaptive-publisher.d.ts +24 -0
- package/dist/env/utils/adaptive-publisher.d.ts.map +1 -0
- package/dist/env/utils/adaptive-publisher.js +79 -0
- package/dist/env/utils/adaptive-publisher.js.map +1 -0
- package/dist/env/utils/output-capture.d.ts +31 -0
- package/dist/env/utils/output-capture.d.ts.map +1 -0
- package/dist/env/utils/output-capture.js +238 -0
- package/dist/env/utils/output-capture.js.map +1 -0
- package/dist/env/utils/truncate.d.ts +71 -0
- package/dist/env/utils/truncate.d.ts.map +1 -0
- package/dist/env/utils/truncate.js +294 -0
- package/dist/env/utils/truncate.js.map +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -0
- package/dist/harness/config.d.ts +20 -0
- package/dist/harness/config.d.ts.map +1 -0
- package/dist/harness/config.js +18 -0
- package/dist/harness/config.js.map +1 -0
- package/dist/harness/context.d.ts +27 -0
- package/dist/harness/context.d.ts.map +1 -0
- package/dist/harness/context.js +109 -0
- package/dist/harness/context.js.map +1 -0
- package/dist/harness/generation.d.ts +36 -0
- package/dist/harness/generation.d.ts.map +1 -0
- package/dist/harness/generation.js +266 -0
- package/dist/harness/generation.js.map +1 -0
- package/dist/harness/harness.d.ts +10 -0
- package/dist/harness/harness.d.ts.map +1 -0
- package/dist/harness/harness.js +266 -0
- package/dist/harness/harness.js.map +1 -0
- package/dist/harness/live.d.ts +40 -0
- package/dist/harness/live.d.ts.map +1 -0
- package/dist/harness/live.js +41 -0
- package/dist/harness/live.js.map +1 -0
- package/dist/harness/prompt.d.ts +24 -0
- package/dist/harness/prompt.d.ts.map +1 -0
- package/dist/harness/prompt.js +88 -0
- package/dist/harness/prompt.js.map +1 -0
- package/dist/harness/registry.d.ts +7 -0
- package/dist/harness/registry.d.ts.map +1 -0
- package/dist/harness/registry.js +309 -0
- package/dist/harness/registry.js.map +1 -0
- package/dist/harness/scheduler.d.ts +63 -0
- package/dist/harness/scheduler.d.ts.map +1 -0
- package/dist/harness/scheduler.js +605 -0
- package/dist/harness/scheduler.js.map +1 -0
- package/dist/harness/submissions.d.ts +24 -0
- package/dist/harness/submissions.d.ts.map +1 -0
- package/dist/harness/submissions.js +133 -0
- package/dist/harness/submissions.js.map +1 -0
- package/dist/harness/types.d.ts +358 -0
- package/dist/harness/types.d.ts.map +1 -0
- package/dist/harness/types.js +2 -0
- package/dist/harness/types.js.map +1 -0
- package/dist/harness/util.d.ts +14 -0
- package/dist/harness/util.d.ts.map +1 -0
- package/dist/harness/util.js +55 -0
- package/dist/harness/util.js.map +1 -0
- package/dist/ids.d.ts +6 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +9 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/session/forks.d.ts +10 -0
- package/dist/session/forks.d.ts.map +1 -0
- package/dist/session/forks.js +42 -0
- package/dist/session/forks.js.map +1 -0
- package/dist/session/observation.d.ts +28 -0
- package/dist/session/observation.d.ts.map +1 -0
- package/dist/session/observation.js +280 -0
- package/dist/session/observation.js.map +1 -0
- package/dist/session/session.d.ts +59 -0
- package/dist/session/session.d.ts.map +1 -0
- package/dist/session/session.js +380 -0
- package/dist/session/session.js.map +1 -0
- package/dist/session/transaction.d.ts +105 -0
- package/dist/session/transaction.d.ts.map +1 -0
- package/dist/session/transaction.js +718 -0
- package/dist/session/transaction.js.map +1 -0
- package/dist/storage/jsonl/index.d.ts +2 -0
- package/dist/storage/jsonl/index.d.ts.map +1 -0
- package/dist/storage/jsonl/index.js +2 -0
- package/dist/storage/jsonl/index.js.map +1 -0
- package/dist/storage/jsonl/node.d.ts +6 -0
- package/dist/storage/jsonl/node.d.ts.map +1 -0
- package/dist/storage/jsonl/node.js +8 -0
- package/dist/storage/jsonl/node.js.map +1 -0
- package/dist/storage/jsonl/storage.d.ts +115 -0
- package/dist/storage/jsonl/storage.d.ts.map +1 -0
- package/dist/storage/jsonl/storage.js +681 -0
- package/dist/storage/jsonl/storage.js.map +1 -0
- package/dist/storage/memory.d.ts +60 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +681 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/sqlite/database.d.ts +30 -0
- package/dist/storage/sqlite/database.d.ts.map +1 -0
- package/dist/storage/sqlite/database.js +2 -0
- package/dist/storage/sqlite/database.js.map +1 -0
- package/dist/storage/sqlite/index.d.ts +4 -0
- package/dist/storage/sqlite/index.d.ts.map +1 -0
- package/dist/storage/sqlite/index.js +3 -0
- package/dist/storage/sqlite/index.js.map +1 -0
- package/dist/storage/sqlite/migrations.d.ts +11 -0
- package/dist/storage/sqlite/migrations.d.ts.map +1 -0
- package/dist/storage/sqlite/migrations.js +111 -0
- package/dist/storage/sqlite/migrations.js.map +1 -0
- package/dist/storage/sqlite/node.d.ts +25 -0
- package/dist/storage/sqlite/node.d.ts.map +1 -0
- package/dist/storage/sqlite/node.js +97 -0
- package/dist/storage/sqlite/node.js.map +1 -0
- package/dist/storage/sqlite/storage.d.ts +51 -0
- package/dist/storage/sqlite/storage.d.ts.map +1 -0
- package/dist/storage/sqlite/storage.js +628 -0
- package/dist/storage/sqlite/storage.js.map +1 -0
- package/dist/tasks.d.ts +6 -0
- package/dist/tasks.d.ts.map +1 -0
- package/dist/tasks.js +5 -0
- package/dist/tasks.js.map +1 -0
- package/dist/testing/assertions.d.ts +5 -0
- package/dist/testing/assertions.d.ts.map +1 -0
- package/dist/testing/assertions.js +25 -0
- package/dist/testing/assertions.js.map +1 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/runner.d.ts +10 -0
- package/dist/testing/runner.d.ts.map +1 -0
- package/dist/testing/runner.js +14 -0
- package/dist/testing/runner.js.map +1 -0
- package/dist/testing/storage-benchmark.d.ts +41 -0
- package/dist/testing/storage-benchmark.d.ts.map +1 -0
- package/dist/testing/storage-benchmark.js +351 -0
- package/dist/testing/storage-benchmark.js.map +1 -0
- package/dist/testing/storage-conformance.d.ts +4 -0
- package/dist/testing/storage-conformance.d.ts.map +1 -0
- package/dist/testing/storage-conformance.js +1122 -0
- package/dist/testing/storage-conformance.js.map +1 -0
- package/dist/testing/types.d.ts +19 -0
- package/dist/testing/types.d.ts.map +1 -0
- package/dist/testing/types.js +2 -0
- package/dist/testing/types.js.map +1 -0
- package/dist/types.d.ts +525 -78
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/chord-delta-findings.md +1 -1
- package/docs/pico-v5-chord-usage.md +90 -102
- package/docs/pico-v5-handoff.md +493 -253
- package/docs/pico-v5.md +1901 -848
- package/package.json +47 -5
- package/dist/memory-storage.d.ts +0 -37
- package/dist/memory-storage.d.ts.map +0 -1
- package/dist/memory-storage.js +0 -322
- package/dist/memory-storage.js.map +0 -1
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import { apply } from "@earendil-works/chord/delta";
|
|
2
|
+
import { StorageRejected } from "../../errors.js";
|
|
3
|
+
import { idFromNumber, seqFromNumber } from "../../ids.js";
|
|
4
|
+
import { applySqliteMigrations } from "./migrations.js";
|
|
5
|
+
const parseJson = (value) => JSON.parse(value);
|
|
6
|
+
const encodeJson = (value) => JSON.stringify(value);
|
|
7
|
+
// Some SQLite bindings replace lone UTF-16 surrogates. JSON encoding keeps indexed identities lossless.
|
|
8
|
+
const encodeIndexedString = (value) => JSON.stringify(value);
|
|
9
|
+
const getRow = (statement, ...params) => statement.get(...params);
|
|
10
|
+
const allRows = (statement, ...params) => statement.all(...params);
|
|
11
|
+
const cursorId = (cursor) => {
|
|
12
|
+
const after = cursor?.after;
|
|
13
|
+
if (after === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (typeof after !== "number" || !Number.isSafeInteger(after))
|
|
16
|
+
throw new TypeError("Invalid storage cursor");
|
|
17
|
+
return idFromNumber(after);
|
|
18
|
+
};
|
|
19
|
+
const page = (values, limit) => {
|
|
20
|
+
const items = values.slice(0, limit);
|
|
21
|
+
if (values.length <= limit)
|
|
22
|
+
return { items };
|
|
23
|
+
return { items, next: { after: items.at(-1).id } };
|
|
24
|
+
};
|
|
25
|
+
const scopeColumns = (scope) => {
|
|
26
|
+
switch (scope.kind) {
|
|
27
|
+
case "session":
|
|
28
|
+
return { scopeKind: "session", ownerId: 0 };
|
|
29
|
+
case "conversation":
|
|
30
|
+
return { scopeKind: "conversation", ownerId: scope.conversationId };
|
|
31
|
+
case "task":
|
|
32
|
+
return { scopeKind: "task", ownerId: scope.taskId };
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const addressParts = (address) => {
|
|
36
|
+
const scope = scopeColumns(address.scope);
|
|
37
|
+
return {
|
|
38
|
+
kind: encodeIndexedString(address.kind),
|
|
39
|
+
...scope,
|
|
40
|
+
family: address.key === undefined ? 0 : 1,
|
|
41
|
+
keyValue: encodeIndexedString(address.key ?? ""),
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
const addressKey = (address) => {
|
|
45
|
+
const parts = addressParts(address);
|
|
46
|
+
return JSON.stringify([parts.kind, parts.scopeKind, parts.ownerId, parts.family, parts.keyValue]);
|
|
47
|
+
};
|
|
48
|
+
const isAliveAt = (record, at) => {
|
|
49
|
+
if (at === "current")
|
|
50
|
+
return record.retiredAt === undefined;
|
|
51
|
+
return record.createdAt <= at && (record.retiredAt === undefined || at < record.retiredAt);
|
|
52
|
+
};
|
|
53
|
+
const isCurrentOnly = (record) => record.scope.kind !== "conversation" || record.history === "latest";
|
|
54
|
+
const writeId = (write) => {
|
|
55
|
+
switch (write.type) {
|
|
56
|
+
case "conversation":
|
|
57
|
+
case "entry":
|
|
58
|
+
case "task":
|
|
59
|
+
case "submission":
|
|
60
|
+
return write.value.id;
|
|
61
|
+
case "document.create":
|
|
62
|
+
case "document.copy":
|
|
63
|
+
return write.record.id;
|
|
64
|
+
case "document.change":
|
|
65
|
+
case "document.retire":
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
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
|
+
/** Portable SQLite implementation of the Pico storage contract. */
|
|
95
|
+
export class SqliteStorage {
|
|
96
|
+
db;
|
|
97
|
+
nextId;
|
|
98
|
+
closed = false;
|
|
99
|
+
constructor(db, nextId) {
|
|
100
|
+
this.db = new StatementCachingDatabase(db);
|
|
101
|
+
this.nextId = nextId;
|
|
102
|
+
}
|
|
103
|
+
/** Initialize storage over an owned SQLite database facade. */
|
|
104
|
+
static async open(db) {
|
|
105
|
+
try {
|
|
106
|
+
await applySqliteMigrations(db);
|
|
107
|
+
const metadata = getRow(db.prepare("SELECT next_id, next_seq FROM durable_metadata WHERE singleton = 1"));
|
|
108
|
+
if (metadata === undefined)
|
|
109
|
+
throw new Error("Durable SQLite metadata is missing");
|
|
110
|
+
return new SqliteStorage(db, Number(metadata.next_id));
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
try {
|
|
114
|
+
await db.close();
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Preserve the initialization failure.
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async commit(writes, _context) {
|
|
123
|
+
this.assertOpen();
|
|
124
|
+
const documentActions = this.prepareDocumentActions(writes);
|
|
125
|
+
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"));
|
|
128
|
+
if (metadata === undefined)
|
|
129
|
+
throw new Error("Durable SQLite metadata is missing");
|
|
130
|
+
const committedSeq = seqFromNumber(metadata.next_seq);
|
|
131
|
+
this.checkGlobalIds(writes);
|
|
132
|
+
this.checkDocumentActions(documentActions);
|
|
133
|
+
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);
|
|
139
|
+
return committedSeq;
|
|
140
|
+
});
|
|
141
|
+
this.nextId = Math.max(this.nextId, candidateNextId);
|
|
142
|
+
return seq;
|
|
143
|
+
}
|
|
144
|
+
async mintId() {
|
|
145
|
+
this.assertOpen();
|
|
146
|
+
if (!Number.isSafeInteger(this.nextId))
|
|
147
|
+
throw new Error("ID space is exhausted");
|
|
148
|
+
return idFromNumber(this.nextId++);
|
|
149
|
+
}
|
|
150
|
+
async conversation(id, _context) {
|
|
151
|
+
this.assertOpen();
|
|
152
|
+
const row = getRow(this.db.prepare("SELECT record FROM conversations WHERE id = ?"), id);
|
|
153
|
+
return row === undefined ? undefined : parseJson(row.record);
|
|
154
|
+
}
|
|
155
|
+
async scanConversations(query, limit, cursor, _context) {
|
|
156
|
+
this.assertOpen();
|
|
157
|
+
const clauses = ["id > ?"];
|
|
158
|
+
const params = [cursorId(cursor) ?? -1];
|
|
159
|
+
if (query.ownerConversationId !== undefined) {
|
|
160
|
+
clauses.push("owner_conversation_id = ?");
|
|
161
|
+
params.push(query.ownerConversationId);
|
|
162
|
+
}
|
|
163
|
+
if (query.ownerTaskId !== undefined) {
|
|
164
|
+
clauses.push("owner_task_id = ?");
|
|
165
|
+
params.push(query.ownerTaskId);
|
|
166
|
+
}
|
|
167
|
+
params.push(limit + 1);
|
|
168
|
+
const rows = allRows(this.db.prepare(`SELECT record FROM conversations WHERE ${clauses.join(" AND ")} ORDER BY id LIMIT ?`), ...params);
|
|
169
|
+
return page(rows.map((row) => parseJson(row.record)), limit);
|
|
170
|
+
}
|
|
171
|
+
async entry(idOrConversationId, idOrContext, context) {
|
|
172
|
+
this.assertOpen();
|
|
173
|
+
const id = context === undefined
|
|
174
|
+
? idFromNumber(idOrConversationId)
|
|
175
|
+
: typeof idOrContext === "number"
|
|
176
|
+
? idFromNumber(idOrContext)
|
|
177
|
+
: undefined;
|
|
178
|
+
if (id === undefined)
|
|
179
|
+
throw new TypeError("Storage.entry() requires an entry ID");
|
|
180
|
+
let conversation;
|
|
181
|
+
if (context !== undefined) {
|
|
182
|
+
const conversationId = idFromNumber(idOrConversationId);
|
|
183
|
+
conversation = this.readConversation(conversationId);
|
|
184
|
+
if (conversation === undefined)
|
|
185
|
+
throw new Error(`Unknown conversation: ${conversationId}`);
|
|
186
|
+
}
|
|
187
|
+
const row = getRow(this.db.prepare("SELECT record, commit_seq FROM entries WHERE id = ?"), id);
|
|
188
|
+
if (row === undefined)
|
|
189
|
+
return undefined;
|
|
190
|
+
const entry = parseJson(row.record);
|
|
191
|
+
if (conversation !== undefined) {
|
|
192
|
+
let upperEntryId = Number.POSITIVE_INFINITY;
|
|
193
|
+
while (conversation.id !== entry.conversationId) {
|
|
194
|
+
if (conversation.parent === undefined)
|
|
195
|
+
return undefined;
|
|
196
|
+
upperEntryId = Math.min(upperEntryId, conversation.parent.at);
|
|
197
|
+
conversation = this.readConversation(conversation.parent.conversationId);
|
|
198
|
+
}
|
|
199
|
+
if (entry.id > upperEntryId)
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
return { entry, commitSeq: seqFromNumber(row.commit_seq) };
|
|
203
|
+
}
|
|
204
|
+
async findLatestHeadMarker(conversationId, atOrBeforeEntryId, _context) {
|
|
205
|
+
this.assertOpen();
|
|
206
|
+
let conversation = this.readConversation(conversationId);
|
|
207
|
+
if (conversation === undefined)
|
|
208
|
+
throw new Error(`Unknown conversation: ${conversationId}`);
|
|
209
|
+
let upper = atOrBeforeEntryId;
|
|
210
|
+
while (true) {
|
|
211
|
+
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);
|
|
214
|
+
if (row !== undefined)
|
|
215
|
+
return parseJson(row.record);
|
|
216
|
+
if (conversation.parent === undefined)
|
|
217
|
+
return undefined;
|
|
218
|
+
upper = upper === undefined ? conversation.parent.at : Math.min(upper, conversation.parent.at);
|
|
219
|
+
conversation = this.readConversation(conversation.parent.conversationId);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async scanEntries(query, limit, cursor, _context) {
|
|
223
|
+
this.assertOpen();
|
|
224
|
+
let conversation = this.readConversation(query.conversationId);
|
|
225
|
+
if (conversation === undefined)
|
|
226
|
+
throw new Error(`Unknown conversation: ${query.conversationId}`);
|
|
227
|
+
const after = cursorId(cursor);
|
|
228
|
+
let upper = query.maxEntryId;
|
|
229
|
+
if (after !== undefined)
|
|
230
|
+
upper = Math.min(upper ?? Number.MAX_SAFE_INTEGER, after - 1);
|
|
231
|
+
const values = [];
|
|
232
|
+
while (true) {
|
|
233
|
+
const clauses = ["conversation_id = ?"];
|
|
234
|
+
const params = [conversation.id];
|
|
235
|
+
if (query.minEntryId !== undefined) {
|
|
236
|
+
clauses.push("id >= ?");
|
|
237
|
+
params.push(query.minEntryId);
|
|
238
|
+
}
|
|
239
|
+
if (upper !== undefined) {
|
|
240
|
+
clauses.push("id <= ?");
|
|
241
|
+
params.push(upper);
|
|
242
|
+
}
|
|
243
|
+
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);
|
|
245
|
+
values.push(...rows.map((row) => parseJson(row.record)));
|
|
246
|
+
if (values.length > limit || conversation.parent === undefined)
|
|
247
|
+
break;
|
|
248
|
+
upper = upper === undefined ? conversation.parent.at : Math.min(upper, conversation.parent.at);
|
|
249
|
+
if (query.minEntryId !== undefined && upper < query.minEntryId)
|
|
250
|
+
break;
|
|
251
|
+
conversation = this.readConversation(conversation.parent.conversationId);
|
|
252
|
+
}
|
|
253
|
+
return page(values, limit);
|
|
254
|
+
}
|
|
255
|
+
async task(id, _context) {
|
|
256
|
+
this.assertOpen();
|
|
257
|
+
const row = getRow(this.db.prepare("SELECT record FROM tasks WHERE id = ?"), id);
|
|
258
|
+
return row === undefined ? undefined : parseJson(row.record);
|
|
259
|
+
}
|
|
260
|
+
async scanTasks(query, limit, cursor, _context) {
|
|
261
|
+
this.assertOpen();
|
|
262
|
+
const clauses = ["id > ?"];
|
|
263
|
+
const params = [cursorId(cursor) ?? -1];
|
|
264
|
+
if (query.conversationId !== undefined) {
|
|
265
|
+
clauses.push("conversation_id = ?");
|
|
266
|
+
params.push(query.conversationId);
|
|
267
|
+
}
|
|
268
|
+
if (query.kind !== undefined) {
|
|
269
|
+
clauses.push("kind = ?");
|
|
270
|
+
params.push(encodeIndexedString(query.kind));
|
|
271
|
+
}
|
|
272
|
+
if (query.status !== undefined) {
|
|
273
|
+
clauses.push("status = ?");
|
|
274
|
+
params.push(query.status);
|
|
275
|
+
}
|
|
276
|
+
if (query.abortRequested !== undefined) {
|
|
277
|
+
clauses.push("abort_requested = ?");
|
|
278
|
+
params.push(query.abortRequested ? 1 : 0);
|
|
279
|
+
}
|
|
280
|
+
if (query.background !== undefined) {
|
|
281
|
+
clauses.push("background = ?");
|
|
282
|
+
params.push(query.background ? 1 : 0);
|
|
283
|
+
}
|
|
284
|
+
params.push(limit + 1);
|
|
285
|
+
const rows = allRows(this.db.prepare(`SELECT record FROM tasks WHERE ${clauses.join(" AND ")} ORDER BY id LIMIT ?`), ...params);
|
|
286
|
+
return page(rows.map((row) => parseJson(row.record)), limit);
|
|
287
|
+
}
|
|
288
|
+
async submission(id, _context) {
|
|
289
|
+
this.assertOpen();
|
|
290
|
+
const row = getRow(this.db.prepare("SELECT record FROM submissions WHERE id = ?"), id);
|
|
291
|
+
return row === undefined ? undefined : parseJson(row.record);
|
|
292
|
+
}
|
|
293
|
+
async scanSubmissions(query, limit, cursor, _context) {
|
|
294
|
+
this.assertOpen();
|
|
295
|
+
const clauses = ["id > ?"];
|
|
296
|
+
const params = [cursorId(cursor) ?? -1];
|
|
297
|
+
if (query.conversationId !== undefined) {
|
|
298
|
+
clauses.push("conversation_id = ?");
|
|
299
|
+
params.push(query.conversationId);
|
|
300
|
+
}
|
|
301
|
+
if (query.status !== undefined) {
|
|
302
|
+
clauses.push("status = ?");
|
|
303
|
+
params.push(query.status);
|
|
304
|
+
}
|
|
305
|
+
params.push(limit + 1);
|
|
306
|
+
const rows = allRows(this.db.prepare(`SELECT record FROM submissions WHERE ${clauses.join(" AND ")} ORDER BY id LIMIT ?`), ...params);
|
|
307
|
+
return page(rows.map((row) => parseJson(row.record)), limit);
|
|
308
|
+
}
|
|
309
|
+
async submissionByRequest(conversationId, requestId, _context) {
|
|
310
|
+
this.assertOpen();
|
|
311
|
+
const row = getRow(this.db.prepare("SELECT record FROM submissions WHERE conversation_id = ? AND request_id = ?"), conversationId, encodeIndexedString(requestId));
|
|
312
|
+
return row === undefined ? undefined : parseJson(row.record);
|
|
313
|
+
}
|
|
314
|
+
async findDocument(address, at, _context) {
|
|
315
|
+
this.assertOpen();
|
|
316
|
+
const parts = addressParts(address);
|
|
317
|
+
const statement = at === "current"
|
|
318
|
+
? this.db.prepare(`SELECT record FROM documents
|
|
319
|
+
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
|
|
322
|
+
WHERE kind = ? AND scope_kind = ? AND owner_id = ? AND family = ? AND key_value = ?
|
|
323
|
+
AND created_at <= ? AND (retired_at IS NULL OR retired_at > ?)
|
|
324
|
+
ORDER BY created_at DESC LIMIT 1`);
|
|
325
|
+
const params = [parts.kind, parts.scopeKind, parts.ownerId, parts.family, parts.keyValue];
|
|
326
|
+
if (at !== "current")
|
|
327
|
+
params.push(at, at);
|
|
328
|
+
const row = getRow(statement, ...params);
|
|
329
|
+
return row === undefined ? undefined : parseJson(row.record);
|
|
330
|
+
}
|
|
331
|
+
async document(id, at, _context) {
|
|
332
|
+
this.assertOpen();
|
|
333
|
+
return this.materializeDocument(id, at);
|
|
334
|
+
}
|
|
335
|
+
async scanDocuments(query, limit, cursor, _context) {
|
|
336
|
+
this.assertOpen();
|
|
337
|
+
const scope = scopeColumns(query.scope);
|
|
338
|
+
const clauses = ["scope_kind = ?", "owner_id = ?", "id > ?"];
|
|
339
|
+
const params = [scope.scopeKind, scope.ownerId, cursorId(cursor) ?? -1];
|
|
340
|
+
if (query.kind !== undefined) {
|
|
341
|
+
clauses.push("kind = ?");
|
|
342
|
+
params.push(encodeIndexedString(query.kind));
|
|
343
|
+
}
|
|
344
|
+
if (query.at === "current") {
|
|
345
|
+
clauses.push("retired_at IS NULL");
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
clauses.push("created_at <= ?", "(retired_at IS NULL OR retired_at > ?)");
|
|
349
|
+
params.push(query.at, query.at);
|
|
350
|
+
}
|
|
351
|
+
params.push(limit + 1);
|
|
352
|
+
const rows = allRows(this.db.prepare(`SELECT record FROM documents WHERE ${clauses.join(" AND ")} ORDER BY id LIMIT ?`), ...params);
|
|
353
|
+
return page(rows.map((row) => parseJson(row.record)), limit);
|
|
354
|
+
}
|
|
355
|
+
async close(_context) {
|
|
356
|
+
if (this.closed)
|
|
357
|
+
return;
|
|
358
|
+
this.closed = true;
|
|
359
|
+
await this.db.close();
|
|
360
|
+
}
|
|
361
|
+
readConversation(id) {
|
|
362
|
+
const row = getRow(this.db.prepare("SELECT record FROM conversations WHERE id = ?"), id);
|
|
363
|
+
return row === undefined ? undefined : parseJson(row.record);
|
|
364
|
+
}
|
|
365
|
+
materializeDocument(id, at) {
|
|
366
|
+
const row = getRow(this.db.prepare("SELECT record FROM documents WHERE id = ?"), id);
|
|
367
|
+
if (row === undefined)
|
|
368
|
+
return undefined;
|
|
369
|
+
const record = parseJson(row.record);
|
|
370
|
+
if (at !== "current" && isCurrentOnly(record)) {
|
|
371
|
+
throw new Error(`Document ${id} does not retain historical content`);
|
|
372
|
+
}
|
|
373
|
+
if (!isAliveAt(record, at))
|
|
374
|
+
return undefined;
|
|
375
|
+
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);
|
|
378
|
+
if (base === undefined)
|
|
379
|
+
throw new Error(`Document ${id} is missing a required base`);
|
|
380
|
+
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);
|
|
383
|
+
for (const revision of tail) {
|
|
384
|
+
if (revision.kind !== "delta" || revision.version !== base.version) {
|
|
385
|
+
throw new Error(`Document ${id} crosses a stored version boundary without a base`);
|
|
386
|
+
}
|
|
387
|
+
value = apply(value, parseJson(revision.content));
|
|
388
|
+
}
|
|
389
|
+
return { record, version: base.version, value, deltasSinceBase: tail.length };
|
|
390
|
+
}
|
|
391
|
+
candidateNextId(writes) {
|
|
392
|
+
let nextId = this.nextId;
|
|
393
|
+
for (const write of writes) {
|
|
394
|
+
const id = writeId(write);
|
|
395
|
+
if (id !== undefined)
|
|
396
|
+
nextId = Math.max(nextId, id + 1);
|
|
397
|
+
}
|
|
398
|
+
return nextId;
|
|
399
|
+
}
|
|
400
|
+
checkGlobalIds(writes) {
|
|
401
|
+
const claimed = new Map();
|
|
402
|
+
const lookup = this.db.prepare("SELECT record_type FROM record_ids WHERE id = ?");
|
|
403
|
+
for (const write of writes) {
|
|
404
|
+
if (write.type === "document.change" || write.type === "document.retire")
|
|
405
|
+
continue;
|
|
406
|
+
const document = write.type === "document.create" || write.type === "document.copy";
|
|
407
|
+
const table = document ? "document" : write.type;
|
|
408
|
+
const id = document ? write.record.id : write.value.id;
|
|
409
|
+
const existing = getRow(lookup, id)?.record_type;
|
|
410
|
+
const earlier = claimed.get(id);
|
|
411
|
+
if (table === "conversation" || table === "entry" || table === "document") {
|
|
412
|
+
if (existing !== undefined)
|
|
413
|
+
throw new Error(`ID ${id} already belongs to ${existing}`);
|
|
414
|
+
if (earlier !== undefined)
|
|
415
|
+
throw new Error(`ID ${id} is written more than once`);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
if (existing !== undefined && existing !== table)
|
|
419
|
+
throw new Error(`ID ${id} already belongs to ${existing}`);
|
|
420
|
+
if (earlier !== undefined && earlier !== table)
|
|
421
|
+
throw new Error(`ID ${id} is written as two record types`);
|
|
422
|
+
}
|
|
423
|
+
claimed.set(id, table);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
prepareDocumentActions(writes) {
|
|
427
|
+
const actions = new Map();
|
|
428
|
+
for (const write of writes) {
|
|
429
|
+
if (write.type !== "document.create" &&
|
|
430
|
+
write.type !== "document.copy" &&
|
|
431
|
+
write.type !== "document.change" &&
|
|
432
|
+
write.type !== "document.retire") {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const id = write.type === "document.create" || write.type === "document.copy" ? write.record.id : write.id;
|
|
436
|
+
let action = actions.get(id);
|
|
437
|
+
if (action === undefined) {
|
|
438
|
+
action = { retire: false };
|
|
439
|
+
actions.set(id, action);
|
|
440
|
+
}
|
|
441
|
+
switch (write.type) {
|
|
442
|
+
case "document.create":
|
|
443
|
+
if (action.create !== undefined || action.content !== undefined || action.copy !== undefined) {
|
|
444
|
+
throw new Error(`Document ${id} has more than one content command`);
|
|
445
|
+
}
|
|
446
|
+
action.create = write.record;
|
|
447
|
+
action.content = write.content;
|
|
448
|
+
break;
|
|
449
|
+
case "document.copy":
|
|
450
|
+
if (action.create !== undefined || action.content !== undefined || action.copy !== undefined) {
|
|
451
|
+
throw new Error(`Document ${id} has more than one content command`);
|
|
452
|
+
}
|
|
453
|
+
action.create = write.record;
|
|
454
|
+
action.copy = write.source;
|
|
455
|
+
break;
|
|
456
|
+
case "document.change":
|
|
457
|
+
if (action.content !== undefined || action.copy !== undefined) {
|
|
458
|
+
throw new Error(`Document ${id} has more than one content command`);
|
|
459
|
+
}
|
|
460
|
+
action.content = write.content;
|
|
461
|
+
break;
|
|
462
|
+
case "document.retire":
|
|
463
|
+
if (action.retire)
|
|
464
|
+
throw new Error(`Document ${id} is retired more than once`);
|
|
465
|
+
action.retire = true;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return actions;
|
|
470
|
+
}
|
|
471
|
+
checkDocumentActions(actions) {
|
|
472
|
+
const liveCounts = new Map();
|
|
473
|
+
for (const [id, action] of actions) {
|
|
474
|
+
if (action.copy !== undefined && actions.has(action.copy.id)) {
|
|
475
|
+
throw new StorageRejected(`Document copy ${id} source is changed in the copy batch`);
|
|
476
|
+
}
|
|
477
|
+
const row = getRow(this.db.prepare("SELECT record FROM documents WHERE id = ?"), id);
|
|
478
|
+
const existing = row === undefined ? undefined : parseJson(row.record);
|
|
479
|
+
if (action.create === undefined && existing === undefined)
|
|
480
|
+
throw new Error(`Unknown document: ${id}`);
|
|
481
|
+
if (action.create !== undefined && existing !== undefined)
|
|
482
|
+
throw new Error(`Document ${id} already exists`);
|
|
483
|
+
if (existing?.retiredAt !== undefined)
|
|
484
|
+
throw new Error(`Document ${id} is retired`);
|
|
485
|
+
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);
|
|
487
|
+
if (previous === undefined)
|
|
488
|
+
throw new Error(`Document ${id} delta has no base`);
|
|
489
|
+
if (previous.version !== action.content.version) {
|
|
490
|
+
throw new Error(`Document ${id} version transition requires a base`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const record = action.create ?? existing;
|
|
494
|
+
const key = addressKey(record);
|
|
495
|
+
let live = liveCounts.get(key);
|
|
496
|
+
if (live === undefined)
|
|
497
|
+
live = this.currentDocumentId(record) === undefined ? 0 : 1;
|
|
498
|
+
if (action.retire && existing !== undefined)
|
|
499
|
+
live--;
|
|
500
|
+
if (action.create !== undefined && !action.retire)
|
|
501
|
+
live++;
|
|
502
|
+
liveCounts.set(key, live);
|
|
503
|
+
}
|
|
504
|
+
for (const live of liveCounts.values()) {
|
|
505
|
+
if (live > 1)
|
|
506
|
+
throw new Error("Document address already has a current incarnation");
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
currentDocumentId(address) {
|
|
510
|
+
const parts = addressParts(address);
|
|
511
|
+
const id = getRow(this.db.prepare(`SELECT id FROM documents
|
|
512
|
+
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;
|
|
514
|
+
return id === undefined ? undefined : idFromNumber(id);
|
|
515
|
+
}
|
|
516
|
+
applyTableWrite(write, seq) {
|
|
517
|
+
switch (write.type) {
|
|
518
|
+
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));
|
|
523
|
+
break;
|
|
524
|
+
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));
|
|
529
|
+
break;
|
|
530
|
+
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)
|
|
534
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
535
|
+
ON CONFLICT(id) DO UPDATE SET conversation_id = excluded.conversation_id, kind = excluded.kind,
|
|
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));
|
|
539
|
+
break;
|
|
540
|
+
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 (?, ?, ?, ?, ?)
|
|
544
|
+
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));
|
|
547
|
+
break;
|
|
548
|
+
case "document.create":
|
|
549
|
+
case "document.copy":
|
|
550
|
+
case "document.change":
|
|
551
|
+
case "document.retire":
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
claimId(id, table) {
|
|
556
|
+
this.db.prepare("INSERT OR IGNORE INTO record_ids (id, record_type) VALUES (?, ?)").run(id, table);
|
|
557
|
+
}
|
|
558
|
+
applyDocumentActions(actions, seq) {
|
|
559
|
+
for (const [id, action] of actions) {
|
|
560
|
+
let content = action.content;
|
|
561
|
+
if (action.copy !== undefined) {
|
|
562
|
+
try {
|
|
563
|
+
const stored = this.materializeDocument(action.copy.id, action.copy.at);
|
|
564
|
+
if (stored === undefined)
|
|
565
|
+
throw new Error(`Fork source document ${action.copy.id} cannot be read`);
|
|
566
|
+
const create = action.create;
|
|
567
|
+
if (stored.record.scope.kind !== "conversation" ||
|
|
568
|
+
create.scope.kind !== "conversation" ||
|
|
569
|
+
stored.record.kind !== create.kind ||
|
|
570
|
+
stored.record.key !== create.key ||
|
|
571
|
+
stored.record.history !== create.history ||
|
|
572
|
+
stored.record.fork !== create.fork) {
|
|
573
|
+
throw new Error(`Fork source document ${action.copy.id} does not match the copied record`);
|
|
574
|
+
}
|
|
575
|
+
content = { kind: "base", version: stored.version, value: stored.value };
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
if (error instanceof StorageRejected)
|
|
579
|
+
throw error;
|
|
580
|
+
throw new StorageRejected(`Document copy ${id} was rejected`, { cause: error });
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
let record;
|
|
584
|
+
if (action.create !== undefined) {
|
|
585
|
+
record = {
|
|
586
|
+
...action.create,
|
|
587
|
+
createdAt: seq,
|
|
588
|
+
...(action.retire ? { retiredAt: seq } : {}),
|
|
589
|
+
};
|
|
590
|
+
const parts = addressParts(record);
|
|
591
|
+
this.claimId(id, "document");
|
|
592
|
+
this.db
|
|
593
|
+
.prepare(`INSERT INTO documents
|
|
594
|
+
(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));
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
const row = getRow(this.db.prepare("SELECT record FROM documents WHERE id = ?"), id);
|
|
600
|
+
record = parseJson(row.record);
|
|
601
|
+
}
|
|
602
|
+
if (content !== undefined) {
|
|
603
|
+
if (content.kind === "base" && isCurrentOnly(record)) {
|
|
604
|
+
this.db.prepare("DELETE FROM document_revisions WHERE document_id = ?").run(id);
|
|
605
|
+
}
|
|
606
|
+
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);
|
|
610
|
+
}
|
|
611
|
+
if (action.retire) {
|
|
612
|
+
if (action.create === undefined) {
|
|
613
|
+
record = { ...record, retiredAt: seq };
|
|
614
|
+
this.db
|
|
615
|
+
.prepare("UPDATE documents SET retired_at = ?, record = ? WHERE id = ?")
|
|
616
|
+
.run(seq, encodeJson(record), id);
|
|
617
|
+
}
|
|
618
|
+
if (isCurrentOnly(record))
|
|
619
|
+
this.db.prepare("DELETE FROM document_revisions WHERE document_id = ?").run(id);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
assertOpen() {
|
|
624
|
+
if (this.closed)
|
|
625
|
+
throw new Error("SqliteStorage is closed");
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
//# sourceMappingURL=storage.js.map
|