@code-yeongyu/senpi-agent-core 2026.10.27-1 → 2026.10.30-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-loop.js +1 -0
- package/dist/agent.js +3 -1
- package/dist/assistant-terminal-state.js +5 -2
- package/dist/conformance/index.d.ts +42 -0
- package/dist/conformance/index.js +23 -0
- package/dist/conformance/mapping.d.ts +24 -0
- package/dist/conformance/mapping.js +62 -0
- package/dist/conformance/registry.d.ts +15 -0
- package/dist/conformance/registry.js +75 -0
- package/dist/conformance/runner.d.ts +27 -0
- package/dist/conformance/runner.js +23 -0
- package/dist/conformance/suite-hash.d.ts +12 -0
- package/dist/conformance/suite-hash.js +20 -0
- package/dist/durable/store-admission.d.ts +7 -0
- package/dist/durable/store-admission.js +11 -0
- package/dist/durable/store-memory.d.ts +15 -0
- package/dist/durable/store-memory.js +140 -0
- package/dist/durable/store.d.ts +116 -0
- package/dist/durable/store.js +14 -0
- package/dist/durable/testing/kill-store.d.ts +13 -0
- package/dist/durable/testing/kill-store.js +77 -0
- package/dist/durable/testing/kill-wake.d.ts +12 -0
- package/dist/durable/testing/kill-wake.js +123 -0
- package/dist/durable/testing/manual-timers.d.ts +8 -0
- package/dist/durable/testing/manual-timers.js +31 -0
- package/dist/durable/testing/multi-host-cases.d.ts +6 -0
- package/dist/durable/testing/multi-host-cases.js +32 -0
- package/dist/durable/testing/store-cases.d.ts +21 -0
- package/dist/durable/testing/store-cases.js +168 -0
- package/dist/durable/testing/store-harness.d.ts +23 -0
- package/dist/durable/testing/store-harness.js +64 -0
- package/dist/durable/testing/wake-cases.d.ts +6 -0
- package/dist/durable/testing/wake-cases.js +100 -0
- package/dist/durable/testing/wake-harness.d.ts +64 -0
- package/dist/durable/testing/wake-harness.js +127 -0
- package/dist/durable/testing/wake-test-loop.d.ts +13 -0
- package/dist/durable/testing/wake-test-loop.js +40 -0
- package/dist/durable/wake-host-bun-timer.d.ts +31 -0
- package/dist/durable/wake-host-bun-timer.js +66 -0
- package/dist/durable/wake-host-store-alarm.d.ts +13 -0
- package/dist/durable/wake-host-store-alarm.js +20 -0
- package/dist/durable/wake-host.d.ts +68 -0
- package/dist/durable/wake-host.js +70 -0
- package/dist/harness/pico3/kinds/generation.js +3 -1
- package/dist/harness/utils/read-folders/index.d.ts +1 -1
- package/dist/harness/utils/read-folders/index.js +1 -1
- package/dist/harness/utils/read-folders/tree-sitter/engine.d.ts +1 -1
- package/dist/harness/utils/read-folders/tree-sitter/engine.js +38 -19
- package/dist/internal-events.d.ts +1 -0
- package/dist/internal-events.js +1 -0
- package/dist/testing/win32-sim.d.ts +76 -0
- package/dist/testing/win32-sim.js +197 -0
- package/dist/transport/in-process-carrier.d.ts +19 -0
- package/dist/transport/in-process-carrier.js +63 -0
- package/dist/transport/local-endpoint.d.ts +13 -0
- package/dist/transport/local-endpoint.js +20 -0
- package/dist/transport/reliable-transport.d.ts +40 -0
- package/dist/transport/reliable-transport.js +232 -0
- package/dist/transport/testing/fault-cases.d.ts +6 -0
- package/dist/transport/testing/fault-cases.js +30 -0
- package/dist/transport/testing/fault-transport.d.ts +44 -0
- package/dist/transport/testing/fault-transport.js +144 -0
- package/dist/transport/testing/multi-cases.d.ts +6 -0
- package/dist/transport/testing/multi-cases.js +113 -0
- package/dist/transport/testing/transport-cases.d.ts +6 -0
- package/dist/transport/testing/transport-cases.js +60 -0
- package/dist/transport/testing/transport-harness.d.ts +53 -0
- package/dist/transport/testing/transport-harness.js +127 -0
- package/dist/transport/transport.d.ts +60 -0
- package/dist/transport/transport.js +15 -0
- package/dist/types.d.ts +1 -0
- package/package.json +11 -3
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DurableStore contract of the durable core (packages/agent/docs/durable-core-adr.md). Every backend (memory, Bun
|
|
3
|
+
* SQLite, Durable Object SQLite) implements it and passes the STORE and KILL-STORE conformance cases.
|
|
4
|
+
*
|
|
5
|
+
* The store does not serialize concurrent writers: a single admission line above it (store-admission.ts) does.
|
|
6
|
+
*/
|
|
7
|
+
/** A JSON value as the store persists it. */
|
|
8
|
+
export type StoreValue = null | boolean | number | string | readonly StoreValue[] | {
|
|
9
|
+
readonly [key: string]: StoreValue;
|
|
10
|
+
};
|
|
11
|
+
/** One log record. `seq` is assigned by the store: gapless from 1, in commit order. `id` is unique in the log. */
|
|
12
|
+
export interface StoreRecord {
|
|
13
|
+
readonly seq: number;
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly kind: string;
|
|
16
|
+
readonly data: StoreValue;
|
|
17
|
+
}
|
|
18
|
+
export interface StoreDocument {
|
|
19
|
+
readonly key: string;
|
|
20
|
+
readonly data: StoreValue;
|
|
21
|
+
}
|
|
22
|
+
export type StoreWrite = {
|
|
23
|
+
readonly op: "append";
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly kind: string;
|
|
26
|
+
readonly data: StoreValue;
|
|
27
|
+
} | {
|
|
28
|
+
readonly op: "put";
|
|
29
|
+
readonly key: string;
|
|
30
|
+
readonly data: StoreValue;
|
|
31
|
+
} | {
|
|
32
|
+
readonly op: "delete";
|
|
33
|
+
readonly key: string;
|
|
34
|
+
};
|
|
35
|
+
export interface CommitResult {
|
|
36
|
+
/** The sequence numbers of the batch's appends, in write order. */
|
|
37
|
+
readonly seqs: readonly number[];
|
|
38
|
+
/** The last committed sequence number after this batch (0 for an empty log). */
|
|
39
|
+
readonly head: number;
|
|
40
|
+
}
|
|
41
|
+
/** Everything a store holds; the unit of claim + import between hosts. */
|
|
42
|
+
export interface StoreSnapshot {
|
|
43
|
+
readonly epoch: number;
|
|
44
|
+
readonly head: number;
|
|
45
|
+
/** Every record, ascending by `seq`. */
|
|
46
|
+
readonly records: readonly StoreRecord[];
|
|
47
|
+
/** Every document, ascending by `key`. */
|
|
48
|
+
readonly documents: readonly StoreDocument[];
|
|
49
|
+
}
|
|
50
|
+
export interface RecordScan {
|
|
51
|
+
readonly fromSeq?: number;
|
|
52
|
+
readonly toSeq?: number;
|
|
53
|
+
readonly kind?: string;
|
|
54
|
+
readonly order?: "asc" | "desc";
|
|
55
|
+
readonly limit?: number;
|
|
56
|
+
}
|
|
57
|
+
export type StoreOperation = "commit" | "claim" | "import";
|
|
58
|
+
/**
|
|
59
|
+
* A point inside a durable write. `before`: nothing of the operation is durable yet. `write`: inside the atomic unit,
|
|
60
|
+
* before write `index` is staged. `durable`: the operation is durable, before `adopt` runs and before it resolves.
|
|
61
|
+
*/
|
|
62
|
+
export type StoreWritePoint = {
|
|
63
|
+
readonly op: StoreOperation;
|
|
64
|
+
readonly at: "before";
|
|
65
|
+
} | {
|
|
66
|
+
readonly op: "commit" | "import";
|
|
67
|
+
readonly at: "write";
|
|
68
|
+
readonly index: number;
|
|
69
|
+
} | {
|
|
70
|
+
readonly op: StoreOperation;
|
|
71
|
+
readonly at: "durable";
|
|
72
|
+
};
|
|
73
|
+
export interface DurableStoreOptions {
|
|
74
|
+
/**
|
|
75
|
+
* Test seam for cuts and kills: called at every write point. A throw at `before` or `write` leaves nothing of the
|
|
76
|
+
* operation durable; a throw at `durable` leaves all of it durable and skips `adopt`.
|
|
77
|
+
*/
|
|
78
|
+
readonly onWritePoint?: (point: StoreWritePoint) => void;
|
|
79
|
+
}
|
|
80
|
+
export type DurableStoreErrorCode = "unclaimed" | "stale-epoch" | "invalid-claim" | "duplicate-record-id" | "import-conflict";
|
|
81
|
+
export declare class DurableStoreError extends Error {
|
|
82
|
+
readonly code: DurableStoreErrorCode;
|
|
83
|
+
constructor(code: DurableStoreErrorCode, message: string);
|
|
84
|
+
}
|
|
85
|
+
export interface DurableStore {
|
|
86
|
+
/** The current owner epoch; 0 when the store was never claimed. */
|
|
87
|
+
readEpoch(): Promise<number>;
|
|
88
|
+
/** The last committed sequence number; 0 for an empty log. */
|
|
89
|
+
readHead(): Promise<number>;
|
|
90
|
+
/**
|
|
91
|
+
* Compare-and-set the owner epoch from `expectedEpoch` to `nextEpoch`, which must be exactly `expectedEpoch + 1`; any
|
|
92
|
+
* other `nextEpoch` (equal, lower, or skipping an epoch) is refused (`invalid-claim`) and changes nothing. A claimant
|
|
93
|
+
* whose expected epoch is no longer current is refused (`stale-epoch`), so an old owner can never take the store back.
|
|
94
|
+
*/
|
|
95
|
+
claim(expectedEpoch: number, nextEpoch: number): Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Commit a batch atomically under the owner `epoch`: all of it becomes durable, or none of it. `adopt` runs exactly
|
|
98
|
+
* once, after the batch is durable and before the commit resolves, and never for a batch that did not become
|
|
99
|
+
* durable. A commit from any epoch but the current one is refused (`stale-epoch`) and leaves the store unchanged.
|
|
100
|
+
*/
|
|
101
|
+
commit(epoch: number, writes: readonly StoreWrite[], adopt: (result: CommitResult) => void): Promise<CommitResult>;
|
|
102
|
+
/** A new ID, unique across instances, reopens and hosts. */
|
|
103
|
+
mintId(): string;
|
|
104
|
+
scanRecords(query?: RecordScan): Promise<StoreRecord[]>;
|
|
105
|
+
getDocument(key: string): Promise<StoreValue | undefined>;
|
|
106
|
+
/** Documents whose key starts with `prefix`, ascending by key. */
|
|
107
|
+
scanDocuments(prefix: string): Promise<StoreDocument[]>;
|
|
108
|
+
exportSnapshot(): Promise<StoreSnapshot>;
|
|
109
|
+
/**
|
|
110
|
+
* Import another host's snapshot under this store's current owner `epoch`, which must be above the snapshot's.
|
|
111
|
+
* Atomic. The store's records must be a prefix of the snapshot's: records past the store's head are imported once,
|
|
112
|
+
* in order, with their seqs and IDs; documents become the snapshot's. Importing the same snapshot again is a no-op.
|
|
113
|
+
*/
|
|
114
|
+
importSnapshot(epoch: number, snapshot: StoreSnapshot): Promise<void>;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DurableStore contract of the durable core (packages/agent/docs/durable-core-adr.md). Every backend (memory, Bun
|
|
3
|
+
* SQLite, Durable Object SQLite) implements it and passes the STORE and KILL-STORE conformance cases.
|
|
4
|
+
*
|
|
5
|
+
* The store does not serialize concurrent writers: a single admission line above it (store-admission.ts) does.
|
|
6
|
+
*/
|
|
7
|
+
export class DurableStoreError extends Error {
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "DurableStoreError";
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CaseImplementation } from "../../conformance/runner.ts";
|
|
2
|
+
import type { StoreOperation } from "../store.ts";
|
|
3
|
+
import { type StoreCaseAdapters } from "./store-harness.ts";
|
|
4
|
+
/** Where a KILL case applies; a host report logs it in kill[] with the registry's expected outcome. */
|
|
5
|
+
export interface KillDeclaration {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly op: StoreOperation;
|
|
8
|
+
readonly at: "before" | "durable";
|
|
9
|
+
readonly placement: "everywhere";
|
|
10
|
+
}
|
|
11
|
+
export declare const KILL_STORE_DECLARATIONS: readonly KillDeclaration[];
|
|
12
|
+
export declare const KILL_STORE_CASES: ReadonlyMap<string, CaseImplementation<StoreCaseAdapters>>;
|
|
13
|
+
//# sourceMappingURL=kill-store.d.ts.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { append, claimedStore, put, stored } from "./store-cases.js";
|
|
2
|
+
import { adoptLog, cutOnceAt, expectCut, expectEqual, expectStoreError, } from "./store-harness.js";
|
|
3
|
+
const killCase = (id, op, at, body) => ({ id, op, at, placement: "everywhere", run: ({ store }) => body(store, { op, at }) });
|
|
4
|
+
async function seeded(adapter) {
|
|
5
|
+
const { medium, store } = await claimedStore(adapter);
|
|
6
|
+
await store.commit(1, [append("r1"), put("doc", 1)], () => { });
|
|
7
|
+
return { medium, baseline: await store.exportSnapshot() };
|
|
8
|
+
}
|
|
9
|
+
async function killThenReopen(adapter, medium, kill, operation) {
|
|
10
|
+
await expectCut(operation(await adapter.open(medium, cutOnceAt(kill))), `kill ${kill.at} ${kill.op}`);
|
|
11
|
+
return adapter.open(medium);
|
|
12
|
+
}
|
|
13
|
+
const content = ({ head, records, documents }) => ({ head, records, documents });
|
|
14
|
+
const SECOND_BATCH = [append("r2"), put("doc", 2)];
|
|
15
|
+
const KILL_CASES = [
|
|
16
|
+
killCase("KILL-STORE-01", "commit", "before", async (adapter, kill) => {
|
|
17
|
+
const { medium, baseline } = await seeded(adapter);
|
|
18
|
+
const log = adoptLog();
|
|
19
|
+
const restarted = await killThenReopen(adapter, medium, kill, (s) => s.commit(1, SECOND_BATCH, log.adopt));
|
|
20
|
+
expectEqual(await restarted.exportSnapshot(), baseline, "nothing of the batch is durable");
|
|
21
|
+
expectEqual(await restarted.commit(1, SECOND_BATCH, log.adopt), { seqs: [2], head: 2 }, "same IDs, next seq");
|
|
22
|
+
expectEqual(log.calls, ["adopt:2"], "only the committed batch adopts");
|
|
23
|
+
}),
|
|
24
|
+
killCase("KILL-STORE-02", "commit", "durable", async (adapter, kill) => {
|
|
25
|
+
const { medium, baseline } = await seeded(adapter);
|
|
26
|
+
const log = adoptLog();
|
|
27
|
+
const restarted = await killThenReopen(adapter, medium, kill, (s) => s.commit(1, SECOND_BATCH, log.adopt));
|
|
28
|
+
const expected = {
|
|
29
|
+
...baseline,
|
|
30
|
+
head: 2,
|
|
31
|
+
records: [stored(1, "r1"), stored(2, "r2")],
|
|
32
|
+
documents: [{ key: "doc", data: 2 }],
|
|
33
|
+
};
|
|
34
|
+
expectEqual(log.calls, [], "the killed process never adopted");
|
|
35
|
+
expectEqual(await restarted.exportSnapshot(), expected, "the whole batch is durable with its IDs and seqs");
|
|
36
|
+
await expectStoreError(restarted.commit(1, SECOND_BATCH, log.adopt), "duplicate-record-id", "a retried batch");
|
|
37
|
+
expectEqual(await restarted.exportSnapshot(), expected, "the retry applies nothing twice");
|
|
38
|
+
}),
|
|
39
|
+
killCase("KILL-STORE-03", "claim", "before", async (adapter, kill) => {
|
|
40
|
+
const { medium, baseline } = await seeded(adapter);
|
|
41
|
+
const restarted = await killThenReopen(adapter, medium, kill, (s) => s.claim(1, 2));
|
|
42
|
+
expectEqual(await restarted.exportSnapshot(), baseline, "the epoch and log are unchanged");
|
|
43
|
+
await restarted.claim(1, 2);
|
|
44
|
+
expectEqual(await restarted.readEpoch(), 2, "the claim succeeds after restart");
|
|
45
|
+
}),
|
|
46
|
+
killCase("KILL-STORE-04", "claim", "durable", async (adapter, kill) => {
|
|
47
|
+
const { medium, baseline } = await seeded(adapter);
|
|
48
|
+
const restarted = await killThenReopen(adapter, medium, kill, (s) => s.claim(1, 2));
|
|
49
|
+
expectEqual(await restarted.exportSnapshot(), { ...baseline, epoch: 2 }, "the claim is durable; the log is intact");
|
|
50
|
+
await expectStoreError(restarted.claim(1, 2), "stale-epoch", "the claim cannot be taken twice");
|
|
51
|
+
await expectStoreError(restarted.commit(1, [append("late")], () => { }), "stale-epoch", "the old epoch is fenced");
|
|
52
|
+
}),
|
|
53
|
+
killCase("KILL-STORE-05", "import", "before", async (adapter, kill) => {
|
|
54
|
+
const { baseline } = await seeded(adapter);
|
|
55
|
+
const { medium } = await claimedStore(adapter, 2);
|
|
56
|
+
const restarted = await killThenReopen(adapter, medium, kill, (s) => s.importSnapshot(2, baseline));
|
|
57
|
+
expectEqual(await restarted.exportSnapshot(), { epoch: 2, head: 0, records: [], documents: [] }, "nothing imported");
|
|
58
|
+
await restarted.importSnapshot(2, baseline);
|
|
59
|
+
expectEqual(content(await restarted.exportSnapshot()), content(baseline), "the import succeeds after restart");
|
|
60
|
+
}),
|
|
61
|
+
killCase("KILL-STORE-06", "import", "durable", async (adapter, kill) => {
|
|
62
|
+
const { baseline } = await seeded(adapter);
|
|
63
|
+
const { medium } = await claimedStore(adapter, 2);
|
|
64
|
+
const restarted = await killThenReopen(adapter, medium, kill, (s) => s.importSnapshot(2, baseline));
|
|
65
|
+
expectEqual(content(await restarted.exportSnapshot()), content(baseline), "the whole import is durable");
|
|
66
|
+
await restarted.importSnapshot(2, baseline);
|
|
67
|
+
expectEqual(content(await restarted.exportSnapshot()), content(baseline), "re-import adds nothing twice");
|
|
68
|
+
}),
|
|
69
|
+
];
|
|
70
|
+
export const KILL_STORE_DECLARATIONS = KILL_CASES.map(({ id, op, at, placement }) => ({
|
|
71
|
+
id,
|
|
72
|
+
op,
|
|
73
|
+
at,
|
|
74
|
+
placement,
|
|
75
|
+
}));
|
|
76
|
+
export const KILL_STORE_CASES = new Map(KILL_CASES.map(({ id, run }) => [id, run]));
|
|
77
|
+
//# sourceMappingURL=kill-store.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CaseImplementation } from "../../conformance/runner.ts";
|
|
2
|
+
import { type WakeCaseAdapters } from "./wake-harness.ts";
|
|
3
|
+
/** Where a KILL-WAKE case applies; a host report logs it in kill[] with the registry's expected outcome. */
|
|
4
|
+
export interface KillWakeDeclaration {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly operation: "admit" | "commit" | "update" | "fire";
|
|
7
|
+
readonly point: string;
|
|
8
|
+
readonly placement: "everywhere";
|
|
9
|
+
}
|
|
10
|
+
export declare const KILL_WAKE_DECLARATIONS: readonly KillWakeDeclaration[];
|
|
11
|
+
export declare const KILL_WAKE_CASES: ReadonlyMap<string, CaseImplementation<WakeCaseAdapters>>;
|
|
12
|
+
//# sourceMappingURL=kill-wake.d.ts.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { expectEqual } from "./store-harness.js";
|
|
2
|
+
import { createSlot, cutAlarm, cutStore, cutWake, expectWakeCut, HEARTBEAT_MS, } from "./wake-harness.js";
|
|
3
|
+
const killCase = (id, operation, point, run) => ({ id, operation, point, placement: "everywhere", run });
|
|
4
|
+
const LATER = 600_000;
|
|
5
|
+
const DUE = 120_000;
|
|
6
|
+
async function killedAdmission(adapters, cut) {
|
|
7
|
+
const slot = await createSlot(adapters);
|
|
8
|
+
const { host, loop } = await slot.instance(cut);
|
|
9
|
+
await expectWakeCut(host.admit(() => loop.schedule("a", slot.base + LATER)), "the killed admission");
|
|
10
|
+
slot.kill();
|
|
11
|
+
return slot;
|
|
12
|
+
}
|
|
13
|
+
/** Recovery with no request from anyone: only the armed alarm brings the session back. */
|
|
14
|
+
async function recoversThroughHeartbeat(adapters, slot) {
|
|
15
|
+
expectEqual(await slot.alarmAt(), HEARTBEAT_MS, "the pre-armed heartbeat survived the kill");
|
|
16
|
+
await adapters.wake.advanceTo(slot.base + HEARTBEAT_MS);
|
|
17
|
+
expectEqual(await slot.alarmAt(), LATER, "the heartbeat's instance armed the committed wake");
|
|
18
|
+
await adapters.wake.advanceTo(slot.base + LATER);
|
|
19
|
+
expectEqual(await slot.ran(), ["a"], "the committed task ran exactly once");
|
|
20
|
+
expectEqual(await slot.alarmAt(), undefined, "and the alarm is cleared");
|
|
21
|
+
}
|
|
22
|
+
async function killedFire(adapters, cut) {
|
|
23
|
+
const slot = await createSlot(adapters);
|
|
24
|
+
const { host, loop } = await slot.instance(cut);
|
|
25
|
+
await host.admit(() => loop.schedule("a", slot.base + DUE));
|
|
26
|
+
await adapters.wake.advanceTo(slot.base + DUE);
|
|
27
|
+
expectEqual(slot.fires(), 1, "the alarm fired into the instance that is killed");
|
|
28
|
+
return slot;
|
|
29
|
+
}
|
|
30
|
+
async function recoversAfterKilledFire(adapters, slot) {
|
|
31
|
+
expectEqual(await slot.alarmAt(), DUE + HEARTBEAT_MS, "the fire's re-arm survived the kill");
|
|
32
|
+
await adapters.wake.advanceTo(slot.base + DUE + HEARTBEAT_MS);
|
|
33
|
+
expectEqual(await slot.ran(), ["a"], "the task ran exactly once");
|
|
34
|
+
expectEqual(await slot.alarmAt(), undefined, "and the alarm is cleared");
|
|
35
|
+
await adapters.wake.advanceTo(slot.base + LATER);
|
|
36
|
+
expectEqual(slot.fires(), 2, "no fire beyond the recovery");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* An admission B runs while a fire resumes other work on the same instance. The fire's updates see an idle loop (B's
|
|
40
|
+
* task is not committed yet), so they must not clear the heartbeat B armed; B is killed after its commit is durable,
|
|
41
|
+
* before its own update. The session runs at epoch 2.
|
|
42
|
+
*/
|
|
43
|
+
async function admissionDuringFire(adapters) {
|
|
44
|
+
const fireDone = Promise.withResolvers();
|
|
45
|
+
let admission;
|
|
46
|
+
let session;
|
|
47
|
+
const slot = await createSlot(adapters, 2);
|
|
48
|
+
const first = await slot.instance({
|
|
49
|
+
store: cutStore("commit", "durable", 3),
|
|
50
|
+
onWakePoint: (point) => {
|
|
51
|
+
if (point.op !== "fire" || session === undefined || admission !== undefined)
|
|
52
|
+
return;
|
|
53
|
+
const { host, loop } = session;
|
|
54
|
+
admission = host.admit(async () => {
|
|
55
|
+
await fireDone.promise;
|
|
56
|
+
await loop.schedule("b", slot.base + LATER);
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
session = first;
|
|
61
|
+
await first.host.admit(() => first.loop.schedule("a", slot.base + DUE));
|
|
62
|
+
await adapters.wake.advanceTo(slot.base + DUE);
|
|
63
|
+
expectEqual(await slot.ran(), ["a"], "the fire resumed its due task");
|
|
64
|
+
if (admission === undefined)
|
|
65
|
+
throw new Error("the admission did not start during the fire");
|
|
66
|
+
fireDone.resolve();
|
|
67
|
+
await expectWakeCut(admission, "the admission killed after its commit");
|
|
68
|
+
slot.kill();
|
|
69
|
+
return slot;
|
|
70
|
+
}
|
|
71
|
+
const KILL_CASES = [
|
|
72
|
+
killCase("KILL-WAKE-01", "admit", "alarm write before durable", async (adapters) => {
|
|
73
|
+
const slot = await killedAdmission(adapters, { alarm: cutAlarm("set", "before", 2) });
|
|
74
|
+
expectEqual(await slot.alarmAt(), undefined, "nothing of the admission is durable");
|
|
75
|
+
const { host, loop } = await slot.instance();
|
|
76
|
+
await host.admit(() => loop.schedule("a", slot.base + LATER));
|
|
77
|
+
await adapters.wake.advanceTo(slot.base + LATER);
|
|
78
|
+
expectEqual(await slot.ran(), ["a"], "the retried admission runs once");
|
|
79
|
+
}),
|
|
80
|
+
killCase("KILL-WAKE-02", "admit", "after the arm, before work", async (adapters) => {
|
|
81
|
+
const slot = await killedAdmission(adapters, { onWakePoint: cutWake("admit") });
|
|
82
|
+
expectEqual(await slot.alarmAt(), HEARTBEAT_MS, "the arm is durable");
|
|
83
|
+
await adapters.wake.advanceTo(slot.base + HEARTBEAT_MS);
|
|
84
|
+
expectEqual(await slot.ran(), [], "an alarm without work runs nothing");
|
|
85
|
+
expectEqual(await slot.alarmAt(), undefined, "and ends cleared");
|
|
86
|
+
expectEqual(slot.fires(), 1, "one harmless fire");
|
|
87
|
+
}),
|
|
88
|
+
killCase("KILL-WAKE-03", "commit", "after the commit is durable, before the alarm update", async (adapters) => {
|
|
89
|
+
const slot = await killedAdmission(adapters, { store: cutStore("commit", "durable") });
|
|
90
|
+
await recoversThroughHeartbeat(adapters, slot);
|
|
91
|
+
}),
|
|
92
|
+
killCase("KILL-WAKE-04", "update", "eviction during the alarm write", async (adapters) => {
|
|
93
|
+
const slot = await killedAdmission(adapters, { alarm: cutAlarm("set", "before", 3) });
|
|
94
|
+
await recoversThroughHeartbeat(adapters, slot);
|
|
95
|
+
}),
|
|
96
|
+
killCase("KILL-WAKE-05", "fire", "after the re-arm, before resume", async (adapters) => {
|
|
97
|
+
const slot = await killedFire(adapters, { onWakePoint: cutWake("fire") });
|
|
98
|
+
expectEqual(await slot.ran(), [], "nothing ran before the kill");
|
|
99
|
+
await recoversAfterKilledFire(adapters, slot);
|
|
100
|
+
}),
|
|
101
|
+
killCase("KILL-WAKE-06", "fire", "after a resumed task's commit is durable", async (adapters) => {
|
|
102
|
+
const slot = await killedFire(adapters, { store: cutStore("commit", "durable", 2) });
|
|
103
|
+
expectEqual(await slot.ran(), ["a"], "the task's commit is durable");
|
|
104
|
+
await recoversAfterKilledFire(adapters, slot);
|
|
105
|
+
}),
|
|
106
|
+
killCase("KILL-WAKE-07", "fire", "eviction during the alarm clear after the work", async (adapters) => {
|
|
107
|
+
const slot = await killedFire(adapters, { alarm: cutAlarm("clear", "before", 2) });
|
|
108
|
+
expectEqual(await slot.ran(), ["a"], "the work is durable");
|
|
109
|
+
await recoversAfterKilledFire(adapters, slot);
|
|
110
|
+
}),
|
|
111
|
+
killCase("KILL-WAKE-08", "admit", "a concurrent fire's updates while the admission is uncommitted, then a kill after its commit", async (adapters) => {
|
|
112
|
+
const slot = await admissionDuringFire(adapters);
|
|
113
|
+
expectEqual(await slot.alarmAt(), DUE + HEARTBEAT_MS, "the admission's heartbeat outlived the fire's updates");
|
|
114
|
+
await adapters.wake.advanceTo(slot.base + DUE + HEARTBEAT_MS);
|
|
115
|
+
expectEqual(await slot.alarmAt(), LATER, "the heartbeat's instance armed the committed wake");
|
|
116
|
+
await adapters.wake.advanceTo(slot.base + LATER);
|
|
117
|
+
expectEqual(await slot.ran(), ["a", "b"], "each task ran exactly once");
|
|
118
|
+
expectEqual(await slot.alarmAt(), undefined, "and the alarm is cleared");
|
|
119
|
+
}),
|
|
120
|
+
];
|
|
121
|
+
export const KILL_WAKE_DECLARATIONS = KILL_CASES.map(({ id, operation, point, placement }) => ({ id, operation, point, placement }));
|
|
122
|
+
export const KILL_WAKE_CASES = new Map(KILL_CASES.map(({ id, run }) => [id, run]));
|
|
123
|
+
//# sourceMappingURL=kill-wake.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Manual timers for a host under test: a clock that moves only through `advanceTo`, which runs due timers in order. */
|
|
2
|
+
export interface ManualTimers {
|
|
3
|
+
now(): number;
|
|
4
|
+
setTimer(callback: () => unknown, ms: number): () => void;
|
|
5
|
+
advanceTo(at: number): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare function createManualTimers(start: number): ManualTimers;
|
|
8
|
+
//# sourceMappingURL=manual-timers.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function createManualTimers(start) {
|
|
2
|
+
let time = start;
|
|
3
|
+
let nextId = 1;
|
|
4
|
+
const timers = new Map();
|
|
5
|
+
return {
|
|
6
|
+
now: () => time,
|
|
7
|
+
setTimer: (callback, ms) => {
|
|
8
|
+
const id = nextId++;
|
|
9
|
+
timers.set(id, { due: time + Math.max(0, ms), callback });
|
|
10
|
+
return () => void timers.delete(id);
|
|
11
|
+
},
|
|
12
|
+
async advanceTo(at) {
|
|
13
|
+
for (let step = 0;; step++) {
|
|
14
|
+
if (step > 10_000)
|
|
15
|
+
throw new Error("manual timers: more than 10000 timers fired in one advance");
|
|
16
|
+
let next;
|
|
17
|
+
for (const entry of timers) {
|
|
18
|
+
if (entry[1].due <= at && (next === undefined || entry[1].due < next[1].due))
|
|
19
|
+
next = entry;
|
|
20
|
+
}
|
|
21
|
+
if (next === undefined)
|
|
22
|
+
break;
|
|
23
|
+
timers.delete(next[0]);
|
|
24
|
+
time = Math.max(time, next[1].due);
|
|
25
|
+
await next[1].callback();
|
|
26
|
+
}
|
|
27
|
+
time = Math.max(time, at);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=manual-timers.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CaseImplementation } from "../../conformance/runner.ts";
|
|
2
|
+
import { type WakeCaseAdapters } from "./wake-harness.ts";
|
|
3
|
+
type MultiHostCase = CaseImplementation<WakeCaseAdapters>;
|
|
4
|
+
export declare const MULTI_HOST_CASES: ReadonlyMap<string, MultiHostCase>;
|
|
5
|
+
export {};
|
|
6
|
+
//# sourceMappingURL=multi-host-cases.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { expectEqual } from "./store-harness.js";
|
|
2
|
+
import { createSlot } from "./wake-harness.js";
|
|
3
|
+
async function schedule(slot, id, offset) {
|
|
4
|
+
const { host, loop } = await slot.instance();
|
|
5
|
+
await host.admit(() => loop.schedule(id, slot.base + offset));
|
|
6
|
+
}
|
|
7
|
+
const observe = async (slots) => ({
|
|
8
|
+
fires: slots.map((slot) => slot.fires()),
|
|
9
|
+
ran: await Promise.all(slots.map((slot) => slot.ran())),
|
|
10
|
+
alarms: await Promise.all(slots.map((slot) => slot.alarmAt())),
|
|
11
|
+
});
|
|
12
|
+
const manySessionsOnOneHost = async (adapters) => {
|
|
13
|
+
const slots = [await createSlot(adapters), await createSlot(adapters), await createSlot(adapters)];
|
|
14
|
+
const [first, second, third] = slots;
|
|
15
|
+
if (first === undefined || second === undefined || third === undefined)
|
|
16
|
+
throw new Error("three sessions");
|
|
17
|
+
await schedule(first, "x", 120_000);
|
|
18
|
+
await schedule(first, "y", 300_000);
|
|
19
|
+
await schedule(second, "x", 200_000);
|
|
20
|
+
await third.instance();
|
|
21
|
+
expectEqual(await observe(slots), { fires: [0, 0, 0], ran: [[], [], []], alarms: [120_000, 200_000, undefined] }, "armed per session");
|
|
22
|
+
second.kill();
|
|
23
|
+
await adapters.wake.advanceTo(first.base + 120_000);
|
|
24
|
+
expectEqual(await observe(slots), { fires: [1, 0, 0], ran: [["x"], [], []], alarms: [300_000, 200_000, undefined] }, "a fire resumes only its own session; the same task ID elsewhere is untouched");
|
|
25
|
+
third.kill();
|
|
26
|
+
await third.instance();
|
|
27
|
+
expectEqual((await observe(slots)).alarms, [300_000, 200_000, undefined], "restarting one session moves no other session's alarm");
|
|
28
|
+
await adapters.wake.advanceTo(first.base + 300_000);
|
|
29
|
+
expectEqual(await observe(slots), { fires: [2, 1, 0], ran: [["x", "y"], ["x"], []], alarms: [undefined, undefined, undefined] }, "an evicted session comes back on its own alarm; records and wakes stay per session");
|
|
30
|
+
};
|
|
31
|
+
export const MULTI_HOST_CASES = new Map([["MULTI-HOST-01", manySessionsOnOneHost]]);
|
|
32
|
+
//# sourceMappingURL=multi-host-cases.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CaseImplementation } from "../../conformance/runner.ts";
|
|
2
|
+
import type { StoreValue, StoreWrite } from "../store.ts";
|
|
3
|
+
import { type StoreCaseAdapters, type StoreConformanceAdapter } from "./store-harness.ts";
|
|
4
|
+
type StoreCase = CaseImplementation<StoreCaseAdapters>;
|
|
5
|
+
export declare const append: (id: string, kind?: string) => StoreWrite;
|
|
6
|
+
export declare const put: (key: string, data: StoreValue) => StoreWrite;
|
|
7
|
+
export declare const stored: (seq: number, id: string, kind?: string) => {
|
|
8
|
+
seq: number;
|
|
9
|
+
id: string;
|
|
10
|
+
kind: string;
|
|
11
|
+
data: {
|
|
12
|
+
id: string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export declare function claimedStore(adapter: StoreConformanceAdapter, epoch?: number): Promise<{
|
|
16
|
+
medium: string;
|
|
17
|
+
store: import("../store.ts").DurableStore;
|
|
18
|
+
}>;
|
|
19
|
+
export declare const STORE_CASES: ReadonlyMap<string, StoreCase>;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=store-cases.d.ts.map
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { createAdmissionLine } from "../store-admission.js";
|
|
2
|
+
import { adoptLog, cutOnceAt, expectCut, expectEqual, expectStoreError, reopenSnapshot, } from "./store-harness.js";
|
|
3
|
+
export const append = (id, kind = "entry") => ({ op: "append", id, kind, data: { id } });
|
|
4
|
+
export const put = (key, data) => ({ op: "put", key, data });
|
|
5
|
+
export const stored = (seq, id, kind = "entry") => ({ seq, id, kind, data: { id } });
|
|
6
|
+
export async function claimedStore(adapter, epoch = 1) {
|
|
7
|
+
const medium = await adapter.createMedium();
|
|
8
|
+
const store = await adapter.open(medium);
|
|
9
|
+
for (let next = 1; next <= epoch; next++)
|
|
10
|
+
await store.claim(next - 1, next);
|
|
11
|
+
return { medium, store };
|
|
12
|
+
}
|
|
13
|
+
const commitAdoptsOnDurable = async ({ store: adapter }) => {
|
|
14
|
+
const medium = await adapter.createMedium();
|
|
15
|
+
const events = [];
|
|
16
|
+
const store = await adapter.open(medium, {
|
|
17
|
+
onWritePoint: (point) => void (point.op === "commit" && point.at === "durable" && events.push("durable")),
|
|
18
|
+
});
|
|
19
|
+
await store.claim(0, 1);
|
|
20
|
+
const adopt = (result) => void events.push(`adopt:${result.seqs.join(",")}`);
|
|
21
|
+
expectEqual(await store.commit(1, [append("r1"), append("r2"), put("doc", 1)], adopt), { seqs: [1, 2], head: 2 }, "batch");
|
|
22
|
+
expectEqual(events, ["durable", "adopt:1,2"], "adopt runs once, after the batch is durable");
|
|
23
|
+
expectEqual(await store.commit(1, [put("doc", 2)], adopt), { seqs: [], head: 2 }, "documents-only batch");
|
|
24
|
+
expectEqual(await reopenSnapshot(adapter, medium), {
|
|
25
|
+
epoch: 1,
|
|
26
|
+
head: 2,
|
|
27
|
+
records: [stored(1, "r1"), stored(2, "r2")],
|
|
28
|
+
documents: [{ key: "doc", data: 2 }],
|
|
29
|
+
}, "another instance on the medium sees every committed batch whole");
|
|
30
|
+
};
|
|
31
|
+
const BATCH = [append("r2"), put("a", 2), { op: "delete", key: "b" }, append("r3"), put("c", 1)];
|
|
32
|
+
const cutAtEveryWritePoint = async ({ store: adapter }) => {
|
|
33
|
+
const points = [
|
|
34
|
+
{ op: "commit", at: "before" },
|
|
35
|
+
...BATCH.map((_, index) => ({ op: "commit", at: "write", index })),
|
|
36
|
+
];
|
|
37
|
+
for (const point of points) {
|
|
38
|
+
const what = `cut at ${JSON.stringify(point)}`;
|
|
39
|
+
const { medium, store: setup } = await claimedStore(adapter);
|
|
40
|
+
await setup.commit(1, [append("r1"), put("a", 1), put("b", 1)], () => { });
|
|
41
|
+
const baseline = await setup.exportSnapshot();
|
|
42
|
+
const store = await adapter.open(medium, cutOnceAt(point));
|
|
43
|
+
const log = adoptLog();
|
|
44
|
+
await expectCut(store.commit(1, BATCH, log.adopt), what);
|
|
45
|
+
expectEqual(log.calls, [], `${what}: no adopt`);
|
|
46
|
+
expectEqual(await reopenSnapshot(adapter, medium), baseline, `${what}: nothing of the batch is durable`);
|
|
47
|
+
expectEqual(await store.commit(1, BATCH, log.adopt), { seqs: [2, 3], head: 3 }, `${what}: no seq burned`);
|
|
48
|
+
expectEqual(log.calls, ["adopt:2,3"], `${what}: the retried batch adopts once`);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const concurrentAdmissions = async ({ store: adapter }) => {
|
|
52
|
+
const { store } = await claimedStore(adapter);
|
|
53
|
+
const line = createAdmissionLine();
|
|
54
|
+
const events = [];
|
|
55
|
+
const gate = Promise.withResolvers();
|
|
56
|
+
const admit = (name, hold) => line.admit(async () => {
|
|
57
|
+
events.push(`${name}:start`);
|
|
58
|
+
if (hold !== undefined)
|
|
59
|
+
await hold;
|
|
60
|
+
return store.commit(1, [append(name)], (result) => void events.push(`${name}:adopt:${result.seqs.join(",")}`));
|
|
61
|
+
});
|
|
62
|
+
const admissions = [
|
|
63
|
+
admit("a", gate.promise),
|
|
64
|
+
admit("b"),
|
|
65
|
+
line.admit(async () => {
|
|
66
|
+
events.push("c:start");
|
|
67
|
+
throw new Error("mutation refused");
|
|
68
|
+
}),
|
|
69
|
+
admit("d"),
|
|
70
|
+
];
|
|
71
|
+
gate.resolve();
|
|
72
|
+
const settled = await Promise.allSettled(admissions);
|
|
73
|
+
expectEqual(settled.map((s) => s.status), ["fulfilled", "fulfilled", "rejected", "fulfilled"], "admission outcomes");
|
|
74
|
+
expectEqual(events, ["a:start", "a:adopt:1", "b:start", "b:adopt:2", "c:start", "d:start", "d:adopt:3"], "one admission at a time, in admission order; a refused mutation releases the line");
|
|
75
|
+
expectEqual((await store.scanRecords()).map((r) => `${r.seq}:${r.id}`), ["1:a", "2:b", "3:d"], "records");
|
|
76
|
+
};
|
|
77
|
+
const delayedOldOwner = async ({ store: adapter }) => {
|
|
78
|
+
const { medium, store: oldOwner } = await claimedStore(adapter);
|
|
79
|
+
await oldOwner.commit(1, [append("old-1")], () => { });
|
|
80
|
+
const newOwner = await adapter.open(medium);
|
|
81
|
+
await newOwner.claim(1, 2);
|
|
82
|
+
await newOwner.commit(2, [append("new-1")], () => { });
|
|
83
|
+
const late = adoptLog();
|
|
84
|
+
await expectStoreError(oldOwner.commit(1, [append("old-2")], late.adopt), "stale-epoch", "old owner's delayed commit");
|
|
85
|
+
await expectStoreError(oldOwner.claim(1, 2), "stale-epoch", "old owner's delayed claim");
|
|
86
|
+
await expectStoreError(newOwner.commit(3, [append("x")], late.adopt), "stale-epoch", "a commit from a future epoch");
|
|
87
|
+
await expectStoreError(newOwner.claim(2, 2), "invalid-claim", "a claim that does not raise the epoch");
|
|
88
|
+
const fresh = await adapter.open(await adapter.createMedium());
|
|
89
|
+
await expectStoreError(fresh.commit(0, [append("x")], late.adopt), "unclaimed", "a commit to an unclaimed store");
|
|
90
|
+
expectEqual(late.calls, [], "no refused commit adopts");
|
|
91
|
+
expectEqual(await reopenSnapshot(adapter, medium), {
|
|
92
|
+
epoch: 2,
|
|
93
|
+
head: 2,
|
|
94
|
+
records: [stored(1, "old-1"), stored(2, "new-1")],
|
|
95
|
+
documents: [],
|
|
96
|
+
}, "the new owner's state");
|
|
97
|
+
};
|
|
98
|
+
const claimAndImport = async ({ store: adapter }) => {
|
|
99
|
+
const { store: source } = await claimedStore(adapter);
|
|
100
|
+
await source.commit(1, [append("r1"), append("r2"), put("doc", "v1")], () => { });
|
|
101
|
+
const first = await source.exportSnapshot();
|
|
102
|
+
const { medium, store: target } = await claimedStore(adapter, 2);
|
|
103
|
+
await expectStoreError(target.importSnapshot(1, first), "stale-epoch", "import under a non-current epoch");
|
|
104
|
+
await target.importSnapshot(2, first);
|
|
105
|
+
await target.importSnapshot(2, first);
|
|
106
|
+
expectEqual(await reopenSnapshot(adapter, medium), { ...first, epoch: 2 }, "import is whole and idempotent");
|
|
107
|
+
await source.commit(1, [append("r3"), put("doc", "v2")], () => { });
|
|
108
|
+
const tail = await source.exportSnapshot();
|
|
109
|
+
await target.importSnapshot(2, tail);
|
|
110
|
+
expectEqual(await reopenSnapshot(adapter, medium), { ...tail, epoch: 2 }, "the tail past the head is imported once");
|
|
111
|
+
const conflicting = { ...tail, records: [stored(1, "r1"), stored(2, "other"), stored(3, "r3")] };
|
|
112
|
+
await expectStoreError(target.importSnapshot(2, conflicting), "import-conflict", "a diverged log");
|
|
113
|
+
await expectStoreError(target.importSnapshot(2, { ...tail, epoch: 2 }), "invalid-claim", "an import not above the source");
|
|
114
|
+
expectEqual(await reopenSnapshot(adapter, medium), { ...tail, epoch: 2 }, "refused imports change nothing");
|
|
115
|
+
expectEqual(await target.commit(2, [append("r4")], () => { }), { seqs: [4], head: 4 }, "the new owner continues the log");
|
|
116
|
+
};
|
|
117
|
+
const claimRaisesEpochByOne = async ({ store: adapter }) => {
|
|
118
|
+
const { medium, store } = await claimedStore(adapter);
|
|
119
|
+
await store.commit(1, [append("r1")], () => { });
|
|
120
|
+
const baseline = await store.exportSnapshot();
|
|
121
|
+
const fresh = await adapter.open(await adapter.createMedium());
|
|
122
|
+
await expectStoreError(store.claim(1, 3), "invalid-claim", "a claim that skips an epoch");
|
|
123
|
+
await expectStoreError(fresh.claim(0, 2), "invalid-claim", "a first claim that skips epoch 1");
|
|
124
|
+
expectEqual(await fresh.readEpoch(), 0, "a refused first claim leaves the store unclaimed");
|
|
125
|
+
expectEqual(await reopenSnapshot(adapter, medium), baseline, "a refused claim leaves the epoch and log unchanged");
|
|
126
|
+
await store.claim(1, 2);
|
|
127
|
+
expectEqual(await store.readEpoch(), 2, "the next epoch is still claimable");
|
|
128
|
+
};
|
|
129
|
+
const idsScansAndDocuments = async ({ store: adapter }) => {
|
|
130
|
+
const { medium, store } = await claimedStore(adapter);
|
|
131
|
+
const reopened = await adapter.open(medium);
|
|
132
|
+
const elsewhere = await adapter.open(await adapter.createMedium());
|
|
133
|
+
const ids = [store, reopened, elsewhere].flatMap((s) => Array.from({ length: 100 }, () => s.mintId()));
|
|
134
|
+
expectEqual(new Set(ids).size, ids.length, "minted IDs are unique across instances, reopens and hosts");
|
|
135
|
+
const kinds = ["a", "b", "a", "b", "a"];
|
|
136
|
+
await store.commit(1, [...kinds.map((kind, i) => append(`r${i + 1}`, kind)), put("s/2", 2), put("s/1", 1), put("t/1", 3)], () => { });
|
|
137
|
+
const seqsOf = async (query) => (await store.scanRecords(query)).map((r) => r.seq);
|
|
138
|
+
expectEqual(await seqsOf(undefined), [1, 2, 3, 4, 5], "full scan");
|
|
139
|
+
expectEqual(await seqsOf({ fromSeq: 2, toSeq: 4 }), [2, 3, 4], "range scan");
|
|
140
|
+
expectEqual(await seqsOf({ kind: "b" }), [2, 4], "kind scan");
|
|
141
|
+
expectEqual(await seqsOf({ order: "desc", limit: 2 }), [5, 4], "descending, limited");
|
|
142
|
+
expectEqual(await seqsOf({ kind: "a", order: "desc", limit: 1 }), [5], "kind, descending, limited");
|
|
143
|
+
expectEqual(await store.scanDocuments("s/"), [
|
|
144
|
+
{ key: "s/1", data: 1 },
|
|
145
|
+
{ key: "s/2", data: 2 },
|
|
146
|
+
], "prefix scan");
|
|
147
|
+
await store.commit(1, [{ op: "delete", key: "s/1" }], () => { });
|
|
148
|
+
expectEqual(await store.getDocument("s/1"), undefined, "a deleted document");
|
|
149
|
+
expectEqual((await store.scanDocuments("")).map((d) => d.key), ["s/2", "t/1"], "all documents by key");
|
|
150
|
+
await expectStoreError(store.commit(1, [append("r1")], () => { }), "duplicate-record-id", "an ID already in the log");
|
|
151
|
+
await expectStoreError(store.commit(1, [append("x"), append("x")], () => { }), "duplicate-record-id", "a repeated ID");
|
|
152
|
+
expectEqual(await store.readHead(), 5, "refused batches change nothing");
|
|
153
|
+
const value = { nested: { n: 1 } };
|
|
154
|
+
await store.commit(1, [put("v", value)], () => { });
|
|
155
|
+
value.nested.n = 2;
|
|
156
|
+
const read = await store.getDocument("v");
|
|
157
|
+
expectEqual(read, { nested: { n: 1 } }, "the store keeps its own copy of written values");
|
|
158
|
+
};
|
|
159
|
+
export const STORE_CASES = new Map([
|
|
160
|
+
["STORE-01", commitAdoptsOnDurable],
|
|
161
|
+
["STORE-02", cutAtEveryWritePoint],
|
|
162
|
+
["STORE-03", concurrentAdmissions],
|
|
163
|
+
["STORE-04", delayedOldOwner],
|
|
164
|
+
["STORE-05", claimAndImport],
|
|
165
|
+
["STORE-06", idsScansAndDocuments],
|
|
166
|
+
["STORE-07", claimRaisesEpochByOne],
|
|
167
|
+
]);
|
|
168
|
+
//# sourceMappingURL=store-cases.js.map
|