@earendil-works/pi-durable 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +227 -0
- package/README.md +287 -82
- package/dist/codemode/codemode.d.ts +4 -4
- package/dist/codemode/codemode.d.ts.map +1 -1
- package/dist/codemode/codemode.js +21 -16
- package/dist/codemode/codemode.js.map +1 -1
- package/dist/codemode/models.d.ts.map +1 -1
- package/dist/codemode/models.js +6 -4
- package/dist/codemode/models.js.map +1 -1
- package/dist/documents.d.ts +1 -1
- package/dist/documents.d.ts.map +1 -1
- package/dist/documents.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 +12 -4
- 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 +13 -2
- package/dist/harness/agent.d.ts.map +1 -1
- package/dist/harness/agent.js +48 -6
- package/dist/harness/agent.js.map +1 -1
- package/dist/harness/compaction.d.ts +3 -1
- package/dist/harness/compaction.d.ts.map +1 -1
- package/dist/harness/compaction.js +38 -6
- 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 +497 -108
- package/dist/harness/generation.js.map +1 -1
- package/dist/harness/harness.d.ts +7 -2
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +147 -57
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/inbox.d.ts +7 -3
- package/dist/harness/inbox.d.ts.map +1 -1
- package/dist/harness/inbox.js +7 -4
- 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/prompt.d.ts +1 -1
- package/dist/harness/prompt.d.ts.map +1 -1
- package/dist/harness/prompt.js +1 -1
- package/dist/harness/prompt.js.map +1 -1
- 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 +86 -14
- package/dist/harness/scheduler.d.ts.map +1 -1
- package/dist/harness/scheduler.js +1459 -369
- package/dist/harness/scheduler.js.map +1 -1
- package/dist/harness/submissions.d.ts +23 -11
- package/dist/harness/submissions.d.ts.map +1 -1
- package/dist/harness/submissions.js +104 -36
- package/dist/harness/submissions.js.map +1 -1
- package/dist/harness/task-graph.d.ts +28 -8
- package/dist/harness/task-graph.d.ts.map +1 -1
- package/dist/harness/task-graph.js +85 -36
- package/dist/harness/task-graph.js.map +1 -1
- package/dist/harness/telemetry.d.ts +307 -0
- package/dist/harness/telemetry.d.ts.map +1 -0
- package/dist/harness/telemetry.js +136 -0
- package/dist/harness/telemetry.js.map +1 -0
- package/dist/harness/tool.d.ts +2 -0
- package/dist/harness/tool.d.ts.map +1 -1
- package/dist/harness/tool.js +223 -61
- 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 +302 -48
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/harness/util.d.ts +7 -1
- package/dist/harness/util.d.ts.map +1 -1
- package/dist/harness/util.js +13 -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 +13 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- 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 +181 -29
- package/dist/session/session.d.ts.map +1 -1
- package/dist/session/session.js +1073 -183
- package/dist/session/session.js.map +1 -1
- package/dist/session/transaction.d.ts +86 -4
- package/dist/session/transaction.d.ts.map +1 -1
- package/dist/session/transaction.js +250 -38
- 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 +68 -0
- package/dist/storage/indexeddb.d.ts.map +1 -0
- package/dist/storage/indexeddb.js +881 -0
- package/dist/storage/indexeddb.js.map +1 -0
- package/dist/storage/jsonl/storage.d.ts +30 -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 +29 -3
- package/dist/storage/memory.js.map +1 -1
- package/dist/storage/sqlite/cloudflare.d.ts +2 -51
- package/dist/storage/sqlite/cloudflare.d.ts.map +1 -1
- package/dist/storage/sqlite/cloudflare.js +2 -133
- package/dist/storage/sqlite/cloudflare.js.map +1 -1
- package/dist/storage/sqlite/database.d.ts +1 -36
- 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 +1 -1
- package/dist/storage/sqlite/index.d.ts.map +1 -1
- package/dist/storage/sqlite/index.js.map +1 -1
- package/dist/storage/sqlite/migrations.d.ts +2 -5
- package/dist/storage/sqlite/migrations.d.ts.map +1 -1
- package/dist/storage/sqlite/migrations.js +28 -28
- package/dist/storage/sqlite/migrations.js.map +1 -1
- package/dist/storage/sqlite/node.d.ts +2 -37
- package/dist/storage/sqlite/node.d.ts.map +1 -1
- package/dist/storage/sqlite/node.js +3 -174
- package/dist/storage/sqlite/node.js.map +1 -1
- package/dist/storage/sqlite/schema.d.ts +1 -3
- package/dist/storage/sqlite/schema.d.ts.map +1 -1
- package/dist/storage/sqlite/schema.js +2 -9
- package/dist/storage/sqlite/schema.js.map +1 -1
- package/dist/storage/sqlite/storage.d.ts +15 -3
- 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 +369 -67
- package/dist/testing/storage-conformance.js.map +1 -1
- package/dist/timers.d.ts +21 -0
- package/dist/timers.d.ts.map +1 -0
- package/dist/timers.js +72 -0
- package/dist/timers.js.map +1 -0
- 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 +331 -64
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +27 -10
- 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,14 +524,31 @@ 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
|
}),
|
|
531
|
+
createCase(options, "stores and scans sleeping tasks by status", async (storage) => {
|
|
532
|
+
const rootId = await createRoot(storage);
|
|
533
|
+
const sleepingId = await storage.mintId();
|
|
534
|
+
const sleeping = {
|
|
535
|
+
...pendingTask(sleepingId, rootId),
|
|
536
|
+
state: { status: "sleeping", checkpoint: { phase: "next" }, until: 61_000 },
|
|
537
|
+
memos: { kept: true },
|
|
538
|
+
};
|
|
539
|
+
await commitWrites(storage, [{ type: "task", value: sleeping }], context);
|
|
540
|
+
expect(await storage.task(sleepingId, context)).toEqual(sleeping);
|
|
541
|
+
const scan = async (status) => (await storage.scanTasks({ status }, 10, undefined, context)).items;
|
|
542
|
+
expect(await scan("sleeping")).toEqual([sleeping]);
|
|
543
|
+
const running = { ...sleeping, state: { status: "running", checkpoint: { phase: "next" } } };
|
|
544
|
+
await commitWrites(storage, [{ type: "task", value: running }], context);
|
|
545
|
+
expect(await scan("sleeping")).toEqual([]);
|
|
546
|
+
expect(await scan("running")).toEqual([running]);
|
|
547
|
+
}),
|
|
473
548
|
createCase(options, "indexes request IDs per conversation and replaces complete submission records", async (storage) => {
|
|
474
549
|
const rootId = await createRoot(storage);
|
|
475
550
|
const secondConversationId = await storage.mintId();
|
|
476
|
-
await storage
|
|
551
|
+
await commitWrites(storage, [{ type: "conversation", value: { id: secondConversationId } }], context);
|
|
477
552
|
const firstId = await storage.mintId();
|
|
478
553
|
const secondId = await storage.mintId();
|
|
479
554
|
const otherConversationId = await storage.mintId();
|
|
@@ -498,7 +573,7 @@ export function createStorageConformance(options) {
|
|
|
498
573
|
type: "input",
|
|
499
574
|
status: "queued",
|
|
500
575
|
};
|
|
501
|
-
await storage
|
|
576
|
+
await commitWrites(storage, [
|
|
502
577
|
{ type: "submission", value: first },
|
|
503
578
|
{ type: "submission", value: second },
|
|
504
579
|
{ type: "submission", value: otherConversation },
|
|
@@ -510,7 +585,7 @@ export function createStorageConformance(options) {
|
|
|
510
585
|
status: "placed",
|
|
511
586
|
entry: await storage.mintId(),
|
|
512
587
|
};
|
|
513
|
-
await storage
|
|
588
|
+
await commitWrites(storage, [{ type: "submission", value: placedSecond }], context);
|
|
514
589
|
expect(await storage.submission(secondId, context)).toEqual(placedSecond);
|
|
515
590
|
expect(await storage.submissionByRequest(rootId, "other", context)).toEqual(placedSecond);
|
|
516
591
|
const ids = async (query) => {
|
|
@@ -554,7 +629,7 @@ export function createStorageConformance(options) {
|
|
|
554
629
|
type: "write",
|
|
555
630
|
status: "queued",
|
|
556
631
|
};
|
|
557
|
-
await storage
|
|
632
|
+
await commitWrites(storage, [
|
|
558
633
|
{ type: "submission", value: queuedDone },
|
|
559
634
|
{ type: "submission", value: queuedFailed },
|
|
560
635
|
], context);
|
|
@@ -569,7 +644,7 @@ export function createStorageConformance(options) {
|
|
|
569
644
|
reason: "closed",
|
|
570
645
|
detail: { retryable: false },
|
|
571
646
|
};
|
|
572
|
-
await storage
|
|
647
|
+
await commitWrites(storage, [
|
|
573
648
|
{ type: "submission", value: done },
|
|
574
649
|
{ type: "submission", value: unanswered },
|
|
575
650
|
], context);
|
|
@@ -578,6 +653,44 @@ export function createStorageConformance(options) {
|
|
|
578
653
|
expect(await storage.submission(failedId, context)).toEqual(unanswered);
|
|
579
654
|
expect(await storage.submissionByRequest(rootId, "passive-failed", context)).toEqual(unanswered);
|
|
580
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
|
+
}),
|
|
581
694
|
createCase(options, "reconstructs rewindable documents and preserves half-open incarnations", async (storage) => {
|
|
582
695
|
const rootId = await createRoot(storage);
|
|
583
696
|
const firstId = await storage.mintId();
|
|
@@ -589,7 +702,7 @@ export function createStorageConformance(options) {
|
|
|
589
702
|
fork: "asOf",
|
|
590
703
|
};
|
|
591
704
|
const initial = { items: ["a"], nested: { count: 1 } };
|
|
592
|
-
const createdAt = await storage
|
|
705
|
+
const createdAt = await commitWrites(storage, [
|
|
593
706
|
{
|
|
594
707
|
type: "document.create",
|
|
595
708
|
record: firstRecord,
|
|
@@ -601,7 +714,7 @@ export function createStorageConformance(options) {
|
|
|
601
714
|
["p", ["items"], 1, 0, appended],
|
|
602
715
|
["s", ["nested", "count"], 2],
|
|
603
716
|
];
|
|
604
|
-
const changedAt = await storage
|
|
717
|
+
const changedAt = await commitWrites(storage, [{ type: "document.change", id: firstId, content: { kind: "delta", version: 1, ops } }], context);
|
|
605
718
|
initial.items.push("caller mutation");
|
|
606
719
|
appended.push("caller mutation");
|
|
607
720
|
expect(await storage.document(firstId, createdAt, context)).toMatchObject({
|
|
@@ -617,14 +730,14 @@ export function createStorageConformance(options) {
|
|
|
617
730
|
items: ["a", "b"],
|
|
618
731
|
nested: { count: 2 },
|
|
619
732
|
});
|
|
620
|
-
const checkpointAt = await storage
|
|
733
|
+
const checkpointAt = await commitWrites(storage, [
|
|
621
734
|
{
|
|
622
735
|
type: "document.change",
|
|
623
736
|
id: firstId,
|
|
624
737
|
content: { kind: "base", version: 2, value: { items: ["checkpoint"], nested: { count: 3 } } },
|
|
625
738
|
},
|
|
626
739
|
], context);
|
|
627
|
-
const replacedAt = await storage
|
|
740
|
+
const replacedAt = await commitWrites(storage, [
|
|
628
741
|
{
|
|
629
742
|
type: "document.change",
|
|
630
743
|
id: firstId,
|
|
@@ -650,7 +763,7 @@ export function createStorageConformance(options) {
|
|
|
650
763
|
});
|
|
651
764
|
expect((await storage.document(firstId, "current", context))?.deltasSinceBase).toBe(1);
|
|
652
765
|
const secondId = await storage.mintId();
|
|
653
|
-
const retiredAt = await storage
|
|
766
|
+
const retiredAt = await commitWrites(storage, [
|
|
654
767
|
{
|
|
655
768
|
type: "document.create",
|
|
656
769
|
record: { ...firstRecord, id: secondId },
|
|
@@ -688,14 +801,14 @@ export function createStorageConformance(options) {
|
|
|
688
801
|
revision: 0,
|
|
689
802
|
rows: Array.from({ length: 512 }, (_, value) => ({ value, stable: `row-${value}` })),
|
|
690
803
|
};
|
|
691
|
-
const createdAt = await storage
|
|
804
|
+
const createdAt = await commitWrites(storage, [{ type: "document.create", record, content: { kind: "base", version: 1, value: initial } }], context);
|
|
692
805
|
const beforeReplacement = structuredClone(initial);
|
|
693
806
|
let beforeReplacementAt = createdAt;
|
|
694
807
|
for (let revision = 1; revision <= 24; revision++) {
|
|
695
808
|
const index = (revision * 17) % beforeReplacement.rows.length;
|
|
696
809
|
beforeReplacement.rows[index].value = -revision;
|
|
697
810
|
beforeReplacement.revision = revision;
|
|
698
|
-
beforeReplacementAt = await storage
|
|
811
|
+
beforeReplacementAt = await commitWrites(storage, [
|
|
699
812
|
{
|
|
700
813
|
type: "document.change",
|
|
701
814
|
id,
|
|
@@ -715,7 +828,7 @@ export function createStorageConformance(options) {
|
|
|
715
828
|
rows: Array.from({ length: 512 }, (_, value) => ({ value: 10_000 + value, stable: `new-${value}` })),
|
|
716
829
|
};
|
|
717
830
|
const replacementSnapshot = structuredClone(replacement);
|
|
718
|
-
const replacementAt = await storage
|
|
831
|
+
const replacementAt = await commitWrites(storage, [
|
|
719
832
|
{
|
|
720
833
|
type: "document.change",
|
|
721
834
|
id,
|
|
@@ -728,7 +841,7 @@ export function createStorageConformance(options) {
|
|
|
728
841
|
const index = (revision * 19) % current.rows.length;
|
|
729
842
|
current.rows[index].value = -revision;
|
|
730
843
|
current.revision = revision;
|
|
731
|
-
await storage
|
|
844
|
+
await commitWrites(storage, [
|
|
732
845
|
{
|
|
733
846
|
type: "document.change",
|
|
734
847
|
id,
|
|
@@ -755,7 +868,7 @@ export function createStorageConformance(options) {
|
|
|
755
868
|
const rootId = await createRoot(storage);
|
|
756
869
|
const childId = await storage.mintId();
|
|
757
870
|
const secondChildId = await storage.mintId();
|
|
758
|
-
await storage
|
|
871
|
+
await commitWrites(storage, [
|
|
759
872
|
{ type: "conversation", value: { id: childId } },
|
|
760
873
|
{ type: "conversation", value: { id: secondChildId } },
|
|
761
874
|
], context);
|
|
@@ -767,14 +880,14 @@ export function createStorageConformance(options) {
|
|
|
767
880
|
history: "rewindable",
|
|
768
881
|
fork: "asOf",
|
|
769
882
|
};
|
|
770
|
-
const createdAt = await storage
|
|
883
|
+
const createdAt = await commitWrites(storage, [
|
|
771
884
|
{
|
|
772
885
|
type: "document.create",
|
|
773
886
|
record: sourceRecord,
|
|
774
887
|
content: { kind: "base", version: 2, value: { count: 1, rows: [{ value: "base" }] } },
|
|
775
888
|
},
|
|
776
889
|
], context);
|
|
777
|
-
await storage
|
|
890
|
+
await commitWrites(storage, [
|
|
778
891
|
{
|
|
779
892
|
type: "document.change",
|
|
780
893
|
id: sourceId,
|
|
@@ -798,7 +911,7 @@ export function createStorageConformance(options) {
|
|
|
798
911
|
history: "rewindable",
|
|
799
912
|
fork: "asOf",
|
|
800
913
|
});
|
|
801
|
-
await storage
|
|
914
|
+
await commitWrites(storage, [
|
|
802
915
|
{
|
|
803
916
|
type: "document.copy",
|
|
804
917
|
record: childRecord(historicalCopyId, childId),
|
|
@@ -825,7 +938,7 @@ export function createStorageConformance(options) {
|
|
|
825
938
|
value: { count: 2, rows: [{ value: "base" }, { value: "current" }] },
|
|
826
939
|
});
|
|
827
940
|
expect(await storage.document(retiredCopyId, "current", context)).toBeUndefined();
|
|
828
|
-
await storage
|
|
941
|
+
await commitWrites(storage, [
|
|
829
942
|
{
|
|
830
943
|
type: "document.change",
|
|
831
944
|
id: sourceId,
|
|
@@ -846,14 +959,14 @@ export function createStorageConformance(options) {
|
|
|
846
959
|
history: "latest",
|
|
847
960
|
fork: "current",
|
|
848
961
|
};
|
|
849
|
-
await storage
|
|
962
|
+
await commitWrites(storage, [
|
|
850
963
|
{
|
|
851
964
|
type: "document.create",
|
|
852
965
|
record: latestSource,
|
|
853
966
|
content: { kind: "base", version: 4, value: { retained: "copy" } },
|
|
854
967
|
},
|
|
855
968
|
], context);
|
|
856
|
-
await storage
|
|
969
|
+
await commitWrites(storage, [
|
|
857
970
|
{
|
|
858
971
|
type: "document.copy",
|
|
859
972
|
record: {
|
|
@@ -864,7 +977,7 @@ export function createStorageConformance(options) {
|
|
|
864
977
|
source: { id: latestSourceId, at: "current" },
|
|
865
978
|
},
|
|
866
979
|
], context);
|
|
867
|
-
await storage
|
|
980
|
+
await commitWrites(storage, [
|
|
868
981
|
{
|
|
869
982
|
type: "document.change",
|
|
870
983
|
id: latestSourceId,
|
|
@@ -879,7 +992,7 @@ export function createStorageConformance(options) {
|
|
|
879
992
|
const conflictId = await storage.mintId();
|
|
880
993
|
let conflictError;
|
|
881
994
|
try {
|
|
882
|
-
await storage
|
|
995
|
+
await commitWrites(storage, [
|
|
883
996
|
{
|
|
884
997
|
type: "document.copy",
|
|
885
998
|
record: childRecord(conflictId, childId),
|
|
@@ -901,7 +1014,7 @@ export function createStorageConformance(options) {
|
|
|
901
1014
|
const mismatchId = await storage.mintId();
|
|
902
1015
|
let mismatchError;
|
|
903
1016
|
try {
|
|
904
|
-
await storage
|
|
1017
|
+
await commitWrites(storage, [
|
|
905
1018
|
{
|
|
906
1019
|
type: "document.copy",
|
|
907
1020
|
record: { ...childRecord(mismatchId, childId), kind: "copy.mismatch" },
|
|
@@ -923,14 +1036,14 @@ export function createStorageConformance(options) {
|
|
|
923
1036
|
kind: "session.settings",
|
|
924
1037
|
scope: { kind: "session" },
|
|
925
1038
|
};
|
|
926
|
-
await storage
|
|
927
|
-
await storage
|
|
928
|
-
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);
|
|
929
1042
|
expect(await storage.document(id, "current", context)).toMatchObject({ version: 2, value: { count: 3 } });
|
|
930
1043
|
await expect(storage.document(id, migratedAt, context)).rejects.toThrow("does not retain historical content");
|
|
931
|
-
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");
|
|
932
1045
|
expect((await storage.document(id, "current", context))?.value).toEqual({ count: 3 });
|
|
933
|
-
await storage
|
|
1046
|
+
await commitWrites(storage, [{ type: "document.retire", id }], context);
|
|
934
1047
|
expect(await storage.document(id, "current", context)).toBeUndefined();
|
|
935
1048
|
}),
|
|
936
1049
|
createCase(options, "indexes logical addresses and exact-scope scans independently", async (storage) => {
|
|
@@ -942,7 +1055,7 @@ export function createStorageConformance(options) {
|
|
|
942
1055
|
const taskSingletonId = await storage.mintId();
|
|
943
1056
|
const taskFamilyId = await storage.mintId();
|
|
944
1057
|
const taskOtherKindId = await storage.mintId();
|
|
945
|
-
const createdAt = await storage
|
|
1058
|
+
const createdAt = await commitWrites(storage, [
|
|
946
1059
|
{ type: "task", value: pendingTask(taskId, rootId) },
|
|
947
1060
|
{
|
|
948
1061
|
type: "document.create",
|
|
@@ -1026,8 +1139,8 @@ export function createStorageConformance(options) {
|
|
|
1026
1139
|
kind: "singleton",
|
|
1027
1140
|
scope: { kind: "session" },
|
|
1028
1141
|
};
|
|
1029
|
-
await storage
|
|
1030
|
-
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, [
|
|
1031
1144
|
{
|
|
1032
1145
|
type: "document.create",
|
|
1033
1146
|
record: { ...record, id: secondId },
|
|
@@ -1038,7 +1151,7 @@ export function createStorageConformance(options) {
|
|
|
1038
1151
|
expect((await storage.document(firstId, "current", context))?.value).toEqual({ value: 1 });
|
|
1039
1152
|
expect(await storage.document(secondId, "current", context)).toBeUndefined();
|
|
1040
1153
|
const emptyId = await storage.mintId();
|
|
1041
|
-
const emptyAt = await storage
|
|
1154
|
+
const emptyAt = await commitWrites(storage, [
|
|
1042
1155
|
{
|
|
1043
1156
|
type: "document.create",
|
|
1044
1157
|
record: {
|
|
@@ -1061,6 +1174,25 @@ export function createStorageConformance(options) {
|
|
|
1061
1174
|
key: "empty",
|
|
1062
1175
|
}, emptyAt, context)).toBeUndefined();
|
|
1063
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
|
+
}),
|
|
1064
1196
|
createCase(options, "rolls back record tables and secondary indexes when a document command fails", async (storage) => {
|
|
1065
1197
|
const rootId = await createRoot(storage);
|
|
1066
1198
|
const taskId = await storage.mintId();
|
|
@@ -1079,14 +1211,14 @@ export function createStorageConformance(options) {
|
|
|
1079
1211
|
kind: "atomic",
|
|
1080
1212
|
scope: { kind: "session" },
|
|
1081
1213
|
};
|
|
1082
|
-
const baselineSeq = await storage
|
|
1214
|
+
const baselineSeq = await commitWrites(storage, [
|
|
1083
1215
|
{ type: "task", value: task },
|
|
1084
1216
|
{ type: "submission", value: submission },
|
|
1085
1217
|
{ type: "document.create", record, content: { kind: "base", version: 1, value: { count: 1 } } },
|
|
1086
1218
|
], context);
|
|
1087
1219
|
const entryId = await storage.mintId();
|
|
1088
1220
|
const conflictingDocumentId = await storage.mintId();
|
|
1089
|
-
await expect(storage
|
|
1221
|
+
await expect(commitWrites(storage, [
|
|
1090
1222
|
{
|
|
1091
1223
|
type: "task",
|
|
1092
1224
|
value: { ...task, state: { status: "running", checkpoint: { phase: "effect" } } },
|
|
@@ -1108,7 +1240,7 @@ export function createStorageConformance(options) {
|
|
|
1108
1240
|
expect(await storage.entry(entryId, context)).toBeUndefined();
|
|
1109
1241
|
expect(await storage.document(conflictingDocumentId, "current", context)).toBeUndefined();
|
|
1110
1242
|
expect((await storage.findDocument({ kind: record.kind, scope: record.scope }, "current", context))?.id).toBe(documentId);
|
|
1111
|
-
const afterRollbackSeq = await storage
|
|
1243
|
+
const afterRollbackSeq = await commitWrites(storage, [
|
|
1112
1244
|
{
|
|
1113
1245
|
type: "document.change",
|
|
1114
1246
|
id: documentId,
|
|
@@ -1129,7 +1261,7 @@ export function createStorageConformance(options) {
|
|
|
1129
1261
|
const secondKindDocumentId = await storage.mintId();
|
|
1130
1262
|
const firstKeyDocumentId = await storage.mintId();
|
|
1131
1263
|
const secondKeyDocumentId = await storage.mintId();
|
|
1132
|
-
await storage
|
|
1264
|
+
await commitWrites(storage, [
|
|
1133
1265
|
{ type: "task", value: { ...pendingTask(firstTaskId, rootId), kind: first } },
|
|
1134
1266
|
{ type: "task", value: { ...pendingTask(secondTaskId, rootId), kind: second } },
|
|
1135
1267
|
{
|
|
@@ -1194,18 +1326,188 @@ export function createStorageConformance(options) {
|
|
|
1194
1326
|
createCase(options, "keeps one global record ID namespace and rejects exhausted ID minting", async (storage) => {
|
|
1195
1327
|
const rootId = await createRoot(storage);
|
|
1196
1328
|
const explicitEntryId = idFromNumber(100);
|
|
1197
|
-
await storage
|
|
1329
|
+
await commitWrites(storage, [{ type: "entry", value: entry(explicitEntryId, rootId) }], context);
|
|
1198
1330
|
expect(await storage.mintId()).toBe(101);
|
|
1199
|
-
await expect(storage
|
|
1200
|
-
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);
|
|
1201
1333
|
await expect(storage.mintId()).rejects.toThrow("ID space is exhausted");
|
|
1202
1334
|
await expect(storage.mintId()).rejects.toThrow("ID space is exhausted");
|
|
1203
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
|
+
}),
|
|
1204
1506
|
createCase(options, "rejects every operation after close", async (storage) => {
|
|
1205
1507
|
await createRoot(storage);
|
|
1206
1508
|
await storage.close(context);
|
|
1207
1509
|
await expect(storage.conversation(ROOT_CONVERSATION_ID, context)).rejects.toThrow("closed");
|
|
1208
|
-
await expect(storage
|
|
1510
|
+
await expect(commitWrites(storage, [{ type: "conversation", value: { id: ROOT_CONVERSATION_ID } }], context)).rejects.toThrow("closed");
|
|
1209
1511
|
await expect(storage.mintId()).rejects.toThrow("closed");
|
|
1210
1512
|
}),
|
|
1211
1513
|
];
|