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