@earendil-works/pi-durable 0.87.1 → 0.99.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -4
- package/dist/documents.d.ts +66 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +98 -0
- package/dist/documents.js.map +1 -0
- package/dist/entries.d.ts +13 -0
- package/dist/entries.d.ts.map +1 -0
- package/dist/entries.js +18 -0
- package/dist/entries.js.map +1 -0
- package/dist/env/index.d.ts +131 -0
- package/dist/env/index.d.ts.map +1 -0
- package/dist/env/index.js +45 -0
- package/dist/env/index.js.map +1 -0
- package/dist/env/node.d.ts +45 -0
- package/dist/env/node.d.ts.map +1 -0
- package/dist/env/node.js +908 -0
- package/dist/env/node.js.map +1 -0
- package/dist/env/utils/adaptive-publisher.d.ts +24 -0
- package/dist/env/utils/adaptive-publisher.d.ts.map +1 -0
- package/dist/env/utils/adaptive-publisher.js +79 -0
- package/dist/env/utils/adaptive-publisher.js.map +1 -0
- package/dist/env/utils/output-capture.d.ts +31 -0
- package/dist/env/utils/output-capture.d.ts.map +1 -0
- package/dist/env/utils/output-capture.js +238 -0
- package/dist/env/utils/output-capture.js.map +1 -0
- package/dist/env/utils/truncate.d.ts +71 -0
- package/dist/env/utils/truncate.d.ts.map +1 -0
- package/dist/env/utils/truncate.js +294 -0
- package/dist/env/utils/truncate.js.map +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -0
- package/dist/harness/config.d.ts +20 -0
- package/dist/harness/config.d.ts.map +1 -0
- package/dist/harness/config.js +18 -0
- package/dist/harness/config.js.map +1 -0
- package/dist/harness/context.d.ts +27 -0
- package/dist/harness/context.d.ts.map +1 -0
- package/dist/harness/context.js +109 -0
- package/dist/harness/context.js.map +1 -0
- package/dist/harness/generation.d.ts +36 -0
- package/dist/harness/generation.d.ts.map +1 -0
- package/dist/harness/generation.js +266 -0
- package/dist/harness/generation.js.map +1 -0
- package/dist/harness/harness.d.ts +10 -0
- package/dist/harness/harness.d.ts.map +1 -0
- package/dist/harness/harness.js +266 -0
- package/dist/harness/harness.js.map +1 -0
- package/dist/harness/live.d.ts +40 -0
- package/dist/harness/live.d.ts.map +1 -0
- package/dist/harness/live.js +41 -0
- package/dist/harness/live.js.map +1 -0
- package/dist/harness/prompt.d.ts +24 -0
- package/dist/harness/prompt.d.ts.map +1 -0
- package/dist/harness/prompt.js +88 -0
- package/dist/harness/prompt.js.map +1 -0
- package/dist/harness/registry.d.ts +7 -0
- package/dist/harness/registry.d.ts.map +1 -0
- package/dist/harness/registry.js +309 -0
- package/dist/harness/registry.js.map +1 -0
- package/dist/harness/scheduler.d.ts +63 -0
- package/dist/harness/scheduler.d.ts.map +1 -0
- package/dist/harness/scheduler.js +605 -0
- package/dist/harness/scheduler.js.map +1 -0
- package/dist/harness/submissions.d.ts +24 -0
- package/dist/harness/submissions.d.ts.map +1 -0
- package/dist/harness/submissions.js +133 -0
- package/dist/harness/submissions.js.map +1 -0
- package/dist/harness/types.d.ts +358 -0
- package/dist/harness/types.d.ts.map +1 -0
- package/dist/harness/types.js +2 -0
- package/dist/harness/types.js.map +1 -0
- package/dist/harness/util.d.ts +14 -0
- package/dist/harness/util.d.ts.map +1 -0
- package/dist/harness/util.js +55 -0
- package/dist/harness/util.js.map +1 -0
- package/dist/ids.d.ts +6 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +9 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/session/forks.d.ts +10 -0
- package/dist/session/forks.d.ts.map +1 -0
- package/dist/session/forks.js +42 -0
- package/dist/session/forks.js.map +1 -0
- package/dist/session/observation.d.ts +28 -0
- package/dist/session/observation.d.ts.map +1 -0
- package/dist/session/observation.js +280 -0
- package/dist/session/observation.js.map +1 -0
- package/dist/session/session.d.ts +59 -0
- package/dist/session/session.d.ts.map +1 -0
- package/dist/session/session.js +380 -0
- package/dist/session/session.js.map +1 -0
- package/dist/session/transaction.d.ts +105 -0
- package/dist/session/transaction.d.ts.map +1 -0
- package/dist/session/transaction.js +718 -0
- package/dist/session/transaction.js.map +1 -0
- package/dist/storage/jsonl/index.d.ts +2 -0
- package/dist/storage/jsonl/index.d.ts.map +1 -0
- package/dist/storage/jsonl/index.js +2 -0
- package/dist/storage/jsonl/index.js.map +1 -0
- package/dist/storage/jsonl/node.d.ts +6 -0
- package/dist/storage/jsonl/node.d.ts.map +1 -0
- package/dist/storage/jsonl/node.js +8 -0
- package/dist/storage/jsonl/node.js.map +1 -0
- package/dist/storage/jsonl/storage.d.ts +115 -0
- package/dist/storage/jsonl/storage.d.ts.map +1 -0
- package/dist/storage/jsonl/storage.js +681 -0
- package/dist/storage/jsonl/storage.js.map +1 -0
- package/dist/storage/memory.d.ts +31 -14
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +208 -61
- package/dist/storage/memory.js.map +1 -1
- package/dist/storage/sqlite/database.d.ts +5 -1
- package/dist/storage/sqlite/database.d.ts.map +1 -1
- package/dist/storage/sqlite/database.js.map +1 -1
- package/dist/storage/sqlite/index.d.ts.map +1 -1
- package/dist/storage/sqlite/migrations.d.ts +1 -1
- package/dist/storage/sqlite/migrations.d.ts.map +1 -1
- package/dist/storage/sqlite/migrations.js +8 -1
- package/dist/storage/sqlite/migrations.js.map +1 -1
- package/dist/storage/sqlite/node.d.ts.map +1 -1
- package/dist/storage/sqlite/node.js +2 -2
- package/dist/storage/sqlite/node.js.map +1 -1
- package/dist/storage/sqlite/storage.d.ts +20 -14
- package/dist/storage/sqlite/storage.d.ts.map +1 -1
- package/dist/storage/sqlite/storage.js +159 -55
- package/dist/storage/sqlite/storage.js.map +1 -1
- package/dist/tasks.d.ts +6 -0
- package/dist/tasks.d.ts.map +1 -0
- package/dist/tasks.js +5 -0
- package/dist/tasks.js.map +1 -0
- package/dist/testing/assertions.d.ts +5 -0
- package/dist/testing/assertions.d.ts.map +1 -0
- package/dist/testing/assertions.js +25 -0
- package/dist/testing/assertions.js.map +1 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/runner.d.ts +10 -0
- package/dist/testing/runner.d.ts.map +1 -0
- package/dist/testing/runner.js +14 -0
- package/dist/testing/runner.js.map +1 -0
- package/dist/testing/storage-benchmark.d.ts +41 -0
- package/dist/testing/storage-benchmark.d.ts.map +1 -0
- package/dist/testing/storage-benchmark.js +351 -0
- package/dist/testing/storage-benchmark.js.map +1 -0
- package/dist/testing/storage-conformance.d.ts +4 -0
- package/dist/testing/storage-conformance.d.ts.map +1 -0
- package/dist/testing/storage-conformance.js +1122 -0
- package/dist/testing/storage-conformance.js.map +1 -0
- package/dist/testing/types.d.ts +19 -0
- package/dist/testing/types.d.ts.map +1 -0
- package/dist/testing/types.js +2 -0
- package/dist/testing/types.js.map +1 -0
- package/dist/types.d.ts +422 -54
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/chord-delta-findings.md +1 -1
- package/docs/pico-v5-chord-usage.md +47 -60
- package/docs/pico-v5-handoff.md +469 -264
- package/docs/pico-v5.md +1523 -639
- package/package.json +30 -5
package/dist/storage/memory.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
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(
|
|
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(
|
|
224
|
-
const values =
|
|
225
|
-
|
|
226
|
-
|
|
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(
|
|
348
|
+
async entry(idOrConversationId, idOrContext, context) {
|
|
230
349
|
this.assertOpen();
|
|
231
|
-
|
|
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,
|
|
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,
|
|
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.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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,
|
|
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
|
|
393
|
-
const
|
|
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);
|