@earendil-works/pi-durable 1.2.0 → 1.4.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 (142) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +128 -3
  3. package/dist/codemode/codemode.d.ts.map +1 -1
  4. package/dist/codemode/codemode.js +12 -4
  5. package/dist/codemode/codemode.js.map +1 -1
  6. package/dist/codemode/models.d.ts.map +1 -1
  7. package/dist/codemode/models.js +2 -0
  8. package/dist/codemode/models.js.map +1 -1
  9. package/dist/documents.d.ts +1 -1
  10. package/dist/documents.d.ts.map +1 -1
  11. package/dist/documents.js.map +1 -1
  12. package/dist/env/index.d.ts +1 -0
  13. package/dist/env/index.d.ts.map +1 -1
  14. package/dist/env/index.js.map +1 -1
  15. package/dist/env/node.d.ts.map +1 -1
  16. package/dist/env/node.js +11 -3
  17. package/dist/env/node.js.map +1 -1
  18. package/dist/harness/agent.d.ts +13 -2
  19. package/dist/harness/agent.d.ts.map +1 -1
  20. package/dist/harness/agent.js +39 -5
  21. package/dist/harness/agent.js.map +1 -1
  22. package/dist/harness/compaction.d.ts +3 -1
  23. package/dist/harness/compaction.d.ts.map +1 -1
  24. package/dist/harness/compaction.js +19 -4
  25. package/dist/harness/compaction.js.map +1 -1
  26. package/dist/harness/context.d.ts +5 -0
  27. package/dist/harness/context.d.ts.map +1 -1
  28. package/dist/harness/context.js +46 -12
  29. package/dist/harness/context.js.map +1 -1
  30. package/dist/harness/generation.d.ts.map +1 -1
  31. package/dist/harness/generation.js +67 -27
  32. package/dist/harness/generation.js.map +1 -1
  33. package/dist/harness/harness.d.ts +5 -2
  34. package/dist/harness/harness.d.ts.map +1 -1
  35. package/dist/harness/harness.js +78 -13
  36. package/dist/harness/harness.js.map +1 -1
  37. package/dist/harness/inbox.d.ts +2 -2
  38. package/dist/harness/inbox.d.ts.map +1 -1
  39. package/dist/harness/inbox.js +3 -3
  40. package/dist/harness/inbox.js.map +1 -1
  41. package/dist/harness/prompt.d.ts +1 -1
  42. package/dist/harness/prompt.d.ts.map +1 -1
  43. package/dist/harness/prompt.js +1 -1
  44. package/dist/harness/prompt.js.map +1 -1
  45. package/dist/harness/scheduler.d.ts +55 -10
  46. package/dist/harness/scheduler.d.ts.map +1 -1
  47. package/dist/harness/scheduler.js +617 -124
  48. package/dist/harness/scheduler.js.map +1 -1
  49. package/dist/harness/submissions.d.ts +7 -2
  50. package/dist/harness/submissions.d.ts.map +1 -1
  51. package/dist/harness/submissions.js +48 -17
  52. package/dist/harness/submissions.js.map +1 -1
  53. package/dist/harness/task-graph.d.ts +18 -1
  54. package/dist/harness/task-graph.d.ts.map +1 -1
  55. package/dist/harness/task-graph.js +50 -11
  56. package/dist/harness/task-graph.js.map +1 -1
  57. package/dist/harness/telemetry.d.ts +307 -0
  58. package/dist/harness/telemetry.d.ts.map +1 -0
  59. package/dist/harness/telemetry.js +136 -0
  60. package/dist/harness/telemetry.js.map +1 -0
  61. package/dist/harness/tool.d.ts.map +1 -1
  62. package/dist/harness/tool.js +54 -8
  63. package/dist/harness/tool.js.map +1 -1
  64. package/dist/harness/types.d.ts +110 -15
  65. package/dist/harness/types.d.ts.map +1 -1
  66. package/dist/harness/types.js.map +1 -1
  67. package/dist/harness/util.d.ts +4 -0
  68. package/dist/harness/util.d.ts.map +1 -1
  69. package/dist/harness/util.js +4 -0
  70. package/dist/harness/util.js.map +1 -1
  71. package/dist/index.d.ts +6 -4
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +3 -2
  74. package/dist/index.js.map +1 -1
  75. package/dist/session/session.d.ts +17 -6
  76. package/dist/session/session.d.ts.map +1 -1
  77. package/dist/session/session.js +64 -32
  78. package/dist/session/session.js.map +1 -1
  79. package/dist/session/transaction.d.ts +5 -1
  80. package/dist/session/transaction.d.ts.map +1 -1
  81. package/dist/session/transaction.js +26 -1
  82. package/dist/session/transaction.js.map +1 -1
  83. package/dist/storage/indexeddb.d.ts +60 -0
  84. package/dist/storage/indexeddb.d.ts.map +1 -0
  85. package/dist/storage/indexeddb.js +831 -0
  86. package/dist/storage/indexeddb.js.map +1 -0
  87. package/dist/storage/jsonl/storage.d.ts +16 -0
  88. package/dist/storage/jsonl/storage.d.ts.map +1 -1
  89. package/dist/storage/memory.js +1 -1
  90. package/dist/storage/memory.js.map +1 -1
  91. package/dist/storage/sqlite/cloudflare.d.ts +6 -43
  92. package/dist/storage/sqlite/cloudflare.d.ts.map +1 -1
  93. package/dist/storage/sqlite/cloudflare.js +6 -101
  94. package/dist/storage/sqlite/cloudflare.js.map +1 -1
  95. package/dist/storage/sqlite/database.d.ts +1 -36
  96. package/dist/storage/sqlite/database.d.ts.map +1 -1
  97. package/dist/storage/sqlite/database.js.map +1 -1
  98. package/dist/storage/sqlite/index.d.ts +2 -2
  99. package/dist/storage/sqlite/index.d.ts.map +1 -1
  100. package/dist/storage/sqlite/index.js.map +1 -1
  101. package/dist/storage/sqlite/migrations.d.ts +7 -6
  102. package/dist/storage/sqlite/migrations.d.ts.map +1 -1
  103. package/dist/storage/sqlite/migrations.js +116 -106
  104. package/dist/storage/sqlite/migrations.js.map +1 -1
  105. package/dist/storage/sqlite/node.d.ts +6 -39
  106. package/dist/storage/sqlite/node.d.ts.map +1 -1
  107. package/dist/storage/sqlite/node.js +8 -175
  108. package/dist/storage/sqlite/node.js.map +1 -1
  109. package/dist/storage/sqlite/schema.d.ts +45 -0
  110. package/dist/storage/sqlite/schema.d.ts.map +1 -0
  111. package/dist/storage/sqlite/schema.js +50 -0
  112. package/dist/storage/sqlite/schema.js.map +1 -0
  113. package/dist/storage/sqlite/storage.d.ts +10 -2
  114. package/dist/storage/sqlite/storage.d.ts.map +1 -1
  115. package/dist/storage/sqlite/storage.js +46 -43
  116. package/dist/storage/sqlite/storage.js.map +1 -1
  117. package/dist/testing/storage-conformance.d.ts.map +1 -1
  118. package/dist/testing/storage-conformance.js +17 -0
  119. package/dist/testing/storage-conformance.js.map +1 -1
  120. package/dist/timers.d.ts +21 -0
  121. package/dist/timers.d.ts.map +1 -0
  122. package/dist/timers.js +72 -0
  123. package/dist/timers.js.map +1 -0
  124. package/dist/tools/image-processor.d.ts +4 -1
  125. package/dist/tools/image-processor.d.ts.map +1 -1
  126. package/dist/tools/image-processor.js +8 -18
  127. package/dist/tools/image-processor.js.map +1 -1
  128. package/dist/tools/read.d.ts.map +1 -1
  129. package/dist/tools/read.js +1 -1
  130. package/dist/tools/read.js.map +1 -1
  131. package/dist/truncate.d.ts +3 -2
  132. package/dist/truncate.d.ts.map +1 -1
  133. package/dist/truncate.js +4 -34
  134. package/dist/truncate.js.map +1 -1
  135. package/dist/types.d.ts +130 -9
  136. package/dist/types.d.ts.map +1 -1
  137. package/dist/types.js.map +1 -1
  138. package/package.json +15 -5
  139. package/dist/harness/output.d.ts +0 -69
  140. package/dist/harness/output.d.ts.map +0 -1
  141. package/dist/harness/output.js +0 -318
  142. package/dist/harness/output.js.map +0 -1
