@earendil-works/pi-durable 1.4.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.
- package/CHANGELOG.md +186 -0
- package/README.md +181 -84
- package/dist/codemode/codemode.d.ts +4 -4
- package/dist/codemode/codemode.d.ts.map +1 -1
- package/dist/codemode/codemode.js +9 -12
- package/dist/codemode/codemode.js.map +1 -1
- package/dist/codemode/models.js +4 -4
- package/dist/codemode/models.js.map +1 -1
- package/dist/env/index.d.ts +12 -10
- package/dist/env/index.d.ts.map +1 -1
- package/dist/env/index.js.map +1 -1
- package/dist/env/node-watch.d.ts +3 -3
- package/dist/env/node-watch.d.ts.map +1 -1
- package/dist/env/node-watch.js +18 -16
- package/dist/env/node-watch.js.map +1 -1
- package/dist/env/node.d.ts +2 -2
- package/dist/env/node.d.ts.map +1 -1
- package/dist/env/node.js +1 -1
- package/dist/env/node.js.map +1 -1
- package/dist/errors.d.ts +18 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +26 -3
- package/dist/errors.js.map +1 -1
- package/dist/harness/agent.d.ts.map +1 -1
- package/dist/harness/agent.js +10 -2
- package/dist/harness/agent.js.map +1 -1
- package/dist/harness/compaction.d.ts.map +1 -1
- package/dist/harness/compaction.js +19 -2
- package/dist/harness/compaction.js.map +1 -1
- package/dist/harness/context.d.ts +23 -7
- package/dist/harness/context.d.ts.map +1 -1
- package/dist/harness/context.js +71 -25
- package/dist/harness/context.js.map +1 -1
- package/dist/harness/define.d.ts +7 -3
- package/dist/harness/define.d.ts.map +1 -1
- package/dist/harness/define.js +4 -0
- package/dist/harness/define.js.map +1 -1
- package/dist/harness/generation.d.ts +38 -5
- package/dist/harness/generation.d.ts.map +1 -1
- package/dist/harness/generation.js +445 -96
- package/dist/harness/generation.js.map +1 -1
- package/dist/harness/harness.d.ts +2 -0
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +87 -62
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/inbox.d.ts +5 -1
- package/dist/harness/inbox.d.ts.map +1 -1
- package/dist/harness/inbox.js +4 -1
- package/dist/harness/inbox.js.map +1 -1
- package/dist/harness/json.d.ts +20 -4
- package/dist/harness/json.d.ts.map +1 -1
- package/dist/harness/json.js +23 -5
- package/dist/harness/json.js.map +1 -1
- package/dist/harness/live.d.ts +122 -16
- package/dist/harness/live.d.ts.map +1 -1
- package/dist/harness/live.js +93 -12
- package/dist/harness/live.js.map +1 -1
- package/dist/harness/partial-changes.d.ts +54 -0
- package/dist/harness/partial-changes.d.ts.map +1 -0
- package/dist/harness/partial-changes.js +162 -0
- package/dist/harness/partial-changes.js.map +1 -0
- package/dist/harness/registry-reader.d.ts +11 -0
- package/dist/harness/registry-reader.d.ts.map +1 -0
- package/dist/harness/registry-reader.js +25 -0
- package/dist/harness/registry-reader.js.map +1 -0
- package/dist/harness/scheduler.d.ts +46 -19
- package/dist/harness/scheduler.d.ts.map +1 -1
- package/dist/harness/scheduler.js +973 -376
- package/dist/harness/scheduler.js.map +1 -1
- package/dist/harness/submissions.d.ts +17 -10
- package/dist/harness/submissions.d.ts.map +1 -1
- package/dist/harness/submissions.js +62 -25
- package/dist/harness/submissions.js.map +1 -1
- package/dist/harness/task-graph.d.ts +12 -9
- package/dist/harness/task-graph.d.ts.map +1 -1
- package/dist/harness/task-graph.js +38 -28
- package/dist/harness/task-graph.js.map +1 -1
- package/dist/harness/telemetry.d.ts +11 -11
- package/dist/harness/telemetry.d.ts.map +1 -1
- package/dist/harness/telemetry.js +7 -7
- package/dist/harness/telemetry.js.map +1 -1
- package/dist/harness/tool.d.ts +2 -0
- package/dist/harness/tool.d.ts.map +1 -1
- package/dist/harness/tool.js +171 -55
- package/dist/harness/tool.js.map +1 -1
- package/dist/harness/transcript.d.ts +72 -0
- package/dist/harness/transcript.d.ts.map +1 -0
- package/dist/harness/transcript.js +264 -0
- package/dist/harness/transcript.js.map +1 -0
- package/dist/harness/types.d.ts +218 -59
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/harness/util.d.ts +3 -1
- package/dist/harness/util.d.ts.map +1 -1
- package/dist/harness/util.js +9 -0
- package/dist/harness/util.js.map +1 -1
- package/dist/harness/view.d.ts +43 -22
- package/dist/harness/view.d.ts.map +1 -1
- package/dist/harness/view.js +130 -71
- package/dist/harness/view.js.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/search.d.ts +19 -3
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +107 -44
- package/dist/search.js.map +1 -1
- package/dist/session/cached-storage.d.ts +71 -0
- package/dist/session/cached-storage.d.ts.map +1 -0
- package/dist/session/cached-storage.js +269 -0
- package/dist/session/cached-storage.js.map +1 -0
- package/dist/session/conversation-tails.d.ts +33 -0
- package/dist/session/conversation-tails.d.ts.map +1 -0
- package/dist/session/conversation-tails.js +101 -0
- package/dist/session/conversation-tails.js.map +1 -0
- package/dist/session/keys.d.ts +68 -0
- package/dist/session/keys.d.ts.map +1 -0
- package/dist/session/keys.js +161 -0
- package/dist/session/keys.js.map +1 -0
- package/dist/session/session.d.ts +167 -26
- package/dist/session/session.d.ts.map +1 -1
- package/dist/session/session.js +1029 -171
- package/dist/session/session.js.map +1 -1
- package/dist/session/transaction.d.ts +81 -3
- package/dist/session/transaction.d.ts.map +1 -1
- package/dist/session/transaction.js +224 -37
- package/dist/session/transaction.js.map +1 -1
- package/dist/session/writer.d.ts +74 -0
- package/dist/session/writer.d.ts.map +1 -0
- package/dist/session/writer.js +294 -0
- package/dist/session/writer.js.map +1 -0
- package/dist/storage/indexeddb.d.ts +10 -2
- package/dist/storage/indexeddb.d.ts.map +1 -1
- package/dist/storage/indexeddb.js +91 -41
- package/dist/storage/indexeddb.js.map +1 -1
- package/dist/storage/jsonl/storage.d.ts +14 -2
- package/dist/storage/jsonl/storage.d.ts.map +1 -1
- package/dist/storage/jsonl/storage.js +88 -24
- package/dist/storage/jsonl/storage.js.map +1 -1
- package/dist/storage/memory.d.ts +7 -2
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +28 -2
- package/dist/storage/memory.js.map +1 -1
- package/dist/storage/sqlite/storage.d.ts +14 -2
- package/dist/storage/sqlite/storage.d.ts.map +1 -1
- package/dist/storage/sqlite/storage.js +110 -36
- package/dist/storage/sqlite/storage.js.map +1 -1
- package/dist/storage/store/commit.d.ts +33 -0
- package/dist/storage/store/commit.d.ts.map +1 -0
- package/dist/storage/store/commit.js +286 -0
- package/dist/storage/store/commit.js.map +1 -0
- package/dist/storage/store/documents.d.ts +43 -0
- package/dist/storage/store/documents.d.ts.map +1 -0
- package/dist/storage/store/documents.js +102 -0
- package/dist/storage/store/documents.js.map +1 -0
- package/dist/storage/store/index.d.ts +2 -0
- package/dist/storage/store/index.d.ts.map +1 -0
- package/dist/storage/store/index.js +2 -0
- package/dist/storage/store/index.js.map +1 -0
- package/dist/storage/store/queries.d.ts +49 -0
- package/dist/storage/store/queries.d.ts.map +1 -0
- package/dist/storage/store/queries.js +93 -0
- package/dist/storage/store/queries.js.map +1 -0
- package/dist/storage/store/storage.d.ts +59 -0
- package/dist/storage/store/storage.d.ts.map +1 -0
- package/dist/storage/store/storage.js +438 -0
- package/dist/storage/store/storage.js.map +1 -0
- package/dist/storage/store/tables.d.ts +54 -0
- package/dist/storage/store/tables.d.ts.map +1 -0
- package/dist/storage/store/tables.js +108 -0
- package/dist/storage/store/tables.js.map +1 -0
- package/dist/testing/commit-writes.d.ts +5 -0
- package/dist/testing/commit-writes.d.ts.map +1 -0
- package/dist/testing/commit-writes.js +12 -0
- package/dist/testing/commit-writes.js.map +1 -0
- package/dist/testing/env-conformance.js +7 -7
- package/dist/testing/env-conformance.js.map +1 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/storage-benchmark.d.ts.map +1 -1
- package/dist/testing/storage-benchmark.js +18 -17
- package/dist/testing/storage-benchmark.js.map +1 -1
- package/dist/testing/storage-conformance.d.ts.map +1 -1
- package/dist/testing/storage-conformance.js +354 -69
- package/dist/testing/storage-conformance.js.map +1 -1
- package/dist/tool-search/tool-search.d.ts +6 -12
- package/dist/tool-search/tool-search.d.ts.map +1 -1
- package/dist/tool-search/tool-search.js +7 -7
- package/dist/tool-search/tool-search.js.map +1 -1
- package/dist/tools/bash.d.ts +12 -12
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +16 -15
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +7 -7
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +9 -9
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/index.d.ts +32 -32
- package/dist/tools/read.d.ts +6 -6
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +19 -8
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/write.d.ts +4 -4
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js +5 -5
- package/dist/tools/write.js.map +1 -1
- package/dist/types.d.ts +223 -77
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +19 -12
- package/dist/harness/events.d.ts +0 -186
- package/dist/harness/events.d.ts.map +0 -1
- package/dist/harness/events.js +0 -373
- package/dist/harness/events.js.map +0 -1
- package/dist/session/observation.d.ts +0 -42
- package/dist/session/observation.d.ts.map +0 -1
- package/dist/session/observation.js +0 -281
- package/dist/session/observation.js.map +0 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
2
2
|
import { idFromNumber } from "../ids.js";
|
|
3
3
|
import { ROOT_CONVERSATION_ID, } from "../types.js";
|
|
4
|
+
import { commitWrites } from "./commit-writes.js";
|
|
4
5
|
const context = BACKGROUND_CONTEXT;
|
|
5
6
|
async function createRoot(storage) {
|
|
6
|
-
await storage
|
|
7
|
+
await commitWrites(storage, [{ type: "conversation", value: { id: ROOT_CONVERSATION_ID } }], context);
|
|
7
8
|
return ROOT_CONVERSATION_ID;
|
|
8
9
|
}
|
|
9
10
|
function pendingTask(id, conversationId, phase = "ready") {
|
|
@@ -38,8 +39,42 @@ function assertionFacade(assertions) {
|
|
|
38
39
|
},
|
|
39
40
|
});
|
|
40
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* `storage` with every commit checked against the receipt contract: one `receive` per stored batch, in order, with
|
|
44
|
+
* increasing seqs, all before the call settles.
|
|
45
|
+
*/
|
|
46
|
+
function checkReceipts(storage, assertions) {
|
|
47
|
+
const commit = async (batches, receive, commitContext) => {
|
|
48
|
+
const seqs = [];
|
|
49
|
+
let settled = false;
|
|
50
|
+
const recording = (index, seq) => {
|
|
51
|
+
assertions.ok(!settled, "Storage.commit() received a batch after it settled");
|
|
52
|
+
assertions.strictEqual(index, seqs.length);
|
|
53
|
+
if (seqs.length > 0)
|
|
54
|
+
assertions.greaterThan(seq, seqs.at(-1));
|
|
55
|
+
seqs.push(seq);
|
|
56
|
+
receive(index, seq);
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
const result = await storage.commit(batches, recording, commitContext);
|
|
60
|
+
assertions.strictEqual(seqs.length, result.stored);
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
settled = true;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
return new Proxy(storage, {
|
|
68
|
+
get(target, property) {
|
|
69
|
+
if (property === "commit")
|
|
70
|
+
return commit;
|
|
71
|
+
const value = Reflect.get(target, property, target);
|
|
72
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
41
76
|
function createCase(options, name, test) {
|
|
42
|
-
return { name, run: () => options.withStorage(test) };
|
|
77
|
+
return { name, run: () => options.withStorage((storage) => test(checkReceipts(storage, options.assertions))) };
|
|
43
78
|
}
|
|
44
79
|
/** Creates runner-independent cases. `withStorage` must call and await its callback exactly once per case. */
|
|
45
80
|
export function createStorageConformance(options) {
|
|
@@ -49,7 +84,30 @@ export function createStorageConformance(options) {
|
|
|
49
84
|
expect(await storage.mintId()).toBe(2);
|
|
50
85
|
await expect(createRoot(storage)).resolves.toBe(ROOT_CONVERSATION_ID);
|
|
51
86
|
expect(await storage.conversation(ROOT_CONVERSATION_ID, context)).toEqual({ id: ROOT_CONVERSATION_ID });
|
|
52
|
-
await expect(storage
|
|
87
|
+
await expect(commitWrites(storage, [{ type: "conversation", value: { id: ROOT_CONVERSATION_ID } }], context)).rejects.toThrow(`ID ${ROOT_CONVERSATION_ID} already belongs to conversation`);
|
|
88
|
+
}),
|
|
89
|
+
createCase(options, "receives a durable commit once before resolving, and never a refused one", async (storage) => {
|
|
90
|
+
const rootId = await createRoot(storage);
|
|
91
|
+
const events = [];
|
|
92
|
+
const entryId = await storage.mintId();
|
|
93
|
+
let read;
|
|
94
|
+
let seq;
|
|
95
|
+
const result = await storage.commit([[{ type: "entry", value: entry(entryId, rootId) }]], (_index, received) => {
|
|
96
|
+
seq = received;
|
|
97
|
+
events.push(`receive ${received}`);
|
|
98
|
+
// The batch is durable when it is received: a read started now observes it.
|
|
99
|
+
read = storage.entry(entryId, context);
|
|
100
|
+
}, context);
|
|
101
|
+
events.push(`resolve ${result.stored}`);
|
|
102
|
+
expect(events).toEqual([`receive ${seq}`, "resolve 1"]);
|
|
103
|
+
expect((await read)?.commitSeq).toBe(seq);
|
|
104
|
+
events.length = 0;
|
|
105
|
+
const refused = await storage.commit([[{ type: "conversation", value: { id: rootId } }]], (_index, received) => events.push(`receive ${received}`), context);
|
|
106
|
+
expect(refused.stored).toBe(0);
|
|
107
|
+
expect(String(refused.refused)).toBe(`Error: ID ${rootId} already belongs to conversation`);
|
|
108
|
+
expect(events).toEqual([]);
|
|
109
|
+
const next = await commitWrites(storage, [{ type: "entry", value: entry(await storage.mintId(), rootId) }], context);
|
|
110
|
+
expect(next).toBeGreaterThan(seq);
|
|
53
111
|
}),
|
|
54
112
|
createCase(options, "commits mixed table writes atomically and rolls all of them back on failure", async (storage) => {
|
|
55
113
|
const rootId = await createRoot(storage);
|
|
@@ -65,7 +123,7 @@ export function createStorageConformance(options) {
|
|
|
65
123
|
status: "placed",
|
|
66
124
|
entry: entryId,
|
|
67
125
|
};
|
|
68
|
-
const initialSeq = await storage
|
|
126
|
+
const initialSeq = await commitWrites(storage, [
|
|
69
127
|
{ type: "entry", value: entry(entryId, rootId, "user", { data: { text: "hello" } }) },
|
|
70
128
|
{ type: "task", value: task },
|
|
71
129
|
{ type: "submission", value: input },
|
|
@@ -82,7 +140,7 @@ export function createStorageConformance(options) {
|
|
|
82
140
|
state: { status: "running", checkpoint: { phase: "effect" } },
|
|
83
141
|
};
|
|
84
142
|
const doneInput = { ...input, status: "done", answer: transientEntryId };
|
|
85
|
-
await expect(storage
|
|
143
|
+
await expect(commitWrites(storage, [
|
|
86
144
|
{ type: "task", value: runningTask },
|
|
87
145
|
{ type: "submission", value: doneInput },
|
|
88
146
|
{ type: "entry", value: entry(transientEntryId, rootId, "assistant") },
|
|
@@ -91,7 +149,7 @@ export function createStorageConformance(options) {
|
|
|
91
149
|
expect(await storage.task(taskId, context)).toEqual(task);
|
|
92
150
|
expect(await storage.submission(submissionId, context)).toEqual(input);
|
|
93
151
|
expect(await storage.entry(transientEntryId, context)).toBeUndefined();
|
|
94
|
-
const afterRollbackSeq = await storage
|
|
152
|
+
const afterRollbackSeq = await commitWrites(storage, [{ type: "entry", value: entry(await storage.mintId(), rootId, "after-rollback") }], context);
|
|
95
153
|
expect(afterRollbackSeq).toBeGreaterThan(initialSeq);
|
|
96
154
|
}),
|
|
97
155
|
createCase(options, "detaches retained writes and every returned record", async (storage) => {
|
|
@@ -115,7 +173,7 @@ export function createStorageConformance(options) {
|
|
|
115
173
|
reason: "failed",
|
|
116
174
|
detail,
|
|
117
175
|
};
|
|
118
|
-
await storage
|
|
176
|
+
await commitWrites(storage, [
|
|
119
177
|
{ type: "entry", value: storedEntry },
|
|
120
178
|
{ type: "task", value: storedTask },
|
|
121
179
|
{ type: "submission", value: storedInput },
|
|
@@ -148,7 +206,7 @@ export function createStorageConformance(options) {
|
|
|
148
206
|
const rootId = await createRoot(storage);
|
|
149
207
|
const entryId = await storage.mintId();
|
|
150
208
|
const data = JSON.parse('{"__proto__":{"polluted":false},"constructor":{"label":"stored"},"toString":"value"}');
|
|
151
|
-
await storage
|
|
209
|
+
await commitWrites(storage, [{ type: "entry", value: entry(entryId, rootId, "note", { data }) }], context);
|
|
152
210
|
Reflect.get(data, "__proto__").polluted = true;
|
|
153
211
|
Reflect.get(data, "constructor").label = "mutated";
|
|
154
212
|
const firstRead = (await storage.entry(entryId, context)).entry.data;
|
|
@@ -166,7 +224,7 @@ export function createStorageConformance(options) {
|
|
|
166
224
|
}),
|
|
167
225
|
createCase(options, "indexes entries committed out of ID order", async (storage) => {
|
|
168
226
|
const rootId = await createRoot(storage);
|
|
169
|
-
await storage
|
|
227
|
+
await commitWrites(storage, [
|
|
170
228
|
{ type: "entry", value: entry(idFromNumber(30), rootId) },
|
|
171
229
|
{ type: "entry", value: entry(idFromNumber(10), rootId) },
|
|
172
230
|
{
|
|
@@ -184,7 +242,7 @@ export function createStorageConformance(options) {
|
|
|
184
242
|
const oldestId = await storage.mintId();
|
|
185
243
|
const middleId = await storage.mintId();
|
|
186
244
|
const newestId = await storage.mintId();
|
|
187
|
-
await storage
|
|
245
|
+
await commitWrites(storage, [
|
|
188
246
|
{ type: "entry", value: entry(oldestId, rootId) },
|
|
189
247
|
{ type: "entry", value: entry(middleId, rootId) },
|
|
190
248
|
{ type: "entry", value: entry(newestId, rootId) },
|
|
@@ -192,7 +250,7 @@ export function createStorageConformance(options) {
|
|
|
192
250
|
const first = await storage.scanEntries({ conversationId: rootId }, 2, undefined, context);
|
|
193
251
|
expect(first.items.map(({ id }) => id)).toEqual([newestId, middleId]);
|
|
194
252
|
const appendedId = await storage.mintId();
|
|
195
|
-
await storage
|
|
253
|
+
await commitWrites(storage, [{ type: "entry", value: entry(appendedId, rootId) }], context);
|
|
196
254
|
const second = await storage.scanEntries({ conversationId: rootId }, 2, first.next, context);
|
|
197
255
|
expect(second.items.map(({ id }) => id)).toEqual([oldestId]);
|
|
198
256
|
expect(second.next).toBeUndefined();
|
|
@@ -201,7 +259,7 @@ export function createStorageConformance(options) {
|
|
|
201
259
|
const rootId = await createRoot(storage);
|
|
202
260
|
const secondId = await storage.mintId();
|
|
203
261
|
const thirdId = await storage.mintId();
|
|
204
|
-
await storage
|
|
262
|
+
await commitWrites(storage, [
|
|
205
263
|
{ type: "conversation", value: { id: thirdId } },
|
|
206
264
|
{ type: "conversation", value: { id: secondId } },
|
|
207
265
|
], context);
|
|
@@ -221,7 +279,7 @@ export function createStorageConformance(options) {
|
|
|
221
279
|
const firstId = await storage.mintId();
|
|
222
280
|
const secondId = await storage.mintId();
|
|
223
281
|
const thirdId = await storage.mintId();
|
|
224
|
-
await storage
|
|
282
|
+
await commitWrites(storage, [
|
|
225
283
|
{ type: "conversation", value: { id: otherOwnerId } },
|
|
226
284
|
{
|
|
227
285
|
type: "conversation",
|
|
@@ -250,7 +308,7 @@ export function createStorageConformance(options) {
|
|
|
250
308
|
const rootFirst = await storage.mintId();
|
|
251
309
|
const rootForkPoint = await storage.mintId();
|
|
252
310
|
const rootExcludedSameCommit = await storage.mintId();
|
|
253
|
-
const rootEntriesSeq = await storage
|
|
311
|
+
const rootEntriesSeq = await commitWrites(storage, [
|
|
254
312
|
{ type: "entry", value: entry(rootFirst, rootId) },
|
|
255
313
|
{
|
|
256
314
|
type: "entry",
|
|
@@ -259,7 +317,7 @@ export function createStorageConformance(options) {
|
|
|
259
317
|
{ type: "entry", value: entry(rootExcludedSameCommit, rootId) },
|
|
260
318
|
], context);
|
|
261
319
|
const childId = await storage.mintId();
|
|
262
|
-
await storage
|
|
320
|
+
await commitWrites(storage, [
|
|
263
321
|
{
|
|
264
322
|
type: "conversation",
|
|
265
323
|
value: { id: childId, parent: { conversationId: rootId, at: rootForkPoint } },
|
|
@@ -267,14 +325,14 @@ export function createStorageConformance(options) {
|
|
|
267
325
|
], context);
|
|
268
326
|
const childForkPoint = await storage.mintId();
|
|
269
327
|
const childExcluded = await storage.mintId();
|
|
270
|
-
await storage
|
|
328
|
+
await commitWrites(storage, [
|
|
271
329
|
{ type: "entry", value: entry(childForkPoint, childId, "note") },
|
|
272
330
|
{ type: "entry", value: entry(childExcluded, childId) },
|
|
273
331
|
], context);
|
|
274
332
|
const rootExcludedLater = await storage.mintId();
|
|
275
|
-
await storage
|
|
333
|
+
await commitWrites(storage, [{ type: "entry", value: entry(rootExcludedLater, rootId) }], context);
|
|
276
334
|
const grandchildId = await storage.mintId();
|
|
277
|
-
await storage
|
|
335
|
+
await commitWrites(storage, [
|
|
278
336
|
{
|
|
279
337
|
type: "conversation",
|
|
280
338
|
value: { id: grandchildId, parent: { conversationId: childId, at: childForkPoint } },
|
|
@@ -282,7 +340,7 @@ export function createStorageConformance(options) {
|
|
|
282
340
|
], context);
|
|
283
341
|
const grandchildHead = await storage.mintId();
|
|
284
342
|
const grandchildTail = await storage.mintId();
|
|
285
|
-
const grandchildEntriesSeq = await storage
|
|
343
|
+
const grandchildEntriesSeq = await commitWrites(storage, [
|
|
286
344
|
{
|
|
287
345
|
type: "entry",
|
|
288
346
|
value: entry(grandchildHead, grandchildId, "marker", { head: grandchildHead }),
|
|
@@ -290,7 +348,7 @@ export function createStorageConformance(options) {
|
|
|
290
348
|
{ type: "entry", value: entry(grandchildTail, grandchildId) },
|
|
291
349
|
], context);
|
|
292
350
|
const childExcludedLater = await storage.mintId();
|
|
293
|
-
await storage
|
|
351
|
+
await commitWrites(storage, [{ type: "entry", value: entry(childExcludedLater, childId) }], context);
|
|
294
352
|
const first = await storage.scanEntries({ conversationId: grandchildId }, 2, undefined, context);
|
|
295
353
|
expect(first.items.map(({ id }) => id)).toEqual([grandchildTail, grandchildHead]);
|
|
296
354
|
const second = await storage.scanEntries({ conversationId: grandchildId }, 2, first.next, context);
|
|
@@ -359,7 +417,7 @@ export function createStorageConformance(options) {
|
|
|
359
417
|
const conversationId = await storage.mintId();
|
|
360
418
|
const taskId = await storage.mintId();
|
|
361
419
|
const submissionId = await storage.mintId();
|
|
362
|
-
await storage
|
|
420
|
+
await commitWrites(storage, [
|
|
363
421
|
{ type: "conversation", value: { id: conversationId } },
|
|
364
422
|
{ type: "task", value: pendingTask(taskId, rootId) },
|
|
365
423
|
{
|
|
@@ -405,7 +463,7 @@ export function createStorageConformance(options) {
|
|
|
405
463
|
const first = { ...pendingTask(firstId, rootId), memos: { winner: "first" } };
|
|
406
464
|
const second = { ...pendingTask(secondId, rootId), background: true };
|
|
407
465
|
const third = { ...pendingTask(thirdId, rootId), abortRequested: true };
|
|
408
|
-
await storage
|
|
466
|
+
await commitWrites(storage, [
|
|
409
467
|
{ type: "task", value: first },
|
|
410
468
|
{ type: "task", value: second },
|
|
411
469
|
{ type: "task", value: third },
|
|
@@ -415,7 +473,7 @@ export function createStorageConformance(options) {
|
|
|
415
473
|
state: { status: "running", checkpoint: { phase: "effect", attempt: 1 } },
|
|
416
474
|
abortRequested: true,
|
|
417
475
|
};
|
|
418
|
-
await storage
|
|
476
|
+
await commitWrites(storage, [{ type: "task", value: running }], context);
|
|
419
477
|
expect(await storage.task(firstId, context)).toEqual(running);
|
|
420
478
|
const terminal = {
|
|
421
479
|
id: firstId,
|
|
@@ -427,7 +485,7 @@ export function createStorageConformance(options) {
|
|
|
427
485
|
background: false,
|
|
428
486
|
abortRequested: true,
|
|
429
487
|
};
|
|
430
|
-
await storage
|
|
488
|
+
await commitWrites(storage, [{ type: "task", value: terminal }], context);
|
|
431
489
|
expect(await storage.task(firstId, context)).toEqual(terminal);
|
|
432
490
|
const pendingPage = await storage.scanTasks({ status: "pending" }, 1, undefined, context);
|
|
433
491
|
expect(pendingPage.items.map(({ id }) => id)).toEqual([secondId]);
|
|
@@ -455,7 +513,7 @@ export function createStorageConformance(options) {
|
|
|
455
513
|
state: { status: "completing", outcome: { status: "failed", error: { message: "held" } } },
|
|
456
514
|
};
|
|
457
515
|
const writes = [owner, waiting, completing].map((value) => ({ type: "task", value }));
|
|
458
|
-
await storage
|
|
516
|
+
await commitWrites(storage, writes, context);
|
|
459
517
|
expect(await storage.task(waitingId, context)).toEqual(waiting);
|
|
460
518
|
expect(await storage.task(completingId, context)).toEqual(completing);
|
|
461
519
|
const scan = async (status) => (await storage.scanTasks({ status }, 10, undefined, context)).items;
|
|
@@ -466,7 +524,7 @@ export function createStorageConformance(options) {
|
|
|
466
524
|
...completing,
|
|
467
525
|
state: { status: "terminal", outcome: completing.state.outcome },
|
|
468
526
|
};
|
|
469
|
-
await storage
|
|
527
|
+
await commitWrites(storage, [{ type: "task", value: terminal }], context);
|
|
470
528
|
expect(await scan("completing")).toEqual([]);
|
|
471
529
|
expect(await scan("terminal")).toEqual([terminal]);
|
|
472
530
|
}),
|
|
@@ -478,19 +536,19 @@ export function createStorageConformance(options) {
|
|
|
478
536
|
state: { status: "sleeping", checkpoint: { phase: "next" }, until: 61_000 },
|
|
479
537
|
memos: { kept: true },
|
|
480
538
|
};
|
|
481
|
-
await storage
|
|
539
|
+
await commitWrites(storage, [{ type: "task", value: sleeping }], context);
|
|
482
540
|
expect(await storage.task(sleepingId, context)).toEqual(sleeping);
|
|
483
541
|
const scan = async (status) => (await storage.scanTasks({ status }, 10, undefined, context)).items;
|
|
484
542
|
expect(await scan("sleeping")).toEqual([sleeping]);
|
|
485
543
|
const running = { ...sleeping, state: { status: "running", checkpoint: { phase: "next" } } };
|
|
486
|
-
await storage
|
|
544
|
+
await commitWrites(storage, [{ type: "task", value: running }], context);
|
|
487
545
|
expect(await scan("sleeping")).toEqual([]);
|
|
488
546
|
expect(await scan("running")).toEqual([running]);
|
|
489
547
|
}),
|
|
490
548
|
createCase(options, "indexes request IDs per conversation and replaces complete submission records", async (storage) => {
|
|
491
549
|
const rootId = await createRoot(storage);
|
|
492
550
|
const secondConversationId = await storage.mintId();
|
|
493
|
-
await storage
|
|
551
|
+
await commitWrites(storage, [{ type: "conversation", value: { id: secondConversationId } }], context);
|
|
494
552
|
const firstId = await storage.mintId();
|
|
495
553
|
const secondId = await storage.mintId();
|
|
496
554
|
const otherConversationId = await storage.mintId();
|
|
@@ -515,7 +573,7 @@ export function createStorageConformance(options) {
|
|
|
515
573
|
type: "input",
|
|
516
574
|
status: "queued",
|
|
517
575
|
};
|
|
518
|
-
await storage
|
|
576
|
+
await commitWrites(storage, [
|
|
519
577
|
{ type: "submission", value: first },
|
|
520
578
|
{ type: "submission", value: second },
|
|
521
579
|
{ type: "submission", value: otherConversation },
|
|
@@ -527,7 +585,7 @@ export function createStorageConformance(options) {
|
|
|
527
585
|
status: "placed",
|
|
528
586
|
entry: await storage.mintId(),
|
|
529
587
|
};
|
|
530
|
-
await storage
|
|
588
|
+
await commitWrites(storage, [{ type: "submission", value: placedSecond }], context);
|
|
531
589
|
expect(await storage.submission(secondId, context)).toEqual(placedSecond);
|
|
532
590
|
expect(await storage.submissionByRequest(rootId, "other", context)).toEqual(placedSecond);
|
|
533
591
|
const ids = async (query) => {
|
|
@@ -571,7 +629,7 @@ export function createStorageConformance(options) {
|
|
|
571
629
|
type: "write",
|
|
572
630
|
status: "queued",
|
|
573
631
|
};
|
|
574
|
-
await storage
|
|
632
|
+
await commitWrites(storage, [
|
|
575
633
|
{ type: "submission", value: queuedDone },
|
|
576
634
|
{ type: "submission", value: queuedFailed },
|
|
577
635
|
], context);
|
|
@@ -586,7 +644,7 @@ export function createStorageConformance(options) {
|
|
|
586
644
|
reason: "closed",
|
|
587
645
|
detail: { retryable: false },
|
|
588
646
|
};
|
|
589
|
-
await storage
|
|
647
|
+
await commitWrites(storage, [
|
|
590
648
|
{ type: "submission", value: done },
|
|
591
649
|
{ type: "submission", value: unanswered },
|
|
592
650
|
], context);
|
|
@@ -595,6 +653,44 @@ export function createStorageConformance(options) {
|
|
|
595
653
|
expect(await storage.submission(failedId, context)).toEqual(unanswered);
|
|
596
654
|
expect(await storage.submissionByRequest(rootId, "passive-failed", context)).toEqual(unanswered);
|
|
597
655
|
}),
|
|
656
|
+
createCase(options, "stores continue submissions, which have no entry and are never queued", async (storage) => {
|
|
657
|
+
const rootId = await createRoot(storage);
|
|
658
|
+
const doneId = await storage.mintId();
|
|
659
|
+
const failedId = await storage.mintId();
|
|
660
|
+
const placedDone = {
|
|
661
|
+
id: doneId,
|
|
662
|
+
conversationId: rootId,
|
|
663
|
+
requestId: "continue-done",
|
|
664
|
+
type: "continue",
|
|
665
|
+
status: "placed",
|
|
666
|
+
};
|
|
667
|
+
const placedFailed = {
|
|
668
|
+
id: failedId,
|
|
669
|
+
conversationId: rootId,
|
|
670
|
+
requestId: "continue-failed",
|
|
671
|
+
type: "continue",
|
|
672
|
+
status: "placed",
|
|
673
|
+
};
|
|
674
|
+
await commitWrites(storage, [
|
|
675
|
+
{ type: "submission", value: placedDone },
|
|
676
|
+
{ type: "submission", value: placedFailed },
|
|
677
|
+
], context);
|
|
678
|
+
expect(await storage.submission(doneId, context)).toEqual(placedDone);
|
|
679
|
+
expect((await storage.scanSubmissions({ status: "placed" }, 10, undefined, context)).items).toEqual([
|
|
680
|
+
placedDone,
|
|
681
|
+
placedFailed,
|
|
682
|
+
]);
|
|
683
|
+
const done = { ...placedDone, status: "done", answer: await storage.mintId() };
|
|
684
|
+
const unanswered = { ...placedFailed, status: "unanswered", reason: "nothing_to_answer" };
|
|
685
|
+
await commitWrites(storage, [
|
|
686
|
+
{ type: "submission", value: done },
|
|
687
|
+
{ type: "submission", value: unanswered },
|
|
688
|
+
], context);
|
|
689
|
+
expect(await storage.submission(doneId, context)).toEqual(done);
|
|
690
|
+
expect(await storage.submissionByRequest(rootId, "continue-done", context)).toEqual(done);
|
|
691
|
+
expect(await storage.submission(failedId, context)).toEqual(unanswered);
|
|
692
|
+
expect(await storage.submissionByRequest(rootId, "continue-failed", context)).toEqual(unanswered);
|
|
693
|
+
}),
|
|
598
694
|
createCase(options, "reconstructs rewindable documents and preserves half-open incarnations", async (storage) => {
|
|
599
695
|
const rootId = await createRoot(storage);
|
|
600
696
|
const firstId = await storage.mintId();
|
|
@@ -606,7 +702,7 @@ export function createStorageConformance(options) {
|
|
|
606
702
|
fork: "asOf",
|
|
607
703
|
};
|
|
608
704
|
const initial = { items: ["a"], nested: { count: 1 } };
|
|
609
|
-
const createdAt = await storage
|
|
705
|
+
const createdAt = await commitWrites(storage, [
|
|
610
706
|
{
|
|
611
707
|
type: "document.create",
|
|
612
708
|
record: firstRecord,
|
|
@@ -618,7 +714,7 @@ export function createStorageConformance(options) {
|
|
|
618
714
|
["p", ["items"], 1, 0, appended],
|
|
619
715
|
["s", ["nested", "count"], 2],
|
|
620
716
|
];
|
|
621
|
-
const changedAt = await storage
|
|
717
|
+
const changedAt = await commitWrites(storage, [{ type: "document.change", id: firstId, content: { kind: "delta", version: 1, ops } }], context);
|
|
622
718
|
initial.items.push("caller mutation");
|
|
623
719
|
appended.push("caller mutation");
|
|
624
720
|
expect(await storage.document(firstId, createdAt, context)).toMatchObject({
|
|
@@ -634,14 +730,14 @@ export function createStorageConformance(options) {
|
|
|
634
730
|
items: ["a", "b"],
|
|
635
731
|
nested: { count: 2 },
|
|
636
732
|
});
|
|
637
|
-
const checkpointAt = await storage
|
|
733
|
+
const checkpointAt = await commitWrites(storage, [
|
|
638
734
|
{
|
|
639
735
|
type: "document.change",
|
|
640
736
|
id: firstId,
|
|
641
737
|
content: { kind: "base", version: 2, value: { items: ["checkpoint"], nested: { count: 3 } } },
|
|
642
738
|
},
|
|
643
739
|
], context);
|
|
644
|
-
const replacedAt = await storage
|
|
740
|
+
const replacedAt = await commitWrites(storage, [
|
|
645
741
|
{
|
|
646
742
|
type: "document.change",
|
|
647
743
|
id: firstId,
|
|
@@ -667,7 +763,7 @@ export function createStorageConformance(options) {
|
|
|
667
763
|
});
|
|
668
764
|
expect((await storage.document(firstId, "current", context))?.deltasSinceBase).toBe(1);
|
|
669
765
|
const secondId = await storage.mintId();
|
|
670
|
-
const retiredAt = await storage
|
|
766
|
+
const retiredAt = await commitWrites(storage, [
|
|
671
767
|
{
|
|
672
768
|
type: "document.create",
|
|
673
769
|
record: { ...firstRecord, id: secondId },
|
|
@@ -705,14 +801,14 @@ export function createStorageConformance(options) {
|
|
|
705
801
|
revision: 0,
|
|
706
802
|
rows: Array.from({ length: 512 }, (_, value) => ({ value, stable: `row-${value}` })),
|
|
707
803
|
};
|
|
708
|
-
const createdAt = await storage
|
|
804
|
+
const createdAt = await commitWrites(storage, [{ type: "document.create", record, content: { kind: "base", version: 1, value: initial } }], context);
|
|
709
805
|
const beforeReplacement = structuredClone(initial);
|
|
710
806
|
let beforeReplacementAt = createdAt;
|
|
711
807
|
for (let revision = 1; revision <= 24; revision++) {
|
|
712
808
|
const index = (revision * 17) % beforeReplacement.rows.length;
|
|
713
809
|
beforeReplacement.rows[index].value = -revision;
|
|
714
810
|
beforeReplacement.revision = revision;
|
|
715
|
-
beforeReplacementAt = await storage
|
|
811
|
+
beforeReplacementAt = await commitWrites(storage, [
|
|
716
812
|
{
|
|
717
813
|
type: "document.change",
|
|
718
814
|
id,
|
|
@@ -732,7 +828,7 @@ export function createStorageConformance(options) {
|
|
|
732
828
|
rows: Array.from({ length: 512 }, (_, value) => ({ value: 10_000 + value, stable: `new-${value}` })),
|
|
733
829
|
};
|
|
734
830
|
const replacementSnapshot = structuredClone(replacement);
|
|
735
|
-
const replacementAt = await storage
|
|
831
|
+
const replacementAt = await commitWrites(storage, [
|
|
736
832
|
{
|
|
737
833
|
type: "document.change",
|
|
738
834
|
id,
|
|
@@ -745,7 +841,7 @@ export function createStorageConformance(options) {
|
|
|
745
841
|
const index = (revision * 19) % current.rows.length;
|
|
746
842
|
current.rows[index].value = -revision;
|
|
747
843
|
current.revision = revision;
|
|
748
|
-
await storage
|
|
844
|
+
await commitWrites(storage, [
|
|
749
845
|
{
|
|
750
846
|
type: "document.change",
|
|
751
847
|
id,
|
|
@@ -772,7 +868,7 @@ export function createStorageConformance(options) {
|
|
|
772
868
|
const rootId = await createRoot(storage);
|
|
773
869
|
const childId = await storage.mintId();
|
|
774
870
|
const secondChildId = await storage.mintId();
|
|
775
|
-
await storage
|
|
871
|
+
await commitWrites(storage, [
|
|
776
872
|
{ type: "conversation", value: { id: childId } },
|
|
777
873
|
{ type: "conversation", value: { id: secondChildId } },
|
|
778
874
|
], context);
|
|
@@ -784,14 +880,14 @@ export function createStorageConformance(options) {
|
|
|
784
880
|
history: "rewindable",
|
|
785
881
|
fork: "asOf",
|
|
786
882
|
};
|
|
787
|
-
const createdAt = await storage
|
|
883
|
+
const createdAt = await commitWrites(storage, [
|
|
788
884
|
{
|
|
789
885
|
type: "document.create",
|
|
790
886
|
record: sourceRecord,
|
|
791
887
|
content: { kind: "base", version: 2, value: { count: 1, rows: [{ value: "base" }] } },
|
|
792
888
|
},
|
|
793
889
|
], context);
|
|
794
|
-
await storage
|
|
890
|
+
await commitWrites(storage, [
|
|
795
891
|
{
|
|
796
892
|
type: "document.change",
|
|
797
893
|
id: sourceId,
|
|
@@ -815,7 +911,7 @@ export function createStorageConformance(options) {
|
|
|
815
911
|
history: "rewindable",
|
|
816
912
|
fork: "asOf",
|
|
817
913
|
});
|
|
818
|
-
await storage
|
|
914
|
+
await commitWrites(storage, [
|
|
819
915
|
{
|
|
820
916
|
type: "document.copy",
|
|
821
917
|
record: childRecord(historicalCopyId, childId),
|
|
@@ -842,7 +938,7 @@ export function createStorageConformance(options) {
|
|
|
842
938
|
value: { count: 2, rows: [{ value: "base" }, { value: "current" }] },
|
|
843
939
|
});
|
|
844
940
|
expect(await storage.document(retiredCopyId, "current", context)).toBeUndefined();
|
|
845
|
-
await storage
|
|
941
|
+
await commitWrites(storage, [
|
|
846
942
|
{
|
|
847
943
|
type: "document.change",
|
|
848
944
|
id: sourceId,
|
|
@@ -863,14 +959,14 @@ export function createStorageConformance(options) {
|
|
|
863
959
|
history: "latest",
|
|
864
960
|
fork: "current",
|
|
865
961
|
};
|
|
866
|
-
await storage
|
|
962
|
+
await commitWrites(storage, [
|
|
867
963
|
{
|
|
868
964
|
type: "document.create",
|
|
869
965
|
record: latestSource,
|
|
870
966
|
content: { kind: "base", version: 4, value: { retained: "copy" } },
|
|
871
967
|
},
|
|
872
968
|
], context);
|
|
873
|
-
await storage
|
|
969
|
+
await commitWrites(storage, [
|
|
874
970
|
{
|
|
875
971
|
type: "document.copy",
|
|
876
972
|
record: {
|
|
@@ -881,7 +977,7 @@ export function createStorageConformance(options) {
|
|
|
881
977
|
source: { id: latestSourceId, at: "current" },
|
|
882
978
|
},
|
|
883
979
|
], context);
|
|
884
|
-
await storage
|
|
980
|
+
await commitWrites(storage, [
|
|
885
981
|
{
|
|
886
982
|
type: "document.change",
|
|
887
983
|
id: latestSourceId,
|
|
@@ -896,7 +992,7 @@ export function createStorageConformance(options) {
|
|
|
896
992
|
const conflictId = await storage.mintId();
|
|
897
993
|
let conflictError;
|
|
898
994
|
try {
|
|
899
|
-
await storage
|
|
995
|
+
await commitWrites(storage, [
|
|
900
996
|
{
|
|
901
997
|
type: "document.copy",
|
|
902
998
|
record: childRecord(conflictId, childId),
|
|
@@ -918,7 +1014,7 @@ export function createStorageConformance(options) {
|
|
|
918
1014
|
const mismatchId = await storage.mintId();
|
|
919
1015
|
let mismatchError;
|
|
920
1016
|
try {
|
|
921
|
-
await storage
|
|
1017
|
+
await commitWrites(storage, [
|
|
922
1018
|
{
|
|
923
1019
|
type: "document.copy",
|
|
924
1020
|
record: { ...childRecord(mismatchId, childId), kind: "copy.mismatch" },
|
|
@@ -940,14 +1036,14 @@ export function createStorageConformance(options) {
|
|
|
940
1036
|
kind: "session.settings",
|
|
941
1037
|
scope: { kind: "session" },
|
|
942
1038
|
};
|
|
943
|
-
await storage
|
|
944
|
-
await storage
|
|
945
|
-
const migratedAt = await storage
|
|
1039
|
+
await commitWrites(storage, [{ type: "document.create", record, content: { kind: "base", version: 1, value: { count: 1 } } }], context);
|
|
1040
|
+
await commitWrites(storage, [{ type: "document.change", id, content: { kind: "delta", version: 1, ops: [["s", ["count"], 2]] } }], context);
|
|
1041
|
+
const migratedAt = await commitWrites(storage, [{ type: "document.change", id, content: { kind: "base", version: 2, value: { count: 3 } } }], context);
|
|
946
1042
|
expect(await storage.document(id, "current", context)).toMatchObject({ version: 2, value: { count: 3 } });
|
|
947
1043
|
await expect(storage.document(id, migratedAt, context)).rejects.toThrow("does not retain historical content");
|
|
948
|
-
await expect(storage
|
|
1044
|
+
await expect(commitWrites(storage, [{ type: "document.change", id, content: { kind: "delta", version: 1, ops: [["s", ["count"], 4]] } }], context)).rejects.toThrow("version transition requires a base");
|
|
949
1045
|
expect((await storage.document(id, "current", context))?.value).toEqual({ count: 3 });
|
|
950
|
-
await storage
|
|
1046
|
+
await commitWrites(storage, [{ type: "document.retire", id }], context);
|
|
951
1047
|
expect(await storage.document(id, "current", context)).toBeUndefined();
|
|
952
1048
|
}),
|
|
953
1049
|
createCase(options, "indexes logical addresses and exact-scope scans independently", async (storage) => {
|
|
@@ -959,7 +1055,7 @@ export function createStorageConformance(options) {
|
|
|
959
1055
|
const taskSingletonId = await storage.mintId();
|
|
960
1056
|
const taskFamilyId = await storage.mintId();
|
|
961
1057
|
const taskOtherKindId = await storage.mintId();
|
|
962
|
-
const createdAt = await storage
|
|
1058
|
+
const createdAt = await commitWrites(storage, [
|
|
963
1059
|
{ type: "task", value: pendingTask(taskId, rootId) },
|
|
964
1060
|
{
|
|
965
1061
|
type: "document.create",
|
|
@@ -1043,8 +1139,8 @@ export function createStorageConformance(options) {
|
|
|
1043
1139
|
kind: "singleton",
|
|
1044
1140
|
scope: { kind: "session" },
|
|
1045
1141
|
};
|
|
1046
|
-
await storage
|
|
1047
|
-
await expect(storage
|
|
1142
|
+
await commitWrites(storage, [{ type: "document.create", record, content: { kind: "base", version: 1, value: { value: 1 } } }], context);
|
|
1143
|
+
await expect(commitWrites(storage, [
|
|
1048
1144
|
{
|
|
1049
1145
|
type: "document.create",
|
|
1050
1146
|
record: { ...record, id: secondId },
|
|
@@ -1055,7 +1151,7 @@ export function createStorageConformance(options) {
|
|
|
1055
1151
|
expect((await storage.document(firstId, "current", context))?.value).toEqual({ value: 1 });
|
|
1056
1152
|
expect(await storage.document(secondId, "current", context)).toBeUndefined();
|
|
1057
1153
|
const emptyId = await storage.mintId();
|
|
1058
|
-
const emptyAt = await storage
|
|
1154
|
+
const emptyAt = await commitWrites(storage, [
|
|
1059
1155
|
{
|
|
1060
1156
|
type: "document.create",
|
|
1061
1157
|
record: {
|
|
@@ -1078,6 +1174,25 @@ export function createStorageConformance(options) {
|
|
|
1078
1174
|
key: "empty",
|
|
1079
1175
|
}, emptyAt, context)).toBeUndefined();
|
|
1080
1176
|
}),
|
|
1177
|
+
createCase(options, "refuses a creation and a change of one document in one commit, in either order", async (storage) => {
|
|
1178
|
+
await createRoot(storage);
|
|
1179
|
+
for (const order of ["create first", "change first"]) {
|
|
1180
|
+
const id = await storage.mintId();
|
|
1181
|
+
const create = {
|
|
1182
|
+
type: "document.create",
|
|
1183
|
+
record: { id, kind: "twice", key: order, scope: { kind: "session" } },
|
|
1184
|
+
content: { kind: "base", version: 1, value: { x: 1 } },
|
|
1185
|
+
};
|
|
1186
|
+
const change = {
|
|
1187
|
+
type: "document.change",
|
|
1188
|
+
id,
|
|
1189
|
+
content: { kind: "delta", version: 1, ops: [["s", ["x"], 2]] },
|
|
1190
|
+
};
|
|
1191
|
+
const writes = order === "create first" ? [create, change] : [change, create];
|
|
1192
|
+
await expect(commitWrites(storage, writes, context)).rejects.toThrow("more than one content command");
|
|
1193
|
+
expect(await storage.document(id, "current", context)).toBeUndefined();
|
|
1194
|
+
}
|
|
1195
|
+
}),
|
|
1081
1196
|
createCase(options, "rolls back record tables and secondary indexes when a document command fails", async (storage) => {
|
|
1082
1197
|
const rootId = await createRoot(storage);
|
|
1083
1198
|
const taskId = await storage.mintId();
|
|
@@ -1096,14 +1211,14 @@ export function createStorageConformance(options) {
|
|
|
1096
1211
|
kind: "atomic",
|
|
1097
1212
|
scope: { kind: "session" },
|
|
1098
1213
|
};
|
|
1099
|
-
const baselineSeq = await storage
|
|
1214
|
+
const baselineSeq = await commitWrites(storage, [
|
|
1100
1215
|
{ type: "task", value: task },
|
|
1101
1216
|
{ type: "submission", value: submission },
|
|
1102
1217
|
{ type: "document.create", record, content: { kind: "base", version: 1, value: { count: 1 } } },
|
|
1103
1218
|
], context);
|
|
1104
1219
|
const entryId = await storage.mintId();
|
|
1105
1220
|
const conflictingDocumentId = await storage.mintId();
|
|
1106
|
-
await expect(storage
|
|
1221
|
+
await expect(commitWrites(storage, [
|
|
1107
1222
|
{
|
|
1108
1223
|
type: "task",
|
|
1109
1224
|
value: { ...task, state: { status: "running", checkpoint: { phase: "effect" } } },
|
|
@@ -1125,7 +1240,7 @@ export function createStorageConformance(options) {
|
|
|
1125
1240
|
expect(await storage.entry(entryId, context)).toBeUndefined();
|
|
1126
1241
|
expect(await storage.document(conflictingDocumentId, "current", context)).toBeUndefined();
|
|
1127
1242
|
expect((await storage.findDocument({ kind: record.kind, scope: record.scope }, "current", context))?.id).toBe(documentId);
|
|
1128
|
-
const afterRollbackSeq = await storage
|
|
1243
|
+
const afterRollbackSeq = await commitWrites(storage, [
|
|
1129
1244
|
{
|
|
1130
1245
|
type: "document.change",
|
|
1131
1246
|
id: documentId,
|
|
@@ -1146,7 +1261,7 @@ export function createStorageConformance(options) {
|
|
|
1146
1261
|
const secondKindDocumentId = await storage.mintId();
|
|
1147
1262
|
const firstKeyDocumentId = await storage.mintId();
|
|
1148
1263
|
const secondKeyDocumentId = await storage.mintId();
|
|
1149
|
-
await storage
|
|
1264
|
+
await commitWrites(storage, [
|
|
1150
1265
|
{ type: "task", value: { ...pendingTask(firstTaskId, rootId), kind: first } },
|
|
1151
1266
|
{ type: "task", value: { ...pendingTask(secondTaskId, rootId), kind: second } },
|
|
1152
1267
|
{
|
|
@@ -1211,18 +1326,188 @@ export function createStorageConformance(options) {
|
|
|
1211
1326
|
createCase(options, "keeps one global record ID namespace and rejects exhausted ID minting", async (storage) => {
|
|
1212
1327
|
const rootId = await createRoot(storage);
|
|
1213
1328
|
const explicitEntryId = idFromNumber(100);
|
|
1214
|
-
await storage
|
|
1329
|
+
await commitWrites(storage, [{ type: "entry", value: entry(explicitEntryId, rootId) }], context);
|
|
1215
1330
|
expect(await storage.mintId()).toBe(101);
|
|
1216
|
-
await expect(storage
|
|
1217
|
-
await storage
|
|
1331
|
+
await expect(commitWrites(storage, [{ type: "task", value: pendingTask(idFromNumber(explicitEntryId), rootId) }], context)).rejects.toThrow(`ID ${explicitEntryId} already belongs to entry`);
|
|
1332
|
+
await commitWrites(storage, [{ type: "entry", value: entry(idFromNumber(Number.MAX_SAFE_INTEGER), rootId, "last-id") }], context);
|
|
1218
1333
|
await expect(storage.mintId()).rejects.toThrow("ID space is exhausted");
|
|
1219
1334
|
await expect(storage.mintId()).rejects.toThrow("ID space is exhausted");
|
|
1220
1335
|
}),
|
|
1336
|
+
createCase(options, "commits batches in order, each with its seq, checked against the earlier ones", async (storage) => {
|
|
1337
|
+
const rootId = await createRoot(storage);
|
|
1338
|
+
const firstId = await storage.mintId();
|
|
1339
|
+
const lastId = await storage.mintId();
|
|
1340
|
+
const documentId = await storage.mintId();
|
|
1341
|
+
const receipts = [];
|
|
1342
|
+
let read;
|
|
1343
|
+
const result = await storage.commit([
|
|
1344
|
+
[
|
|
1345
|
+
{ type: "entry", value: entry(firstId, rootId) },
|
|
1346
|
+
{
|
|
1347
|
+
type: "document.create",
|
|
1348
|
+
record: {
|
|
1349
|
+
id: documentId,
|
|
1350
|
+
kind: "batch.doc",
|
|
1351
|
+
scope: { kind: "conversation", conversationId: rootId },
|
|
1352
|
+
history: "latest",
|
|
1353
|
+
fork: "initial",
|
|
1354
|
+
},
|
|
1355
|
+
content: { kind: "base", version: 1, value: { count: 1 } },
|
|
1356
|
+
},
|
|
1357
|
+
],
|
|
1358
|
+
// Changes the document the batch before created.
|
|
1359
|
+
[
|
|
1360
|
+
{
|
|
1361
|
+
type: "document.change",
|
|
1362
|
+
id: documentId,
|
|
1363
|
+
content: { kind: "delta", version: 1, ops: [["s", ["count"], 2]] },
|
|
1364
|
+
},
|
|
1365
|
+
],
|
|
1366
|
+
[{ type: "entry", value: entry(lastId, rootId) }],
|
|
1367
|
+
], (index, seq) => {
|
|
1368
|
+
receipts.push([index, seq]);
|
|
1369
|
+
// Receipts follow the last durable batch: a read started at the first observes it.
|
|
1370
|
+
read ??= storage.entry(lastId, context);
|
|
1371
|
+
}, context);
|
|
1372
|
+
expect(result).toEqual({ stored: 3 });
|
|
1373
|
+
expect(receipts.map(([index]) => index)).toEqual([0, 1, 2]);
|
|
1374
|
+
const [[, first], [, second], [, last]] = receipts;
|
|
1375
|
+
expect(second).toBeGreaterThan(first);
|
|
1376
|
+
expect(last).toBeGreaterThan(second);
|
|
1377
|
+
expect((await read)?.commitSeq).toBe(last);
|
|
1378
|
+
expect((await storage.entry(firstId, context))?.commitSeq).toBe(first);
|
|
1379
|
+
expect((await storage.document(documentId, "current", context))?.value).toEqual({ count: 2 });
|
|
1380
|
+
const next = await commitWrites(storage, [{ type: "entry", value: entry(await storage.mintId(), rootId) }], context);
|
|
1381
|
+
expect(next).toBeGreaterThan(last);
|
|
1382
|
+
}),
|
|
1383
|
+
createCase(options, "stores the batches before the first one refused, and none after it", async (storage) => {
|
|
1384
|
+
const rootId = await createRoot(storage);
|
|
1385
|
+
const firstId = await storage.mintId();
|
|
1386
|
+
const afterId = await storage.mintId();
|
|
1387
|
+
const receipts = [];
|
|
1388
|
+
const result = await storage.commit([
|
|
1389
|
+
[{ type: "entry", value: entry(firstId, rootId) }],
|
|
1390
|
+
// Refused against the state the first batch left: the ID is taken now.
|
|
1391
|
+
[
|
|
1392
|
+
{ type: "task", value: pendingTask(await storage.mintId(), rootId) },
|
|
1393
|
+
{ type: "entry", value: entry(firstId, rootId) },
|
|
1394
|
+
],
|
|
1395
|
+
[{ type: "entry", value: entry(afterId, rootId) }],
|
|
1396
|
+
], (index) => receipts.push(index), context);
|
|
1397
|
+
expect(result.stored).toBe(1);
|
|
1398
|
+
expect(String(result.refused)).toBe(`Error: ID ${firstId} already belongs to entry`);
|
|
1399
|
+
expect(receipts).toEqual([0]);
|
|
1400
|
+
expect(await storage.entry(firstId, context)).toBeDefined();
|
|
1401
|
+
expect(await storage.entry(afterId, context)).toBeUndefined();
|
|
1402
|
+
expect((await storage.scanTasks({}, 10, undefined, context)).items.map((task) => task.id)).toEqual([]);
|
|
1403
|
+
// A refused first batch stores nothing; the storage goes on.
|
|
1404
|
+
const refused = await storage.commit([[{ type: "conversation", value: { id: rootId } }]], (index) => receipts.push(index), context);
|
|
1405
|
+
expect(refused.stored).toBe(0);
|
|
1406
|
+
expect(String(refused.refused)).toBe(`Error: ID ${rootId} already belongs to conversation`);
|
|
1407
|
+
expect(receipts).toEqual([0]);
|
|
1408
|
+
await commitWrites(storage, [{ type: "entry", value: entry(afterId, rootId) }], context);
|
|
1409
|
+
expect(await storage.entry(afterId, context)).toBeDefined();
|
|
1410
|
+
}),
|
|
1411
|
+
createCase(options, "refuses a later batch its own malformed actions fail, keeping the batches before it", async (storage) => {
|
|
1412
|
+
const rootId = await createRoot(storage);
|
|
1413
|
+
const documentId = await storage.mintId();
|
|
1414
|
+
await commitWrites(storage, [
|
|
1415
|
+
{
|
|
1416
|
+
type: "document.create",
|
|
1417
|
+
record: { id: documentId, kind: "staged", scope: { kind: "session" } },
|
|
1418
|
+
content: { kind: "base", version: 1, value: { n: 1 } },
|
|
1419
|
+
},
|
|
1420
|
+
], context);
|
|
1421
|
+
const firstId = await storage.mintId();
|
|
1422
|
+
const secondId = await storage.mintId();
|
|
1423
|
+
const afterId = await storage.mintId();
|
|
1424
|
+
const receipts = [];
|
|
1425
|
+
const result = await storage.commit([
|
|
1426
|
+
[{ type: "entry", value: entry(firstId, rootId) }],
|
|
1427
|
+
// Malformed in itself, whatever is stored: one document retired twice.
|
|
1428
|
+
[
|
|
1429
|
+
{ type: "entry", value: entry(secondId, rootId) },
|
|
1430
|
+
{ type: "document.retire", id: documentId },
|
|
1431
|
+
{ type: "document.retire", id: documentId },
|
|
1432
|
+
],
|
|
1433
|
+
[{ type: "entry", value: entry(afterId, rootId) }],
|
|
1434
|
+
], (index) => receipts.push(index), context);
|
|
1435
|
+
expect(result.stored).toBe(1);
|
|
1436
|
+
expect(result.refused).toBeDefined();
|
|
1437
|
+
expect(receipts).toEqual([0]);
|
|
1438
|
+
expect(await storage.entry(firstId, context)).toBeDefined();
|
|
1439
|
+
expect(await storage.entry(secondId, context)).toBeUndefined();
|
|
1440
|
+
expect(await storage.entry(afterId, context)).toBeUndefined();
|
|
1441
|
+
expect((await storage.document(documentId, "current", context))?.value).toEqual({ n: 1 });
|
|
1442
|
+
}),
|
|
1443
|
+
createCase(options, "resolves a batched copy against the state the earlier batches left", async (storage) => {
|
|
1444
|
+
const rootId = await createRoot(storage);
|
|
1445
|
+
const childId = await storage.mintId();
|
|
1446
|
+
const sourceId = await storage.mintId();
|
|
1447
|
+
const copyId = await storage.mintId();
|
|
1448
|
+
const record = (id, conversationId) => ({
|
|
1449
|
+
id,
|
|
1450
|
+
kind: "batch.copy",
|
|
1451
|
+
scope: { kind: "conversation", conversationId },
|
|
1452
|
+
history: "rewindable",
|
|
1453
|
+
fork: "asOf",
|
|
1454
|
+
});
|
|
1455
|
+
const seqs = [];
|
|
1456
|
+
const result = await storage.commit([
|
|
1457
|
+
[
|
|
1458
|
+
{
|
|
1459
|
+
type: "document.create",
|
|
1460
|
+
record: record(sourceId, rootId),
|
|
1461
|
+
content: { kind: "base", version: 1, value: { count: 1 } },
|
|
1462
|
+
},
|
|
1463
|
+
],
|
|
1464
|
+
[
|
|
1465
|
+
{
|
|
1466
|
+
type: "document.change",
|
|
1467
|
+
id: sourceId,
|
|
1468
|
+
content: { kind: "delta", version: 1, ops: [["s", ["count"], 2]] },
|
|
1469
|
+
},
|
|
1470
|
+
],
|
|
1471
|
+
[
|
|
1472
|
+
{ type: "conversation", value: { id: childId } },
|
|
1473
|
+
{
|
|
1474
|
+
type: "document.copy",
|
|
1475
|
+
record: record(copyId, childId),
|
|
1476
|
+
source: { id: sourceId, at: "current" },
|
|
1477
|
+
},
|
|
1478
|
+
],
|
|
1479
|
+
], (_index, seq) => seqs.push(seq), context);
|
|
1480
|
+
expect(result).toEqual({ stored: 3 });
|
|
1481
|
+
expect((await storage.document(copyId, "current", context))?.value).toEqual({ count: 2 });
|
|
1482
|
+
expect((await storage.document(sourceId, seqs[0], context))?.value).toEqual({ count: 1 });
|
|
1483
|
+
// A copy whose source the same batch changes is refused before the batch writes anything.
|
|
1484
|
+
const laterId = await storage.mintId();
|
|
1485
|
+
const laterCopyId = await storage.mintId();
|
|
1486
|
+
const refused = await storage.commit([
|
|
1487
|
+
[
|
|
1488
|
+
{ type: "conversation", value: { id: laterId } },
|
|
1489
|
+
{
|
|
1490
|
+
type: "document.copy",
|
|
1491
|
+
record: record(laterCopyId, laterId),
|
|
1492
|
+
source: { id: sourceId, at: "current" },
|
|
1493
|
+
},
|
|
1494
|
+
{
|
|
1495
|
+
type: "document.change",
|
|
1496
|
+
id: sourceId,
|
|
1497
|
+
content: { kind: "delta", version: 1, ops: [["s", ["count"], 3]] },
|
|
1498
|
+
},
|
|
1499
|
+
],
|
|
1500
|
+
], () => { }, context);
|
|
1501
|
+
expect(refused.stored).toBe(0);
|
|
1502
|
+
expect(refused.refused).toBeDefined();
|
|
1503
|
+
expect(await storage.conversation(laterId, context)).toBeUndefined();
|
|
1504
|
+
expect((await storage.document(sourceId, "current", context))?.value).toEqual({ count: 2 });
|
|
1505
|
+
}),
|
|
1221
1506
|
createCase(options, "rejects every operation after close", async (storage) => {
|
|
1222
1507
|
await createRoot(storage);
|
|
1223
1508
|
await storage.close(context);
|
|
1224
1509
|
await expect(storage.conversation(ROOT_CONVERSATION_ID, context)).rejects.toThrow("closed");
|
|
1225
|
-
await expect(storage
|
|
1510
|
+
await expect(commitWrites(storage, [{ type: "conversation", value: { id: ROOT_CONVERSATION_ID } }], context)).rejects.toThrow("closed");
|
|
1226
1511
|
await expect(storage.mintId()).rejects.toThrow("closed");
|
|
1227
1512
|
}),
|
|
1228
1513
|
];
|