@earendil-works/pi-durable 0.87.1 → 0.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +46 -4
  2. package/dist/documents.d.ts +66 -0
  3. package/dist/documents.d.ts.map +1 -0
  4. package/dist/documents.js +98 -0
  5. package/dist/documents.js.map +1 -0
  6. package/dist/entries.d.ts +13 -0
  7. package/dist/entries.d.ts.map +1 -0
  8. package/dist/entries.js +18 -0
  9. package/dist/entries.js.map +1 -0
  10. package/dist/env/index.d.ts +131 -0
  11. package/dist/env/index.d.ts.map +1 -0
  12. package/dist/env/index.js +45 -0
  13. package/dist/env/index.js.map +1 -0
  14. package/dist/env/node.d.ts +45 -0
  15. package/dist/env/node.d.ts.map +1 -0
  16. package/dist/env/node.js +908 -0
  17. package/dist/env/node.js.map +1 -0
  18. package/dist/env/utils/adaptive-publisher.d.ts +24 -0
  19. package/dist/env/utils/adaptive-publisher.d.ts.map +1 -0
  20. package/dist/env/utils/adaptive-publisher.js +79 -0
  21. package/dist/env/utils/adaptive-publisher.js.map +1 -0
  22. package/dist/env/utils/output-capture.d.ts +31 -0
  23. package/dist/env/utils/output-capture.d.ts.map +1 -0
  24. package/dist/env/utils/output-capture.js +238 -0
  25. package/dist/env/utils/output-capture.js.map +1 -0
  26. package/dist/env/utils/truncate.d.ts +71 -0
  27. package/dist/env/utils/truncate.d.ts.map +1 -0
  28. package/dist/env/utils/truncate.js +294 -0
  29. package/dist/env/utils/truncate.js.map +1 -0
  30. package/dist/errors.d.ts +15 -0
  31. package/dist/errors.d.ts.map +1 -0
  32. package/dist/errors.js +24 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/harness/config.d.ts +20 -0
  35. package/dist/harness/config.d.ts.map +1 -0
  36. package/dist/harness/config.js +18 -0
  37. package/dist/harness/config.js.map +1 -0
  38. package/dist/harness/context.d.ts +27 -0
  39. package/dist/harness/context.d.ts.map +1 -0
  40. package/dist/harness/context.js +109 -0
  41. package/dist/harness/context.js.map +1 -0
  42. package/dist/harness/generation.d.ts +36 -0
  43. package/dist/harness/generation.d.ts.map +1 -0
  44. package/dist/harness/generation.js +266 -0
  45. package/dist/harness/generation.js.map +1 -0
  46. package/dist/harness/harness.d.ts +10 -0
  47. package/dist/harness/harness.d.ts.map +1 -0
  48. package/dist/harness/harness.js +266 -0
  49. package/dist/harness/harness.js.map +1 -0
  50. package/dist/harness/live.d.ts +40 -0
  51. package/dist/harness/live.d.ts.map +1 -0
  52. package/dist/harness/live.js +41 -0
  53. package/dist/harness/live.js.map +1 -0
  54. package/dist/harness/prompt.d.ts +24 -0
  55. package/dist/harness/prompt.d.ts.map +1 -0
  56. package/dist/harness/prompt.js +88 -0
  57. package/dist/harness/prompt.js.map +1 -0
  58. package/dist/harness/registry.d.ts +7 -0
  59. package/dist/harness/registry.d.ts.map +1 -0
  60. package/dist/harness/registry.js +309 -0
  61. package/dist/harness/registry.js.map +1 -0
  62. package/dist/harness/scheduler.d.ts +63 -0
  63. package/dist/harness/scheduler.d.ts.map +1 -0
  64. package/dist/harness/scheduler.js +605 -0
  65. package/dist/harness/scheduler.js.map +1 -0
  66. package/dist/harness/submissions.d.ts +24 -0
  67. package/dist/harness/submissions.d.ts.map +1 -0
  68. package/dist/harness/submissions.js +133 -0
  69. package/dist/harness/submissions.js.map +1 -0
  70. package/dist/harness/types.d.ts +358 -0
  71. package/dist/harness/types.d.ts.map +1 -0
  72. package/dist/harness/types.js +2 -0
  73. package/dist/harness/types.js.map +1 -0
  74. package/dist/harness/util.d.ts +14 -0
  75. package/dist/harness/util.d.ts.map +1 -0
  76. package/dist/harness/util.js +55 -0
  77. package/dist/harness/util.js.map +1 -0
  78. package/dist/ids.d.ts +6 -0
  79. package/dist/ids.d.ts.map +1 -0
  80. package/dist/ids.js +9 -0
  81. package/dist/ids.js.map +1 -0
  82. package/dist/index.d.ts +12 -1
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +10 -0
  85. package/dist/index.js.map +1 -1
  86. package/dist/session/forks.d.ts +10 -0
  87. package/dist/session/forks.d.ts.map +1 -0
  88. package/dist/session/forks.js +42 -0
  89. package/dist/session/forks.js.map +1 -0
  90. package/dist/session/observation.d.ts +28 -0
  91. package/dist/session/observation.d.ts.map +1 -0
  92. package/dist/session/observation.js +280 -0
  93. package/dist/session/observation.js.map +1 -0
  94. package/dist/session/session.d.ts +59 -0
  95. package/dist/session/session.d.ts.map +1 -0
  96. package/dist/session/session.js +380 -0
  97. package/dist/session/session.js.map +1 -0
  98. package/dist/session/transaction.d.ts +105 -0
  99. package/dist/session/transaction.d.ts.map +1 -0
  100. package/dist/session/transaction.js +718 -0
  101. package/dist/session/transaction.js.map +1 -0
  102. package/dist/storage/jsonl/index.d.ts +2 -0
  103. package/dist/storage/jsonl/index.d.ts.map +1 -0
  104. package/dist/storage/jsonl/index.js +2 -0
  105. package/dist/storage/jsonl/index.js.map +1 -0
  106. package/dist/storage/jsonl/node.d.ts +6 -0
  107. package/dist/storage/jsonl/node.d.ts.map +1 -0
  108. package/dist/storage/jsonl/node.js +8 -0
  109. package/dist/storage/jsonl/node.js.map +1 -0
  110. package/dist/storage/jsonl/storage.d.ts +115 -0
  111. package/dist/storage/jsonl/storage.d.ts.map +1 -0
  112. package/dist/storage/jsonl/storage.js +681 -0
  113. package/dist/storage/jsonl/storage.js.map +1 -0
  114. package/dist/storage/memory.d.ts +31 -14
  115. package/dist/storage/memory.d.ts.map +1 -1
  116. package/dist/storage/memory.js +208 -61
  117. package/dist/storage/memory.js.map +1 -1
  118. package/dist/storage/sqlite/database.d.ts +5 -1
  119. package/dist/storage/sqlite/database.d.ts.map +1 -1
  120. package/dist/storage/sqlite/database.js.map +1 -1
  121. package/dist/storage/sqlite/index.d.ts.map +1 -1
  122. package/dist/storage/sqlite/migrations.d.ts +1 -1
  123. package/dist/storage/sqlite/migrations.d.ts.map +1 -1
  124. package/dist/storage/sqlite/migrations.js +8 -1
  125. package/dist/storage/sqlite/migrations.js.map +1 -1
  126. package/dist/storage/sqlite/node.d.ts.map +1 -1
  127. package/dist/storage/sqlite/node.js +2 -2
  128. package/dist/storage/sqlite/node.js.map +1 -1
  129. package/dist/storage/sqlite/storage.d.ts +20 -14
  130. package/dist/storage/sqlite/storage.d.ts.map +1 -1
  131. package/dist/storage/sqlite/storage.js +159 -55
  132. package/dist/storage/sqlite/storage.js.map +1 -1
  133. package/dist/tasks.d.ts +6 -0
  134. package/dist/tasks.d.ts.map +1 -0
  135. package/dist/tasks.js +5 -0
  136. package/dist/tasks.js.map +1 -0
  137. package/dist/testing/assertions.d.ts +5 -0
  138. package/dist/testing/assertions.d.ts.map +1 -0
  139. package/dist/testing/assertions.js +25 -0
  140. package/dist/testing/assertions.js.map +1 -0
  141. package/dist/testing/index.d.ts +6 -0
  142. package/dist/testing/index.d.ts.map +1 -0
  143. package/dist/testing/index.js +5 -0
  144. package/dist/testing/index.js.map +1 -0
  145. package/dist/testing/runner.d.ts +10 -0
  146. package/dist/testing/runner.d.ts.map +1 -0
  147. package/dist/testing/runner.js +14 -0
  148. package/dist/testing/runner.js.map +1 -0
  149. package/dist/testing/storage-benchmark.d.ts +41 -0
  150. package/dist/testing/storage-benchmark.d.ts.map +1 -0
  151. package/dist/testing/storage-benchmark.js +351 -0
  152. package/dist/testing/storage-benchmark.js.map +1 -0
  153. package/dist/testing/storage-conformance.d.ts +4 -0
  154. package/dist/testing/storage-conformance.d.ts.map +1 -0
  155. package/dist/testing/storage-conformance.js +1122 -0
  156. package/dist/testing/storage-conformance.js.map +1 -0
  157. package/dist/testing/types.d.ts +19 -0
  158. package/dist/testing/types.d.ts.map +1 -0
  159. package/dist/testing/types.js +2 -0
  160. package/dist/testing/types.js.map +1 -0
  161. package/dist/types.d.ts +422 -54
  162. package/dist/types.d.ts.map +1 -1
  163. package/dist/types.js.map +1 -1
  164. package/docs/chord-delta-findings.md +1 -1
  165. package/docs/pico-v5-chord-usage.md +47 -60
  166. package/docs/pico-v5-handoff.md +469 -264
  167. package/docs/pico-v5.md +1523 -639
  168. package/package.json +30 -5