@@ -0,0 +1,831 @@
1
+ /// <reference lib="dom" />
2
+ import { apply } from "@earendil-works/chord/delta";
3
+ import { StorageRequestError } from "../errors.js";
4
+ import { idFromNumber, seqFromNumber } from "../ids.js";
5
+ import { nextCursor, scanStart } from "./scan.js";
6
+ const DATABASE_VERSION = 1;
7
+ const MAX_KEY = Number.MAX_SAFE_INTEGER;
8
+ const META_KEY = "state";
9
+ const STORES = {
10
+ meta: "meta",
11
+ recordIds: "record_ids",
12
+ conversations: "conversations",
13
+ entries: "entries",
14
+ tasks: "tasks",
15
+ submissions: "submissions",
16
+ documents: "documents",
17
+ documentRevisions: "document_revisions",
18
+ };
19
+ const ALL_STORES = Object.values(STORES);
20
+ const INDEXES = {
21
+ conversationsByOwnerConversation: "conversations_by_owner_conversation",
22
+ conversationsByOwnerTask: "conversations_by_owner_task",
23
+ entriesByConversation: "entries_by_conversation",
24
+ entryHeadsByConversation: "entry_heads_by_conversation",
25
+ tasksByStatus: "tasks_by_status",
26
+ tasksByConversation: "tasks_by_conversation",
27
+ tasksByKind: "tasks_by_kind",
28
+ tasksByAbortRequested: "tasks_by_abort_requested",
29
+ tasksByBackground: "tasks_by_background",
30
+ submissionsByRequest: "submissions_by_request",
31
+ submissionsByConversation: "submissions_by_conversation",
32
+ submissionsByStatus: "submissions_by_status",
33
+ documentsByAddress: "documents_by_address",
34
+ documentsByScope: "documents_by_scope",
35
+ documentsByScopeKind: "documents_by_scope_kind",
36
+ documentRevisionsByKind: "document_revisions_by_kind",
37
+ };
38
+ const requestResult = (request) => new Promise((resolve, reject) => {
39
+ request.addEventListener("success", () => resolve(request.result), { once: true });
40
+ request.addEventListener("error", () => reject(request.error ?? new Error("IndexedDB request failed")), {
41
+ once: true,
42
+ });
43
+ });
44
+ const transactionResult = (transaction) => new Promise((resolve, reject) => {
45
+ transaction.addEventListener("complete", () => resolve(), { once: true });
46
+ transaction.addEventListener("abort", () => reject(transaction.error ?? new Error("IndexedDB transaction was aborted")), { once: true });
47
+ transaction.addEventListener("error", () => reject(transaction.error ?? new Error("IndexedDB transaction failed")), { once: true });
48
+ });
49
+ const keyRanges = () => {
50
+ if (typeof IDBKeyRange === "undefined")
51
+ throw new Error("IndexedDB key ranges are not available");
52
+ return IDBKeyRange;
53
+ };
54
+ const cursorValues = (source, range, direction, limit, accept = () => true) => new Promise((resolve, reject) => {
55
+ const values = [];
56
+ const request = source.openCursor(range, direction);
57
+ request.addEventListener("error", () => reject(request.error ?? new Error("IndexedDB cursor failed")), {
58
+ once: true,
59
+ });
60
+ request.addEventListener("success", () => {
61
+ const cursor = request.result;
62
+ if (cursor === null) {
63
+ resolve(values);
64
+ return;
65
+ }
66
+ const value = cursor.value;
67
+ if (accept(value))
68
+ values.push(value);
69
+ if (values.length >= limit) {
70
+ resolve(values);
71
+ return;
72
+ }
73
+ cursor.continue();
74
+ });
75
+ });
76
+ const firstCursorValue = (source, range, direction, accept = () => true) => new Promise((resolve, reject) => {
77
+ const request = source.openCursor(range, direction);
78
+ request.addEventListener("error", () => reject(request.error ?? new Error("IndexedDB cursor failed")), {
79
+ once: true,
80
+ });
81
+ request.addEventListener("success", () => {
82
+ const cursor = request.result;
83
+ if (cursor === null) {
84
+ resolve(undefined);
85
+ return;
86
+ }
87
+ const value = cursor.value;
88
+ if (accept(value)) {
89
+ resolve(value);
90
+ return;
91
+ }
92
+ cursor.continue();
93
+ });
94
+ });
95
+ const page = (values, limit, order = "ascending") => {
96
+ const items = values.slice(0, limit);
97
+ if (values.length <= limit)
98
+ return { items };
99
+ return { items, next: nextCursor(items.at(-1).id, order) };
100
+ };
101
+ const cursorId = (cursor) => {
102
+ const after = cursor?.after;
103
+ if (after === undefined)
104
+ return undefined;
105
+ if (typeof after !== "number" || !Number.isSafeInteger(after)) {
106
+ throw new StorageRequestError("Invalid storage cursor");
107
+ }
108
+ return idFromNumber(after);
109
+ };
110
+ const primaryRange = (start) => {
111
+ if (start.after === undefined)
112
+ return null;
113
+ return start.order === "ascending"
114
+ ? keyRanges().lowerBound(start.after, true)
115
+ : keyRanges().upperBound(start.after, true);
116
+ };
117
+ const compoundIdRange = (prefix, minimum, maximum, minimumOpen = false, maximumOpen = false) => keyRanges().bound([...prefix, minimum], [...prefix, maximum], minimumOpen, maximumOpen);
118
+ const scanCompoundRows = (index, prefix, start, limit, accept) => {
119
+ const minimum = start.order === "ascending" && start.after !== undefined ? start.after : 0;
120
+ const maximum = start.order === "descending" && start.after !== undefined ? start.after : MAX_KEY;
121
+ return cursorValues(index, compoundIdRange(prefix, minimum, maximum, start.order === "ascending" && start.after !== undefined, start.order === "descending" && start.after !== undefined), start.order === "ascending" ? "next" : "prev", limit, accept);
122
+ };
123
+ const scopeColumns = (scope) => {
124
+ switch (scope.kind) {
125
+ case "session":
126
+ return { scopeKind: "session", ownerId: 0 };
127
+ case "conversation":
128
+ return { scopeKind: "conversation", ownerId: scope.conversationId };
129
+ case "task":
130
+ return { scopeKind: "task", ownerId: scope.taskId };
131
+ }
132
+ };
133
+ const addressParts = (address) => {
134
+ const scope = scopeColumns(address.scope);
135
+ return {
136
+ kind: address.kind,
137
+ ...scope,
138
+ family: address.key === undefined ? 0 : 1,
139
+ keyValue: address.key ?? "",
140
+ };
141
+ };
142
+ const addressPrefix = (address) => {
143
+ const parts = addressParts(address);
144
+ return [parts.kind, parts.scopeKind, parts.ownerId, parts.family, parts.keyValue];
145
+ };
146
+ const addressKey = (address) => JSON.stringify(addressPrefix(address));
147
+ const isAliveAt = (record, at) => {
148
+ if (at === "current")
149
+ return record.retiredAt === undefined;
150
+ return record.createdAt <= at && (record.retiredAt === undefined || at < record.retiredAt);
151
+ };
152
+ const isCurrentOnly = (record) => record.scope.kind !== "conversation" || record.history === "latest";
153
+ const writeId = (write) => {
154
+ switch (write.type) {
155
+ case "conversation":
156
+ case "entry":
157
+ case "task":
158
+ case "submission":
159
+ return write.value.id;
160
+ case "document.create":
161
+ case "document.copy":
162
+ return write.record.id;
163
+ case "document.change":
164
+ case "document.retire":
165
+ return undefined;
166
+ }
167
+ };
168
+ function createSchema(database) {
169
+ const meta = database.createObjectStore(STORES.meta, { keyPath: "key" });
170
+ meta.add({ key: META_KEY, nextId: 2, nextSeq: 1 });
171
+ database.createObjectStore(STORES.recordIds, { keyPath: "id" });
172
+ const conversations = database.createObjectStore(STORES.conversations, { keyPath: "id" });
173
+ conversations.createIndex(INDEXES.conversationsByOwnerConversation, ["ownerConversationId", "id"]);
174
+ conversations.createIndex(INDEXES.conversationsByOwnerTask, ["ownerTaskId", "id"]);
175
+ const entries = database.createObjectStore(STORES.entries, { keyPath: "id" });
176
+ entries.createIndex(INDEXES.entriesByConversation, ["conversationId", "id"]);
177
+ entries.createIndex(INDEXES.entryHeadsByConversation, ["headConversationId", "id"]);
178
+ const tasks = database.createObjectStore(STORES.tasks, { keyPath: "id" });
179
+ tasks.createIndex(INDEXES.tasksByStatus, ["status", "id"]);
180
+ tasks.createIndex(INDEXES.tasksByConversation, ["conversationId", "id"]);
181
+ tasks.createIndex(INDEXES.tasksByKind, ["kind", "id"]);
182
+ tasks.createIndex(INDEXES.tasksByAbortRequested, ["abortRequested", "id"]);
183
+ tasks.createIndex(INDEXES.tasksByBackground, ["background", "id"]);
184
+ const submissions = database.createObjectStore(STORES.submissions, { keyPath: "id" });
185
+ submissions.createIndex(INDEXES.submissionsByRequest, ["conversationId", "requestId"]);
186
+ submissions.createIndex(INDEXES.submissionsByConversation, ["conversationId", "id"]);
187
+ submissions.createIndex(INDEXES.submissionsByStatus, ["status", "id"]);
188
+ const documents = database.createObjectStore(STORES.documents, { keyPath: "id" });
189
+ documents.createIndex(INDEXES.documentsByAddress, [
190
+ "kind",
191
+ "scopeKind",
192
+ "ownerId",
193
+ "family",
194
+ "keyValue",
195
+ "createdAt",
196
+ "id",
197
+ ]);
198
+ documents.createIndex(INDEXES.documentsByScope, ["scopeKind", "ownerId", "id"]);
199
+ documents.createIndex(INDEXES.documentsByScopeKind, ["scopeKind", "ownerId", "kind", "id"]);
200
+ const revisions = database.createObjectStore(STORES.documentRevisions, { keyPath: ["documentId", "seq"] });
201
+ revisions.createIndex(INDEXES.documentRevisionsByKind, ["documentId", "kind", "seq"]);
202
+ }
203
+ /** Browser IndexedDB implementation of the durable storage contract. One owner must serialize access to each database. */
204
+ export class IndexedDbStorage {
205
+ database;
206
+ closed = false;
207
+ closing;
208
+ activeOperations = 0;
209
+ operationsDrained;
210
+ constructor(database) {
211
+ this.database = database;
212
+ }
213
+ /** Open or create a browser-only IndexedDB database. */
214
+ static async open(name, options = {}) {
215
+ const factory = options.indexedDB ?? globalThis.indexedDB;
216
+ if (factory === undefined)
217
+ throw new Error("IndexedDB is not available");
218
+ const request = factory.open(name, DATABASE_VERSION);
219
+ request.addEventListener("upgradeneeded", (event) => {
220
+ if (event.oldVersion === 0)
221
+ createSchema(request.result);
222
+ });
223
+ const database = await requestResult(request);
224
+ try {
225
+ const transaction = database.transaction(STORES.meta, "readonly");
226
+ const done = transactionResult(transaction);
227
+ const metadata = await requestResult(transaction.objectStore(STORES.meta).get(META_KEY));
228
+ await done;
229
+ if (metadata === undefined)
230
+ throw new Error("Durable IndexedDB metadata is missing");
231
+ return new IndexedDbStorage(database);
232
+ }
233
+ catch (error) {
234
+ database.close();
235
+ throw error;
236
+ }
237
+ }
238
+ /** Delete a closed database. Open connections in this or another owner can block deletion. */
239
+ static async delete(name, options = {}) {
240
+ const factory = options.indexedDB ?? globalThis.indexedDB;
241
+ if (factory === undefined)
242
+ throw new Error("IndexedDB is not available");
243
+ await requestResult(factory.deleteDatabase(name));
244
+ }
245
+ async commit(writes, _context) {
246
+ this.assertOpen();
247
+ const documentActions = this.prepareDocumentActions(writes);
248
+ return this.transaction(ALL_STORES, "readwrite", async (transaction) => {
249
+ const metaStore = transaction.objectStore(STORES.meta);
250
+ const metadata = await requestResult(metaStore.get(META_KEY));
251
+ if (metadata === undefined)
252
+ throw new Error("Durable IndexedDB metadata is missing");
253
+ const seq = seqFromNumber(metadata.nextSeq);
254
+ await this.checkGlobalIds(transaction, writes);
255
+ await this.checkDocumentActions(transaction, documentActions);
256
+ for (const write of writes)
257
+ await this.applyTableWrite(transaction, write, seq);
258
+ await this.applyDocumentActions(transaction, documentActions, seq);
259
+ let nextId = metadata.nextId;
260
+ for (const write of writes) {
261
+ const id = writeId(write);
262
+ if (id !== undefined)
263
+ nextId = Math.max(nextId, id + 1);
264
+ }
265
+ await requestResult(metaStore.put({ key: META_KEY, nextId, nextSeq: seq + 1 }));
266
+ return seq;
267
+ });
268
+ }
269
+ async mintId() {
270
+ this.assertOpen();
271
+ return this.transaction(STORES.meta, "readwrite", async (transaction) => {
272
+ const store = transaction.objectStore(STORES.meta);
273
+ const metadata = await requestResult(store.get(META_KEY));
274
+ if (metadata === undefined)
275
+ throw new Error("Durable IndexedDB metadata is missing");
276
+ if (!Number.isSafeInteger(metadata.nextId))
277
+ throw new Error("ID space is exhausted");
278
+ await requestResult(store.put({ ...metadata, nextId: metadata.nextId + 1 }));
279
+ return idFromNumber(metadata.nextId);
280
+ });
281
+ }
282
+ conversation(id, _context) {
283
+ return this.transaction(STORES.conversations, "readonly", async (transaction) => (await requestResult(transaction.objectStore(STORES.conversations).get(id)))?.record);
284
+ }
285
+ async scanConversations(query, limit, cursor, _context) {
286
+ const start = scanStart(query.order, cursor, "ascending");
287
+ return this.transaction(STORES.conversations, "readonly", async (transaction) => {
288
+ const store = transaction.objectStore(STORES.conversations);
289
+ const accept = (row) => query.ownerConversationId === undefined || row.ownerConversationId === query.ownerConversationId;
290
+ let rows;
291
+ if (query.ownerTaskId !== undefined) {
292
+ rows = await scanCompoundRows(store.index(INDEXES.conversationsByOwnerTask), [query.ownerTaskId], start, limit + 1, accept);
293
+ }
294
+ else if (query.ownerConversationId !== undefined) {
295
+ rows = await scanCompoundRows(store.index(INDEXES.conversationsByOwnerConversation), [query.ownerConversationId], start, limit + 1, accept);
296
+ }
297
+ else {
298
+ rows = await cursorValues(store, primaryRange(start), start.order === "ascending" ? "next" : "prev", limit + 1, accept);
299
+ }
300
+ return page(rows.map((row) => row.record), limit, start.order);
301
+ });
302
+ }
303
+ entry(idOrConversationId, idOrContext, context) {
304
+ return this.transaction([STORES.conversations, STORES.entries], "readonly", async (transaction) => {
305
+ const id = context === undefined
306
+ ? idFromNumber(idOrConversationId)
307
+ : typeof idOrContext === "number"
308
+ ? idFromNumber(idOrContext)
309
+ : undefined;
310
+ if (id === undefined)
311
+ throw new StorageRequestError("Storage.entry() requires an entry ID");
312
+ let conversation;
313
+ if (context !== undefined) {
314
+ const conversationId = idFromNumber(idOrConversationId);
315
+ conversation = await this.readConversation(transaction, conversationId);
316
+ if (conversation === undefined)
317
+ throw new StorageRequestError(`Unknown conversation: ${conversationId}`);
318
+ }
319
+ const row = await requestResult(transaction.objectStore(STORES.entries).get(id));
320
+ if (row === undefined)
321
+ return undefined;
322
+ if (conversation !== undefined) {
323
+ let upperEntryId = MAX_KEY;
324
+ while (conversation.id !== row.conversationId) {
325
+ if (conversation.parent === undefined)
326
+ return undefined;
327
+ upperEntryId = Math.min(upperEntryId, conversation.parent.at);
328
+ conversation = (await this.readConversation(transaction, conversation.parent.conversationId));
329
+ }
330
+ if (row.id > upperEntryId)
331
+ return undefined;
332
+ }
333
+ return { entry: row.record, commitSeq: row.commitSeq };
334
+ });
335
+ }
336
+ findLatestHeadMarker(conversationId, atOrBeforeEntryId, _context) {
337
+ return this.transaction([STORES.conversations, STORES.entries], "readonly", async (transaction) => {
338
+ let conversation = await this.readConversation(transaction, conversationId);
339
+ if (conversation === undefined)
340
+ throw new StorageRequestError(`Unknown conversation: ${conversationId}`);
341
+ let upper = atOrBeforeEntryId ?? MAX_KEY;
342
+ const index = transaction.objectStore(STORES.entries).index(INDEXES.entryHeadsByConversation);
343
+ while (true) {
344
+ const row = await firstCursorValue(index, compoundIdRange([conversation.id], 0, upper), "prev");
345
+ if (row !== undefined)
346
+ return row.record;
347
+ if (conversation.parent === undefined)
348
+ return undefined;
349
+ upper = Math.min(upper, conversation.parent.at);
350
+ conversation = (await this.readConversation(transaction, conversation.parent.conversationId));
351
+ }
352
+ });
353
+ }
354
+ async scanEntries(query, limit, cursor, _context) {
355
+ const start = scanStart(query.order, cursor, "descending");
356
+ return this.transaction([STORES.conversations, STORES.entries], "readonly", async (transaction) => {
357
+ const values = start.order === "ascending"
358
+ ? await this.readEntriesAscending(transaction, query, limit, start.after)
359
+ : await this.readEntriesDescending(transaction, query, limit, start.after);
360
+ return page(values, limit, start.order);
361
+ });
362
+ }
363
+ task(id, _context) {
364
+ return this.transaction(STORES.tasks, "readonly", async (transaction) => (await requestResult(transaction.objectStore(STORES.tasks).get(id)))
365
+ ?.record);
366
+ }
367
+ async scanTasks(query, limit, cursor, _context) {
368
+ const start = scanStart(query.order, cursor, "ascending");
369
+ return this.transaction(STORES.tasks, "readonly", async (transaction) => {
370
+ const store = transaction.objectStore(STORES.tasks);
371
+ const accept = (row) => (query.conversationId === undefined || row.conversationId === query.conversationId) &&
372
+ (query.kind === undefined || row.kind === query.kind) &&
373
+ (query.status === undefined || row.status === query.status) &&
374
+ (query.abortRequested === undefined || row.abortRequested === (query.abortRequested ? 1 : 0)) &&
375
+ (query.background === undefined || row.background === (query.background ? 1 : 0));
376
+ let source = store;
377
+ let prefix;
378
+ if (query.status !== undefined) {
379
+ source = store.index(INDEXES.tasksByStatus);
380
+ prefix = [query.status];
381
+ }
382
+ else if (query.conversationId !== undefined) {
383
+ source = store.index(INDEXES.tasksByConversation);
384
+ prefix = [query.conversationId];
385
+ }
386
+ else if (query.kind !== undefined) {
387
+ source = store.index(INDEXES.tasksByKind);
388
+ prefix = [query.kind];
389
+ }
390
+ else if (query.abortRequested !== undefined) {
391
+ source = store.index(INDEXES.tasksByAbortRequested);
392
+ prefix = [query.abortRequested ? 1 : 0];
393
+ }
394
+ else if (query.background !== undefined) {
395
+ source = store.index(INDEXES.tasksByBackground);
396
+ prefix = [query.background ? 1 : 0];
397
+ }
398
+ const rows = prefix === undefined
399
+ ? await cursorValues(source, primaryRange(start), start.order === "ascending" ? "next" : "prev", limit + 1, accept)
400
+ : await scanCompoundRows(source, prefix, start, limit + 1, accept);
401
+ return page(rows.map((row) => row.record), limit, start.order);
402
+ });
403
+ }
404
+ submission(id, _context) {
405
+ return this.transaction(STORES.submissions, "readonly", async (transaction) => (await requestResult(transaction.objectStore(STORES.submissions).get(id)))?.record);
406
+ }
407
+ async scanSubmissions(query, limit, cursor, _context) {
408
+ const start = scanStart(query.order, cursor, "ascending");
409
+ return this.transaction(STORES.submissions, "readonly", async (transaction) => {
410
+ const store = transaction.objectStore(STORES.submissions);
411
+ const accept = (row) => (query.conversationId === undefined || row.conversationId === query.conversationId) &&
412
+ (query.status === undefined || row.status === query.status);
413
+ let rows;
414
+ if (query.status !== undefined) {
415
+ rows = await scanCompoundRows(store.index(INDEXES.submissionsByStatus), [query.status], start, limit + 1, accept);
416
+ }
417
+ else if (query.conversationId !== undefined) {
418
+ rows = await scanCompoundRows(store.index(INDEXES.submissionsByConversation), [query.conversationId], start, limit + 1, accept);
419
+ }
420
+ else {
421
+ rows = await cursorValues(store, primaryRange(start), start.order === "ascending" ? "next" : "prev", limit + 1, accept);
422
+ }
423
+ return page(rows.map((row) => row.record), limit, start.order);
424
+ });
425
+ }
426
+ submissionByRequest(conversationId, requestId, _context) {
427
+ return this.transaction(STORES.submissions, "readonly", async (transaction) => {
428
+ const row = await requestResult(transaction
429
+ .objectStore(STORES.submissions)
430
+ .index(INDEXES.submissionsByRequest)
431
+ .get([conversationId, requestId]));
432
+ return row?.record;
433
+ });
434
+ }
435
+ findDocument(address, at, _context) {
436
+ return this.transaction(STORES.documents, "readonly", async (transaction) => {
437
+ const row = await firstCursorValue(transaction.objectStore(STORES.documents).index(INDEXES.documentsByAddress), keyRanges().bound([...addressPrefix(address), 0, 0], [...addressPrefix(address), MAX_KEY, MAX_KEY]), "prev", (value) => isAliveAt(value.record, at));
438
+ return row?.record;
439
+ });
440
+ }
441
+ document(id, at, _context) {
442
+ return this.transaction([STORES.documents, STORES.documentRevisions], "readonly", (transaction) => this.materializeDocument(transaction, id, at));
443
+ }
444
+ async scanDocuments(query, limit, cursor, _context) {
445
+ const after = cursorId(cursor);
446
+ return this.transaction(STORES.documents, "readonly", async (transaction) => {
447
+ const scope = scopeColumns(query.scope);
448
+ const store = transaction.objectStore(STORES.documents);
449
+ const index = query.kind === undefined
450
+ ? store.index(INDEXES.documentsByScope)
451
+ : store.index(INDEXES.documentsByScopeKind);
452
+ const prefix = query.kind === undefined ? [scope.scopeKind, scope.ownerId] : [scope.scopeKind, scope.ownerId, query.kind];
453
+ const rows = await cursorValues(index, compoundIdRange(prefix, after ?? 0, MAX_KEY, after !== undefined), "next", limit + 1, (row) => isAliveAt(row.record, query.at));
454
+ return page(rows.map((row) => row.record), limit);
455
+ });
456
+ }
457
+ close(_context) {
458
+ if (this.closing === undefined) {
459
+ this.closed = true;
460
+ this.closing = this.closeDatabase();
461
+ }
462
+ return this.closing;
463
+ }
464
+ async closeDatabase() {
465
+ if (this.activeOperations > 0) {
466
+ await new Promise((resolve) => {
467
+ this.operationsDrained = resolve;
468
+ });
469
+ }
470
+ this.database.close();
471
+ }
472
+ async transaction(stores, mode, operation) {
473
+ this.assertOpen();
474
+ this.activeOperations++;
475
+ try {
476
+ const transaction = this.database.transaction(stores, mode);
477
+ const done = transactionResult(transaction);
478
+ try {
479
+ const value = await operation(transaction);
480
+ await done;
481
+ return value;
482
+ }
483
+ catch (error) {
484
+ try {
485
+ transaction.abort();
486
+ }
487
+ catch {
488
+ // It may already have aborted or committed.
489
+ }
490
+ try {
491
+ await done;
492
+ }
493
+ catch {
494
+ // Preserve the operation failure.
495
+ }
496
+ throw error;
497
+ }
498
+ }
499
+ finally {
500
+ if (--this.activeOperations === 0)
501
+ this.operationsDrained?.();
502
+ }
503
+ }
504
+ async readConversation(transaction, id) {
505
+ return (await requestResult(transaction.objectStore(STORES.conversations).get(id)))?.record;
506
+ }
507
+ async readEntriesDescending(transaction, query, limit, after) {
508
+ let conversation = await this.readConversation(transaction, query.conversationId);
509
+ if (conversation === undefined)
510
+ throw new StorageRequestError(`Unknown conversation: ${query.conversationId}`);
511
+ let upper = query.maxEntryId ?? MAX_KEY;
512
+ if (after !== undefined)
513
+ upper = Math.min(upper, after - 1);
514
+ const minimum = query.minEntryId ?? 0;
515
+ const values = [];
516
+ const index = transaction.objectStore(STORES.entries).index(INDEXES.entriesByConversation);
517
+ while (true) {
518
+ if (upper >= minimum) {
519
+ const rows = await cursorValues(index, compoundIdRange([conversation.id], minimum, upper), "prev", limit + 1 - values.length);
520
+ values.push(...rows.map((row) => row.record));
521
+ }
522
+ if (values.length > limit || conversation.parent === undefined)
523
+ break;
524
+ upper = Math.min(upper, conversation.parent.at);
525
+ if (upper < minimum)
526
+ break;
527
+ conversation = (await this.readConversation(transaction, conversation.parent.conversationId));
528
+ }
529
+ return values;
530
+ }
531
+ async readEntriesAscending(transaction, query, limit, after) {
532
+ const segments = [];
533
+ let conversation = await this.readConversation(transaction, query.conversationId);
534
+ if (conversation === undefined)
535
+ throw new StorageRequestError(`Unknown conversation: ${query.conversationId}`);
536
+ let upper = query.maxEntryId ?? MAX_KEY;
537
+ const requestedMinimum = query.minEntryId ?? 0;
538
+ while (true) {
539
+ segments.push({ conversationId: conversation.id, upper });
540
+ if (conversation.parent === undefined)
541
+ break;
542
+ upper = Math.min(upper, conversation.parent.at);
543
+ if (upper < requestedMinimum)
544
+ break;
545
+ conversation = (await this.readConversation(transaction, conversation.parent.conversationId));
546
+ }
547
+ const minimum = after === undefined ? requestedMinimum : Math.max(requestedMinimum, after + 1);
548
+ const values = [];
549
+ const index = transaction.objectStore(STORES.entries).index(INDEXES.entriesByConversation);
550
+ for (const segment of segments.reverse()) {
551
+ if (segment.upper < minimum)
552
+ continue;
553
+ const rows = await cursorValues(index, compoundIdRange([segment.conversationId], minimum, segment.upper), "next", limit + 1 - values.length);
554
+ values.push(...rows.map((row) => row.record));
555
+ if (values.length > limit)
556
+ break;
557
+ }
558
+ return values;
559
+ }
560
+ async materializeDocument(transaction, id, at) {
561
+ const record = (await requestResult(transaction.objectStore(STORES.documents).get(id)))?.record;
562
+ if (record === undefined)
563
+ return undefined;
564
+ if (at !== "current" && isCurrentOnly(record)) {
565
+ throw new StorageRequestError(`Document ${id} does not retain historical content`);
566
+ }
567
+ if (!isAliveAt(record, at))
568
+ return undefined;
569
+ const upper = at === "current" ? MAX_KEY : at;
570
+ const revisions = transaction.objectStore(STORES.documentRevisions);
571
+ const base = await firstCursorValue(revisions.index(INDEXES.documentRevisionsByKind), keyRanges().bound([id, "base", 0], [id, "base", upper]), "prev");
572
+ if (base === undefined || base.kind !== "base")
573
+ throw new Error(`Document ${id} is missing a required base`);
574
+ let value = base.content;
575
+ const tail = base.seq >= upper
576
+ ? []
577
+ : await cursorValues(revisions, keyRanges().bound([id, base.seq], [id, upper], true), "next", MAX_KEY);
578
+ for (const revision of tail) {
579
+ if (revision.kind !== "delta" || revision.version !== base.version) {
580
+ throw new Error(`Document ${id} crosses a stored version boundary without a base`);
581
+ }
582
+ value = apply(value, revision.content);
583
+ }
584
+ return { record, version: base.version, value, deltasSinceBase: tail.length };
585
+ }
586
+ async checkGlobalIds(transaction, writes) {
587
+ const store = transaction.objectStore(STORES.recordIds);
588
+ const claimed = new Map();
589
+ for (const write of writes) {
590
+ if (write.type === "document.change" || write.type === "document.retire")
591
+ continue;
592
+ const document = write.type === "document.create" || write.type === "document.copy";
593
+ const table = document ? "document" : write.type;
594
+ const id = document ? write.record.id : write.value.id;
595
+ const existing = (await requestResult(store.get(id)))?.recordType;
596
+ const earlier = claimed.get(id);
597
+ if (table === "conversation" || table === "entry" || table === "document") {
598
+ if (existing !== undefined)
599
+ throw new Error(`ID ${id} already belongs to ${existing}`);
600
+ if (earlier !== undefined)
601
+ throw new Error(`ID ${id} is written more than once`);
602
+ }
603
+ else {
604
+ if (existing !== undefined && existing !== table)
605
+ throw new Error(`ID ${id} already belongs to ${existing}`);
606
+ if (earlier !== undefined && earlier !== table)
607
+ throw new Error(`ID ${id} is written as two record types`);
608
+ }
609
+ claimed.set(id, table);
610
+ }
611
+ }
612
+ prepareDocumentActions(writes) {
613
+ const actions = new Map();
614
+ for (const write of writes) {
615
+ if (write.type !== "document.create" &&
616
+ write.type !== "document.copy" &&
617
+ write.type !== "document.change" &&
618
+ write.type !== "document.retire") {
619
+ continue;
620
+ }
621
+ const id = write.type === "document.create" || write.type === "document.copy" ? write.record.id : write.id;
622
+ let action = actions.get(id);
623
+ if (action === undefined) {
624
+ action = { retire: false };
625
+ actions.set(id, action);
626
+ }
627
+ switch (write.type) {
628
+ case "document.create":
629
+ if (action.create !== undefined || action.content !== undefined || action.copy !== undefined) {
630
+ throw new Error(`Document ${id} has more than one content command`);
631
+ }
632
+ action.create = write.record;
633
+ action.content = write.content;
634
+ break;
635
+ case "document.copy":
636
+ if (action.create !== undefined || action.content !== undefined || action.copy !== undefined) {
637
+ throw new Error(`Document ${id} has more than one content command`);
638
+ }
639
+ action.create = write.record;
640
+ action.copy = write.source;
641
+ break;
642
+ case "document.change":
643
+ if (action.content !== undefined || action.copy !== undefined) {
644
+ throw new Error(`Document ${id} has more than one content command`);
645
+ }
646
+ action.content = write.content;
647
+ break;
648
+ case "document.retire":
649
+ if (action.retire)
650
+ throw new Error(`Document ${id} is retired more than once`);
651
+ action.retire = true;
652
+ break;
653
+ }
654
+ }
655
+ return actions;
656
+ }
657
+ async checkDocumentActions(transaction, actions) {
658
+ const documents = transaction.objectStore(STORES.documents);
659
+ const revisions = transaction.objectStore(STORES.documentRevisions);
660
+ const liveCounts = new Map();
661
+ for (const [id, action] of actions) {
662
+ if (action.copy !== undefined && actions.has(action.copy.id)) {
663
+ throw new Error(`Document copy ${id} source is changed in the copy batch`);
664
+ }
665
+ const existing = (await requestResult(documents.get(id)))?.record;
666
+ if (action.create === undefined && existing === undefined)
667
+ throw new Error(`Unknown document: ${id}`);
668
+ if (action.create !== undefined && existing !== undefined)
669
+ throw new Error(`Document ${id} already exists`);
670
+ if (existing?.retiredAt !== undefined)
671
+ throw new Error(`Document ${id} is retired`);
672
+ if (action.content?.kind === "delta") {
673
+ const previous = await firstCursorValue(revisions, compoundIdRange([id], 0, MAX_KEY), "prev");
674
+ if (previous === undefined)
675
+ throw new Error(`Document ${id} delta has no base`);
676
+ if (previous.version !== action.content.version) {
677
+ throw new Error(`Document ${id} version transition requires a base`);
678
+ }
679
+ }
680
+ const record = action.create ?? existing;
681
+ const key = addressKey(record);
682
+ let live = liveCounts.get(key);
683
+ if (live === undefined)
684
+ live = (await this.currentDocumentId(transaction, record)) === undefined ? 0 : 1;
685
+ if (action.retire && existing !== undefined)
686
+ live--;
687
+ if (action.create !== undefined && !action.retire)
688
+ live++;
689
+ liveCounts.set(key, live);
690
+ }
691
+ for (const live of liveCounts.values()) {
692
+ if (live > 1)
693
+ throw new Error("Document address already has a current incarnation");
694
+ }
695
+ }
696
+ async currentDocumentId(transaction, address) {
697
+ const row = await firstCursorValue(transaction.objectStore(STORES.documents).index(INDEXES.documentsByAddress), keyRanges().bound([...addressPrefix(address), 0, 0], [...addressPrefix(address), MAX_KEY, MAX_KEY]), "prev", (value) => value.retiredAt === undefined);
698
+ return row?.id;
699
+ }
700
+ async claimId(transaction, id, recordType) {
701
+ await requestResult(transaction.objectStore(STORES.recordIds).put({ id, recordType }));
702
+ }
703
+ async applyTableWrite(transaction, write, seq) {
704
+ switch (write.type) {
705
+ case "conversation":
706
+ await this.claimId(transaction, write.value.id, "conversation");
707
+ await requestResult(transaction.objectStore(STORES.conversations).add({
708
+ id: write.value.id,
709
+ ...(write.value.owner === undefined
710
+ ? {}
711
+ : {
712
+ ownerConversationId: write.value.owner.conversationId,
713
+ ownerTaskId: write.value.owner.taskId,
714
+ }),
715
+ record: write.value,
716
+ }));
717
+ break;
718
+ case "entry":
719
+ await this.claimId(transaction, write.value.id, "entry");
720
+ await requestResult(transaction.objectStore(STORES.entries).add({
721
+ id: write.value.id,
722
+ conversationId: write.value.conversationId,
723
+ ...(write.value.head === undefined ? {} : { headConversationId: write.value.conversationId }),
724
+ commitSeq: seq,
725
+ record: write.value,
726
+ }));
727
+ break;
728
+ case "task":
729
+ await this.claimId(transaction, write.value.id, "task");
730
+ await requestResult(transaction.objectStore(STORES.tasks).put({
731
+ id: write.value.id,
732
+ conversationId: write.value.conversationId,
733
+ kind: write.value.kind,
734
+ status: write.value.state.status,
735
+ abortRequested: write.value.abortRequested ? 1 : 0,
736
+ background: write.value.background ? 1 : 0,
737
+ record: write.value,
738
+ }));
739
+ break;
740
+ case "submission":
741
+ await this.claimId(transaction, write.value.id, "submission");
742
+ await requestResult(transaction.objectStore(STORES.submissions).put({
743
+ id: write.value.id,
744
+ conversationId: write.value.conversationId,
745
+ ...(write.value.requestId === undefined ? {} : { requestId: write.value.requestId }),
746
+ status: write.value.status,
747
+ record: write.value,
748
+ }));
749
+ break;
750
+ case "document.create":
751
+ case "document.copy":
752
+ case "document.change":
753
+ case "document.retire":
754
+ break;
755
+ }
756
+ }
757
+ async applyDocumentActions(transaction, actions, seq) {
758
+ const documents = transaction.objectStore(STORES.documents);
759
+ const revisions = transaction.objectStore(STORES.documentRevisions);
760
+ for (const [id, action] of actions) {
761
+ let content = action.content;
762
+ if (action.copy !== undefined) {
763
+ try {
764
+ const stored = await this.materializeDocument(transaction, action.copy.id, action.copy.at);
765
+ if (stored === undefined)
766
+ throw new Error(`Fork source document ${action.copy.id} cannot be read`);
767
+ const create = action.create;
768
+ if (stored.record.scope.kind !== "conversation" ||
769
+ create.scope.kind !== "conversation" ||
770
+ stored.record.kind !== create.kind ||
771
+ stored.record.key !== create.key ||
772
+ stored.record.history !== create.history ||
773
+ stored.record.fork !== create.fork) {
774
+ throw new Error(`Fork source document ${action.copy.id} does not match the copied record`);
775
+ }
776
+ content = { kind: "base", version: stored.version, value: stored.value };
777
+ }
778
+ catch (error) {
779
+ throw new Error(`Document copy ${id} was rejected`, { cause: error });
780
+ }
781
+ }
782
+ let record;
783
+ if (action.create !== undefined) {
784
+ record = {
785
+ ...action.create,
786
+ createdAt: seq,
787
+ ...(action.retire ? { retiredAt: seq } : {}),
788
+ };
789
+ const parts = addressParts(record);
790
+ await this.claimId(transaction, id, "document");
791
+ await requestResult(documents.add({
792
+ id,
793
+ ...parts,
794
+ createdAt: seq,
795
+ ...(action.retire ? { retiredAt: seq } : {}),
796
+ record,
797
+ }));
798
+ }
799
+ else {
800
+ record = (await requestResult(documents.get(id))).record;
801
+ }
802
+ if (content !== undefined) {
803
+ if (content.kind === "base" && isCurrentOnly(record)) {
804
+ await requestResult(revisions.delete(compoundIdRange([id], 0, MAX_KEY)));
805
+ }
806
+ await requestResult(revisions.add({
807
+ documentId: id,
808
+ seq,
809
+ kind: content.kind,
810
+ version: content.version,
811
+ content: content.kind === "base" ? content.value : content.ops,
812
+ }));
813
+ }
814
+ if (action.retire) {
815
+ if (action.create === undefined) {
816
+ record = { ...record, retiredAt: seq };
817
+ const row = (await requestResult(documents.get(id)));
818
+ await requestResult(documents.put({ ...row, retiredAt: seq, record }));
819
+ }
820
+ if (isCurrentOnly(record)) {
821
+ await requestResult(revisions.delete(compoundIdRange([id], 0, MAX_KEY)));
822
+ }
823
+ }
824
+ }
825
+ }
826
+ assertOpen() {
827
+ if (this.closed)
828
+ throw new Error("IndexedDbStorage is closed");
829
+ }
830
+ }
831
+ //# sourceMappingURL=indexeddb.js.map