@@ -1,4 +1,15 @@
1
- import { applyImmutable } from "@earendil-works/chord/delta";
1
+ import { applyImmutableBatches } from "@earendil-works/chord/delta";
2
+ import { StorageRejected } from "../errors.js";
3
+ import { idFromNumber, seqFromNumber } from "../ids.js";
4
+ function* documentDeltaBatches(id, version, revisions, start) {
5
+ for (let index = start; index < revisions.length; index++) {
6
+ const revision = revisions[index];
7
+ if (revision.kind !== "delta" || revision.version !== version) {
8
+ throw new Error(`Document ${id} crosses a stored version boundary without a base`);
9
+ }
10
+ yield revision.ops;
11
+ }
12
+ }
2
13
  const clone = (value) => {
3
14
  if (value === null || typeof value !== "object")
4
15
  return value;
@@ -23,7 +34,21 @@ const clone = (value) => {
23
34
  }
24
35
  return result;
25
36
  };
26
- const cursorId = (cursor) => cursor?.after;
37
+ const freeze = (value) => {
38
+ if (value === null || typeof value !== "object" || Object.isFrozen(value))
39
+ return value;
40
+ for (const child of Object.values(value))
41
+ freeze(child);
42
+ return Object.freeze(value);
43
+ };
44
+ const cursorId = (cursor) => {
45
+ const after = cursor?.after;
46
+ if (after === undefined)
47
+ return undefined;
48
+ if (typeof after !== "number" || !Number.isSafeInteger(after))
49
+ throw new TypeError("Invalid storage cursor");
50
+ return idFromNumber(after);
51
+ };
27
52
  const lowerBound = (ids, target) => {
28
53
  let low = 0;
29
54
  let high = ids.length;
@@ -59,6 +84,14 @@ const removeSorted = (ids, id) => {
59
84
  if (ids[index] === id)
60
85
  ids.splice(index, 1);
61
86
  };
87
+ const insertMapId = (index, key, id) => {
88
+ let ids = index.get(key);
89
+ if (ids === undefined) {
90
+ ids = [];
91
+ index.set(key, ids);
92
+ }
93
+ insertSorted(ids, id);
94
+ };
62
95
  const scopeKey = (scope) => {
63
96
  switch (scope.kind) {
64
97
  case "session":
@@ -81,19 +114,7 @@ const isAliveAt = (record, at) => {
81
114
  return record.createdAt <= at && (record.retiredAt === undefined || at < record.retiredAt);
82
115
  };
83
116
  const isCurrentOnly = (record) => record.scope.kind !== "conversation" || record.history === "latest";
84
- const tableContaining = (state, id) => {
85
- if (state.conversations.has(id))
86
- return "conversation";
87
- if (state.entries.has(id))
88
- return "entry";
89
- if (state.tasks.has(id))
90
- return "task";
91
- if (state.submissions.has(id))
92
- return "submission";
93
- if (state.documents.has(id))
94
- return "document";
95
- return undefined;
96
- };
117
+ const tableContaining = (state, id) => state.recordTypes.get(id);
97
118
  const page = (values, limit) => {
98
119
  const items = values.slice(0, limit);
99
120
  if (values.length <= limit)
@@ -108,8 +129,11 @@ const page = (values, limit) => {
108
129
  */
109
130
  export class MemoryStorage {
110
131
  state = {
132
+ recordTypes: new Map(),
111
133
  conversations: new Map(),
112
134
  conversationIds: [],
135
+ conversationIdsByOwnerConversation: new Map(),
136
+ conversationIdsByOwnerTask: new Map(),
113
137
  entries: new Map(),
114
138
  entryIds: new Map(),
115
139
  headEntryIds: new Map(),
@@ -118,6 +142,8 @@ export class MemoryStorage {
118
142
  taskIds: [],
119
143
  taskIdsByStatus: { pending: [], running: [], terminal: [] },
120
144
  submissions: new Map(),
145
+ submissionIds: [],
146
+ submissionIdsByStatus: { queued: [], placed: [], done: [], unanswered: [] },
121
147
  submissionIdsByRequest: new Map(),
122
148
  documents: new Map(),
123
149
  documentAddresses: new Map(),
@@ -127,20 +153,91 @@ export class MemoryStorage {
127
153
  nextSeq = 1;
128
154
  closed = false;
129
155
  async commit(writes, _context) {
156
+ return this.prepareCommit(writes).apply();
157
+ }
158
+ /** Validate and detach one commit without changing observable state. */
159
+ prepareCommit(writes, seq = seqFromNumber(this.nextSeq)) {
130
160
  this.assertOpen();
131
- const prepared = writes.map((write) => clone(write));
132
- const seq = this.nextSeq;
133
- this.checkGlobalIds(prepared);
134
- const documentActions = this.prepareDocumentActions(prepared);
161
+ if (!Number.isSafeInteger(seq) || seq < this.nextSeq) {
162
+ throw new Error(`Commit sequence ${seq} does not strictly increase`);
163
+ }
164
+ const detachedWrites = freeze(this.resolveDocumentCopies(writes.map((write) => clone(write))));
165
+ this.checkGlobalIds(detachedWrites);
166
+ const documentActions = this.prepareDocumentActions(detachedWrites);
135
167
  this.checkDocumentActions(documentActions);
168
+ let applied = false;
169
+ return {
170
+ seq,
171
+ writes: detachedWrites,
172
+ apply: () => {
173
+ if (!applied) {
174
+ applied = true;
175
+ this.applyPreparedCommit(detachedWrites, documentActions, seq);
176
+ }
177
+ return seq;
178
+ },
179
+ };
180
+ }
181
+ resolveDocumentCopies(writes) {
182
+ if (!writes.some((write) => write.type === "document.copy"))
183
+ return writes;
184
+ const changedDocumentIds = new Set();
185
+ for (const write of writes) {
186
+ if (write.type === "document.create" || write.type === "document.copy") {
187
+ changedDocumentIds.add(write.record.id);
188
+ }
189
+ else if (write.type === "document.change" || write.type === "document.retire") {
190
+ changedDocumentIds.add(write.id);
191
+ }
192
+ }
193
+ return writes.map((write) => {
194
+ if (write.type !== "document.copy")
195
+ return write;
196
+ try {
197
+ if (changedDocumentIds.has(write.source.id)) {
198
+ throw new Error(`Fork source document ${write.source.id} is changed in the copy batch`);
199
+ }
200
+ const stored = this.materializeDocument(write.source.id, write.source.at);
201
+ if (stored === undefined)
202
+ throw new Error(`Fork source document ${write.source.id} cannot be read`);
203
+ if (stored.record.scope.kind !== "conversation" ||
204
+ write.record.scope.kind !== "conversation" ||
205
+ stored.record.kind !== write.record.kind ||
206
+ stored.record.key !== write.record.key ||
207
+ stored.record.history !== write.record.history ||
208
+ stored.record.fork !== write.record.fork) {
209
+ throw new Error(`Fork source document ${write.source.id} does not match the copied record`);
210
+ }
211
+ return {
212
+ type: "document.create",
213
+ record: write.record,
214
+ content: { kind: "base", version: stored.version, value: stored.value },
215
+ };
216
+ }
217
+ catch (error) {
218
+ if (error instanceof StorageRejected)
219
+ throw error;
220
+ throw new StorageRejected(`Document copy ${write.record.id} was rejected`, { cause: error });
221
+ }
222
+ });
223
+ }
224
+ applyPreparedCommit(prepared, documentActions, seq) {
136
225
  for (const write of prepared) {
137
226
  switch (write.type) {
138
- case "conversation":
227
+ case "conversation": {
228
+ this.state.recordTypes.set(write.value.id, "conversation");
139
229
  this.state.conversations.set(write.value.id, write.value);
140
230
  insertSorted(this.state.conversationIds, write.value.id);
231
+ const owner = write.value.owner;
232
+ if (owner !== undefined) {
233
+ insertMapId(this.state.conversationIdsByOwnerConversation, owner.conversationId, write.value.id);
234
+ insertMapId(this.state.conversationIdsByOwnerTask, owner.taskId, write.value.id);
235
+ }
141
236
  this.nextId = Math.max(this.nextId, write.value.id + 1);
142
237
  break;
238
+ }
143
239
  case "entry": {
240
+ this.state.recordTypes.set(write.value.id, "entry");
144
241
  this.state.entries.set(write.value.id, write.value);
145
242
  this.state.entryCommitSeqs.set(write.value.id, seq);
146
243
  let ids = this.state.entryIds.get(write.value.conversationId);
@@ -161,6 +258,7 @@ export class MemoryStorage {
161
258
  break;
162
259
  }
163
260
  case "task": {
261
+ this.state.recordTypes.set(write.value.id, "task");
164
262
  const previous = this.state.tasks.get(write.value.id);
165
263
  if (previous === undefined) {
166
264
  insertSorted(this.state.taskIds, write.value.id);
@@ -175,7 +273,16 @@ export class MemoryStorage {
175
273
  break;
176
274
  }
177
275
  case "submission": {
276
+ this.state.recordTypes.set(write.value.id, "submission");
178
277
  const previous = this.state.submissions.get(write.value.id);
278
+ if (previous === undefined) {
279
+ insertSorted(this.state.submissionIds, write.value.id);
280
+ insertSorted(this.state.submissionIdsByStatus[write.value.status], write.value.id);
281
+ }
282
+ else if (previous.status !== write.value.status) {
283
+ removeSorted(this.state.submissionIdsByStatus[previous.status], write.value.id);
284
+ insertSorted(this.state.submissionIdsByStatus[write.value.status], write.value.id);
285
+ }
179
286
  if (previous?.requestId !== undefined) {
180
287
  const previousRequests = this.state.submissionIdsByRequest.get(previous.conversationId);
181
288
  if (previousRequests?.get(previous.requestId) === write.value.id) {
@@ -196,6 +303,8 @@ export class MemoryStorage {
196
303
  this.nextId = Math.max(this.nextId, write.value.id + 1);
197
304
  break;
198
305
  }
306
+ case "document.copy":
307
+ throw new Error("Prepared document copy was not resolved");
199
308
  case "document.create":
200
309
  case "document.change":
201
310
  case "document.retire":
@@ -203,32 +312,53 @@ export class MemoryStorage {
203
312
  }
204
313
  }
205
314
  this.applyDocumentActions(documentActions, seq);
206
- this.nextSeq++;
315
+ this.nextSeq = seq + 1;
207
316
  return seq;
208
317
  }
209
318
  async mintId() {
210
319
  this.assertOpen();
211
320
  if (!Number.isSafeInteger(this.nextId))
212
321
  throw new Error("ID space is exhausted");
213
- return this.nextId++;
322
+ return idFromNumber(this.nextId++);
214
323
  }
215
324
  async conversation(id, _context) {
216
325
  this.assertOpen();
217
326
  const value = this.state.conversations.get(id);
218
327
  return value === undefined ? undefined : clone(value);
219
328
  }
220
- async scanConversations(cursor, limit, _context) {
329
+ async scanConversations(query, limit, cursor, _context) {
221
330
  this.assertOpen();
331
+ const ids = query.ownerTaskId !== undefined
332
+ ? (this.state.conversationIdsByOwnerTask.get(query.ownerTaskId) ?? [])
333
+ : query.ownerConversationId !== undefined
334
+ ? (this.state.conversationIdsByOwnerConversation.get(query.ownerConversationId) ?? [])
335
+ : this.state.conversationIds;
222
336
  const after = cursorId(cursor);
223
- const start = after === undefined ? 0 : upperBound(this.state.conversationIds, after);
224
- const values = this.state.conversationIds
225
- .slice(start, start + limit + 1)
226
- .map((id) => this.state.conversations.get(id));
337
+ const start = after === undefined ? 0 : upperBound(ids, after);
338
+ const values = [];
339
+ for (let index = start; index < ids.length && values.length <= limit; index++) {
340
+ const value = this.state.conversations.get(ids[index]);
341
+ if (query.ownerConversationId !== undefined && value.owner?.conversationId !== query.ownerConversationId) {
342
+ continue;
343
+ }
344
+ values.push(value);
345
+ }
227
346
  return page(values, limit);
228
347
  }
229
- async entry(id, _context) {
348
+ async entry(idOrConversationId, idOrContext, context) {
230
349
  this.assertOpen();
231
- const entry = this.state.entries.get(id);
350
+ if (context === undefined) {
351
+ const id = idFromNumber(idOrConversationId);
352
+ const entry = this.state.entries.get(id);
353
+ if (entry === undefined)
354
+ return undefined;
355
+ return { entry: clone(entry), commitSeq: this.state.entryCommitSeqs.get(id) };
356
+ }
357
+ if (typeof idOrContext !== "number")
358
+ throw new TypeError("Storage.entry() requires an entry ID");
359
+ const conversationId = idFromNumber(idOrConversationId);
360
+ const id = idFromNumber(idOrContext);
361
+ const entry = this.visibleEntries(conversationId, id, id).next().value;
232
362
  if (entry === undefined)
233
363
  return undefined;
234
364
  return { entry: clone(entry), commitSeq: this.state.entryCommitSeqs.get(id) };
@@ -254,7 +384,7 @@ export class MemoryStorage {
254
384
  currentId = conversation.parent.conversationId;
255
385
  }
256
386
  }
257
- async scanEntries(query, cursor, limit, _context) {
387
+ async scanEntries(query, limit, cursor, _context) {
258
388
  this.assertOpen();
259
389
  const after = cursorId(cursor);
260
390
  const maxEntryId = after === undefined ? query.maxEntryId : Math.min(query.maxEntryId ?? Number.POSITIVE_INFINITY, after - 1);
@@ -271,7 +401,7 @@ export class MemoryStorage {
271
401
  const value = this.state.tasks.get(id);
272
402
  return value === undefined ? undefined : clone(value);
273
403
  }
274
- async scanTasks(query, cursor, limit, _context) {
404
+ async scanTasks(query, limit, cursor, _context) {
275
405
  this.assertOpen();
276
406
  const after = cursorId(cursor);
277
407
  const ids = query.status === undefined ? this.state.taskIds : this.state.taskIdsByStatus[query.status];
@@ -296,6 +426,20 @@ export class MemoryStorage {
296
426
  const value = this.state.submissions.get(id);
297
427
  return value === undefined ? undefined : clone(value);
298
428
  }
429
+ async scanSubmissions(query, limit, cursor, _context) {
430
+ this.assertOpen();
431
+ const after = cursorId(cursor);
432
+ const ids = query.status === undefined ? this.state.submissionIds : this.state.submissionIdsByStatus[query.status];
433
+ const start = after === undefined ? 0 : upperBound(ids, after);
434
+ const values = [];
435
+ for (let index = start; index < ids.length && values.length <= limit; index++) {
436
+ const value = this.state.submissions.get(ids[index]);
437
+ if (query.conversationId !== undefined && value.conversationId !== query.conversationId)
438
+ continue;
439
+ values.push(value);
440
+ }
441
+ return page(values, limit);
442
+ }
299
443
  async submissionByRequest(conversationId, requestId, _context) {
300
444
  this.assertOpen();
301
445
  const id = this.state.submissionIdsByRequest.get(conversationId)?.get(requestId);
@@ -318,32 +462,17 @@ export class MemoryStorage {
318
462
  }
319
463
  async document(id, at, _context) {
320
464
  this.assertOpen();
321
- const stored = this.state.documents.get(id);
322
- if (stored === undefined)
323
- return undefined;
324
- if (at !== "current" && isCurrentOnly(stored.record)) {
325
- throw new Error(`Document ${id} does not retain historical content`);
326
- }
327
- if (!isAliveAt(stored.record, at))
328
- return undefined;
329
- const revisions = at === "current" ? stored.revisions : stored.revisions.filter((revision) => revision.seq <= at);
330
- let baseIndex = revisions.length - 1;
331
- while (baseIndex >= 0 && revisions[baseIndex].kind !== "base")
332
- baseIndex--;
333
- const base = revisions[baseIndex];
334
- if (base?.kind !== "base")
335
- throw new Error(`Document ${id} is missing a required base`);
336
- let value = base.value;
337
- for (let index = baseIndex + 1; index < revisions.length; index++) {
338
- const revision = revisions[index];
339
- if (revision.kind !== "delta" || revision.version !== base.version) {
340
- throw new Error(`Document ${id} crosses a stored version boundary without a base`);
341
- }
342
- value = applyImmutable(value, revision.ops);
343
- }
344
- return { record: clone(stored.record), version: base.version, value: clone(value) };
465
+ const stored = this.materializeDocument(id, at);
466
+ return stored === undefined
467
+ ? undefined
468
+ : {
469
+ record: clone(stored.record),
470
+ version: stored.version,
471
+ value: clone(stored.value),
472
+ deltasSinceBase: stored.deltasSinceBase,
473
+ };
345
474
  }
346
- async scanDocuments(query, cursor, limit, _context) {
475
+ async scanDocuments(query, limit, cursor, _context) {
347
476
  this.assertOpen();
348
477
  const ids = this.state.documentIdsByScope.get(scopeKey(query.scope)) ?? [];
349
478
  const after = cursorId(cursor);
@@ -361,6 +490,25 @@ export class MemoryStorage {
361
490
  async close(_context) {
362
491
  this.closed = true;
363
492
  }
493
+ materializeDocument(id, at) {
494
+ const stored = this.state.documents.get(id);
495
+ if (stored === undefined)
496
+ return undefined;
497
+ if (at !== "current" && isCurrentOnly(stored.record)) {
498
+ throw new Error(`Document ${id} does not retain historical content`);
499
+ }
500
+ if (!isAliveAt(stored.record, at))
501
+ return undefined;
502
+ const revisions = at === "current" ? stored.revisions : stored.revisions.filter((revision) => revision.seq <= at);
503
+ let baseIndex = revisions.length - 1;
504
+ while (baseIndex >= 0 && revisions[baseIndex].kind !== "base")
505
+ baseIndex--;
506
+ const base = revisions[baseIndex];
507
+ if (base?.kind !== "base")
508
+ throw new Error(`Document ${id} is missing a required base`);
509
+ const value = applyImmutableBatches(base.value, documentDeltaBatches(id, base.version, revisions, baseIndex + 1));
510
+ return { record: stored.record, version: base.version, value, deltasSinceBase: revisions.length - baseIndex - 1 };
511
+ }
364
512
  *visibleEntries(conversationId, minEntryId = Number.NEGATIVE_INFINITY, maxEntryId = Number.POSITIVE_INFINITY) {
365
513
  if (!this.state.conversations.has(conversationId)) {
366
514
  throw new Error(`Unknown conversation: ${conversationId}`);
@@ -389,8 +537,9 @@ export class MemoryStorage {
389
537
  for (const write of writes) {
390
538
  if (write.type === "document.change" || write.type === "document.retire")
391
539
  continue;
392
- const table = write.type === "document.create" ? "document" : write.type;
393
- const id = write.type === "document.create" ? write.record.id : write.value.id;
540
+ const document = write.type === "document.create" || write.type === "document.copy";
541
+ const table = document ? "document" : write.type;
542
+ const id = document ? write.record.id : write.value.id;
394
543
  const existing = tableContaining(this.state, id);
395
544
  const earlier = claimed.get(id);
396
545
  if (table === "conversation" || table === "entry" || table === "document") {
@@ -453,9 +602,6 @@ export class MemoryStorage {
453
602
  throw new Error(`Document ${id} already exists`);
454
603
  if (existing?.record.retiredAt !== undefined)
455
604
  throw new Error(`Document ${id} is retired`);
456
- if (action.create !== undefined && action.content?.kind !== "base") {
457
- throw new Error(`Document ${id} creation requires a base`);
458
- }
459
605
  const previous = existing?.revisions.at(-1);
460
606
  if (action.content?.kind === "delta") {
461
607
  if (previous === undefined)
@@ -490,6 +636,7 @@ export class MemoryStorage {
490
636
  ...(action.retire ? { retiredAt: seq } : {}),
491
637
  };
492
638
  stored = { record, revisions: [{ ...action.content, seq }] };
639
+ this.state.recordTypes.set(id, "document");
493
640
  this.state.documents.set(id, stored);
494
641
  const key = recordAddressKey(record);
495
642
  let address = this.state.documentAddresses.get(key);