@code-yeongyu/senpi-agent-core 2026.10.27-2 → 2026.10.30-2
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 +26 -7
- package/dist/agent.d.ts +3 -0
- package/dist/agent.js +2 -0
- 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/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/stream-keepalive.d.ts +28 -0
- package/dist/stream-keepalive.js +42 -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 +6 -0
- package/package.json +11 -3
|
@@ -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
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { DurableStore, DurableStoreErrorCode, DurableStoreOptions, StoreSnapshot, StoreWritePoint } from "../store.ts";
|
|
2
|
+
/** What a host provides to the STORE and KILL-STORE cases. */
|
|
3
|
+
export interface StoreConformanceAdapter {
|
|
4
|
+
/** A new, empty durable medium (a Durable Object's storage, an SQLite file); its name opens it again. */
|
|
5
|
+
createMedium(): Promise<string>;
|
|
6
|
+
/** A store instance over `medium`. Instances opened on one medium share its durable state. */
|
|
7
|
+
open(medium: string, options?: DurableStoreOptions): Promise<DurableStore>;
|
|
8
|
+
}
|
|
9
|
+
export interface StoreCaseAdapters {
|
|
10
|
+
readonly store: StoreConformanceAdapter;
|
|
11
|
+
}
|
|
12
|
+
export declare function cutOnceAt(target: StoreWritePoint): DurableStoreOptions;
|
|
13
|
+
export declare function expectEqual(actual: unknown, expected: unknown, what: string): void;
|
|
14
|
+
export declare function expectStoreError(operation: Promise<unknown>, code: DurableStoreErrorCode, what: string): Promise<void>;
|
|
15
|
+
export declare function expectCut(operation: Promise<unknown>, what: string): Promise<void>;
|
|
16
|
+
export declare function reopenSnapshot(adapter: StoreConformanceAdapter, medium: string): Promise<StoreSnapshot>;
|
|
17
|
+
export declare function adoptLog(): {
|
|
18
|
+
readonly calls: string[];
|
|
19
|
+
adopt: (result: {
|
|
20
|
+
readonly seqs: readonly number[];
|
|
21
|
+
}) => void;
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=store-harness.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { DurableStoreError } from "../store.js";
|
|
2
|
+
class CutSignal extends Error {
|
|
3
|
+
constructor(point) {
|
|
4
|
+
super(`cut at ${JSON.stringify(point)}`);
|
|
5
|
+
this.name = "CutSignal";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
const samePoint = (a, b) => a.op === b.op && a.at === b.at && (a.at !== "write" || (b.at === "write" && a.index === b.index));
|
|
9
|
+
export function cutOnceAt(target) {
|
|
10
|
+
let armed = true;
|
|
11
|
+
return {
|
|
12
|
+
onWritePoint: (point) => {
|
|
13
|
+
if (armed && samePoint(point, target)) {
|
|
14
|
+
armed = false;
|
|
15
|
+
throw new CutSignal(point);
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function canonical(value) {
|
|
21
|
+
if (Array.isArray(value))
|
|
22
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
23
|
+
if (value !== null && typeof value === "object") {
|
|
24
|
+
const entries = Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
25
|
+
return `{${entries.map(([key, inner]) => `${JSON.stringify(key)}:${canonical(inner)}`).join(",")}}`;
|
|
26
|
+
}
|
|
27
|
+
return JSON.stringify(value) ?? "undefined";
|
|
28
|
+
}
|
|
29
|
+
export function expectEqual(actual, expected, what) {
|
|
30
|
+
const a = canonical(actual);
|
|
31
|
+
const e = canonical(expected);
|
|
32
|
+
if (a !== e)
|
|
33
|
+
throw new Error(`${what}: expected ${e}, got ${a}`);
|
|
34
|
+
}
|
|
35
|
+
export async function expectStoreError(operation, code, what) {
|
|
36
|
+
try {
|
|
37
|
+
await operation;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof DurableStoreError && error.code === code)
|
|
41
|
+
return;
|
|
42
|
+
throw new Error(`${what}: expected DurableStoreError ${code}, got ${String(error)}`);
|
|
43
|
+
}
|
|
44
|
+
throw new Error(`${what}: expected DurableStoreError ${code}, but it resolved`);
|
|
45
|
+
}
|
|
46
|
+
export async function expectCut(operation, what) {
|
|
47
|
+
try {
|
|
48
|
+
await operation;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error instanceof CutSignal)
|
|
52
|
+
return;
|
|
53
|
+
throw new Error(`${what}: expected the cut to surface, got ${String(error)}`);
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`${what}: expected the cut to surface, but it resolved`);
|
|
56
|
+
}
|
|
57
|
+
export async function reopenSnapshot(adapter, medium) {
|
|
58
|
+
return (await adapter.open(medium)).exportSnapshot();
|
|
59
|
+
}
|
|
60
|
+
export function adoptLog() {
|
|
61
|
+
const calls = [];
|
|
62
|
+
return { calls, adopt: (result) => void calls.push(`adopt:${result.seqs.join(",")}`) };
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=store-harness.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CaseImplementation } from "../../conformance/runner.ts";
|
|
2
|
+
import { type WakeCaseAdapters } from "./wake-harness.ts";
|
|
3
|
+
type WakeCase = CaseImplementation<WakeCaseAdapters>;
|
|
4
|
+
export declare const WAKE_CASES: ReadonlyMap<string, WakeCase>;
|
|
5
|
+
export {};
|
|
6
|
+
//# sourceMappingURL=wake-cases.d.ts.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { expectEqual } from "./store-harness.js";
|
|
2
|
+
import { createSlot, HEARTBEAT_MS, restartHost } from "./wake-harness.js";
|
|
3
|
+
const TEN_MINUTES = 600_000;
|
|
4
|
+
const preArm = async (adapters) => {
|
|
5
|
+
const slot = await createSlot(adapters);
|
|
6
|
+
const session = await slot.instance();
|
|
7
|
+
expectEqual(slot.trace, [`set:+${HEARTBEAT_MS}`, "resume", "clear"], "start arms a fallback before it resumes");
|
|
8
|
+
slot.trace.length = 0;
|
|
9
|
+
await session.host.admit(async () => {
|
|
10
|
+
slot.trace.push(`work alarm=+${await slot.alarmAt()}`);
|
|
11
|
+
});
|
|
12
|
+
expectEqual(slot.trace, [`set:+${HEARTBEAT_MS}`, `work alarm=+${HEARTBEAT_MS}`, "clear"], "admit arms durably before work starts, and arms from the idle loop's wake when the work ends");
|
|
13
|
+
const failing = session.host.admit(async () => {
|
|
14
|
+
throw new Error("work failed");
|
|
15
|
+
});
|
|
16
|
+
await failing.then(() => {
|
|
17
|
+
throw new Error("a failing admission resolved");
|
|
18
|
+
}, () => undefined);
|
|
19
|
+
expectEqual(await slot.alarmAt(), HEARTBEAT_MS, "work that fails leaves the recovery alarm armed");
|
|
20
|
+
await session.host.admit(() => session.loop.schedule("late", slot.base + TEN_MINUTES));
|
|
21
|
+
slot.kill();
|
|
22
|
+
slot.trace.length = 0;
|
|
23
|
+
await slot.instance();
|
|
24
|
+
expectEqual(slot.trace, [`set:+${HEARTBEAT_MS}`, "resume", `set:+${TEN_MINUTES}`], "a new instance lowers a later alarm to the fallback before resuming");
|
|
25
|
+
await (await slot.instance()).host.update({ active: false, at: slot.base + 30_000 });
|
|
26
|
+
slot.kill();
|
|
27
|
+
slot.trace.length = 0;
|
|
28
|
+
await slot.instance();
|
|
29
|
+
expectEqual(slot.trace, ["resume", `set:+${TEN_MINUTES}`], "an earlier alarm is kept as the fallback");
|
|
30
|
+
};
|
|
31
|
+
const update = async (adapters) => {
|
|
32
|
+
const slot = await createSlot(adapters);
|
|
33
|
+
const { host, loop } = await slot.instance();
|
|
34
|
+
const schedule = (id, offset) => host.admit(() => loop.schedule(id, slot.base + offset));
|
|
35
|
+
slot.trace.length = 0;
|
|
36
|
+
await schedule("a", TEN_MINUTES);
|
|
37
|
+
expectEqual(slot.trace, [`set:+${HEARTBEAT_MS}`, `set:+${HEARTBEAT_MS}`, `set:+${TEN_MINUTES}`], "the commit's update keeps the heartbeat while the admission is in flight; after it, the alarm is the wake");
|
|
38
|
+
await schedule("b", 300_000);
|
|
39
|
+
expectEqual(await slot.alarmAt(), 300_000, "an earlier wake lowers the alarm");
|
|
40
|
+
await schedule("c", 900_000);
|
|
41
|
+
expectEqual(await slot.alarmAt(), 300_000, "a later wake never raises it past the earliest");
|
|
42
|
+
await host.update({ active: true, at: slot.base + 300_000 });
|
|
43
|
+
expectEqual(await slot.alarmAt(), HEARTBEAT_MS, "while active, a heartbeat earlier than the wake");
|
|
44
|
+
await host.update({ active: true, at: slot.base + 30_000 });
|
|
45
|
+
expectEqual(await slot.alarmAt(), 30_000, "never later than the earliest wake");
|
|
46
|
+
await host.update({ active: false });
|
|
47
|
+
expectEqual(await slot.alarmAt(), undefined, "cleared only when the loop is idle");
|
|
48
|
+
expectEqual(await slot.ran(), [], "updates run nothing");
|
|
49
|
+
};
|
|
50
|
+
const resume = async (adapters) => {
|
|
51
|
+
const slot = await createSlot(adapters);
|
|
52
|
+
const { host, loop } = await slot.instance();
|
|
53
|
+
for (const [id, offset] of [
|
|
54
|
+
["a", 120_000],
|
|
55
|
+
["b", 120_000],
|
|
56
|
+
["c", TEN_MINUTES],
|
|
57
|
+
]) {
|
|
58
|
+
await host.admit(() => loop.schedule(id, slot.base + offset));
|
|
59
|
+
}
|
|
60
|
+
slot.trace.length = 0;
|
|
61
|
+
await adapters.wake.advanceTo(slot.base + 120_000);
|
|
62
|
+
expectEqual(slot.fires(), 1, "one fire at the wake");
|
|
63
|
+
expectEqual(slot.trace.slice(0, 2), [`set:+${120_000 + HEARTBEAT_MS}`, "resume"], "a fire re-arms before resume");
|
|
64
|
+
expectEqual(await slot.ran(), ["a", "b"], "the due tasks run once, in order");
|
|
65
|
+
expectEqual(await slot.alarmAt(), TEN_MINUTES, "then the alarm is the next wake");
|
|
66
|
+
await host.update({ active: true });
|
|
67
|
+
await adapters.wake.advanceTo(slot.base + 120_000 + HEARTBEAT_MS);
|
|
68
|
+
expectEqual(await slot.ran(), ["a", "b"], "an early alarm runs nothing");
|
|
69
|
+
expectEqual(await slot.alarmAt(), TEN_MINUTES, "and re-arms from the loop's wake");
|
|
70
|
+
await adapters.wake.advanceTo(slot.base + TEN_MINUTES);
|
|
71
|
+
expectEqual(await slot.ran(), ["a", "b", "c"], "the last task runs at its wake");
|
|
72
|
+
expectEqual(await slot.alarmAt(), undefined, "an idle loop leaves no alarm");
|
|
73
|
+
expectEqual(slot.fires(), 3, "no fire beyond the alarms armed");
|
|
74
|
+
};
|
|
75
|
+
const retention = async (adapters) => {
|
|
76
|
+
const slot = await createSlot(adapters);
|
|
77
|
+
const { host, loop } = await slot.instance();
|
|
78
|
+
await host.admit(() => loop.schedule("a", slot.base + TEN_MINUTES));
|
|
79
|
+
await restartHost(adapters, [slot]);
|
|
80
|
+
expectEqual(await slot.alarmAt(), TEN_MINUTES, "the alarm is durable across a host restart");
|
|
81
|
+
await slot.instance();
|
|
82
|
+
slot.kill();
|
|
83
|
+
await slot.instance();
|
|
84
|
+
expectEqual(await slot.alarmAt(), TEN_MINUTES, "reopened instances keep it");
|
|
85
|
+
await adapters.wake.advanceTo(slot.base + TEN_MINUTES - 1);
|
|
86
|
+
expectEqual(slot.fires(), 0, "nothing fires early, whatever instances came and went");
|
|
87
|
+
await adapters.wake.advanceTo(slot.base + TEN_MINUTES);
|
|
88
|
+
expectEqual(slot.fires(), 1, "it fires once");
|
|
89
|
+
expectEqual(await slot.ran(), ["a"], "the task runs once");
|
|
90
|
+
expectEqual(await slot.alarmAt(), undefined, "and the alarm is not retained after the work");
|
|
91
|
+
await adapters.wake.advanceTo(slot.base + 3 * TEN_MINUTES);
|
|
92
|
+
expectEqual(slot.fires(), 1, "a consumed alarm never fires again");
|
|
93
|
+
};
|
|
94
|
+
export const WAKE_CASES = new Map([
|
|
95
|
+
["WAKE-01", preArm],
|
|
96
|
+
["WAKE-02", update],
|
|
97
|
+
["WAKE-03", resume],
|
|
98
|
+
["WAKE-RET-01", retention],
|
|
99
|
+
]);
|
|
100
|
+
//# sourceMappingURL=wake-cases.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { DurableStore, DurableStoreOptions, StoreWritePoint } from "../store.ts";
|
|
2
|
+
import { type WakeAlarm, type WakeAlarmOptions, type WakeAlarmPoint, type WakeHost, type WakePoint } from "../wake-host.ts";
|
|
3
|
+
import type { StoreCaseAdapters } from "./store-harness.ts";
|
|
4
|
+
import { type TestLoop } from "./wake-test-loop.ts";
|
|
5
|
+
/** The session a host's alarm belongs to. Hosts that persist the alarm in the store use `store` under `epoch`. */
|
|
6
|
+
export interface WakeAlarmSession {
|
|
7
|
+
readonly medium: string;
|
|
8
|
+
readonly store: DurableStore;
|
|
9
|
+
readonly epoch: number;
|
|
10
|
+
readonly onFire: () => Promise<void>;
|
|
11
|
+
readonly options?: WakeAlarmOptions;
|
|
12
|
+
}
|
|
13
|
+
/** What a host provides to the WAKE, KILL-WAKE and MULTI-HOST cases, next to its store adapter. */
|
|
14
|
+
export interface WakeConformanceAdapter {
|
|
15
|
+
/** The host clock, epoch milliseconds; only `advanceTo` moves it. */
|
|
16
|
+
now(): number;
|
|
17
|
+
/** Move the clock to `at`, delivering every alarm that falls due on the way, in time order, each awaited. */
|
|
18
|
+
advanceTo(at: number): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* The durable alarm of one session. Opening it again (a new instance after a kill) replaces the earlier instance:
|
|
21
|
+
* fires go to the newest `onFire` only. The armed time outlives instances and host restarts.
|
|
22
|
+
*/
|
|
23
|
+
openAlarm(session: WakeAlarmSession): Promise<WakeAlarm>;
|
|
24
|
+
/** The host process restarts: instances and in-memory timers are lost, durable alarms are kept. */
|
|
25
|
+
restart(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export interface WakeCaseAdapters extends StoreCaseAdapters {
|
|
28
|
+
readonly wake: WakeConformanceAdapter;
|
|
29
|
+
}
|
|
30
|
+
export declare const HEARTBEAT_MS = 60000;
|
|
31
|
+
export declare const cutStore: (op: StoreWritePoint["op"], at: StoreWritePoint["at"], nth?: number) => DurableStoreOptions;
|
|
32
|
+
export declare const cutAlarm: (op: WakeAlarmPoint["op"], at: WakeAlarmPoint["at"], nth?: number) => WakeAlarmOptions;
|
|
33
|
+
export declare const cutWake: (op: WakePoint["op"], nth?: number) => ((point: WakePoint) => void);
|
|
34
|
+
export declare function expectWakeCut(operation: Promise<unknown>, what: string): Promise<void>;
|
|
35
|
+
export interface SessionInstance {
|
|
36
|
+
readonly loop: TestLoop;
|
|
37
|
+
readonly host: WakeHost;
|
|
38
|
+
}
|
|
39
|
+
export interface BootOptions {
|
|
40
|
+
readonly store?: DurableStoreOptions;
|
|
41
|
+
readonly alarm?: WakeAlarmOptions;
|
|
42
|
+
readonly onWakePoint?: (point: WakePoint) => void;
|
|
43
|
+
}
|
|
44
|
+
/** One session on the host: its medium, its live instance (if any), and what the cases observe. */
|
|
45
|
+
export interface SessionSlot {
|
|
46
|
+
/** The clock time the slot was created at; trace times are relative to it. */
|
|
47
|
+
readonly base: number;
|
|
48
|
+
/** Durable alarm writes (`set:+ms` / `clear`) and `resume` calls, in order, across instances. */
|
|
49
|
+
readonly trace: string[];
|
|
50
|
+
/** The live instance; with none, boot one (open + `start`) with `options`. */
|
|
51
|
+
instance(options?: BootOptions): Promise<SessionInstance>;
|
|
52
|
+
/** Drop the live instance, as an eviction or crash does. */
|
|
53
|
+
kill(): void;
|
|
54
|
+
/** Alarm fires delivered to this session. */
|
|
55
|
+
fires(): number;
|
|
56
|
+
/** Task IDs the loop committed as run, in commit order. */
|
|
57
|
+
ran(): Promise<string[]>;
|
|
58
|
+
/** The durable alarm, relative to `base`; undefined when cleared. */
|
|
59
|
+
alarmAt(): Promise<number | undefined>;
|
|
60
|
+
}
|
|
61
|
+
export declare function createSlot(adapters: WakeCaseAdapters, epoch?: number): Promise<SessionSlot>;
|
|
62
|
+
/** Restart the host process: every slot loses its live instance; durable state stays. */
|
|
63
|
+
export declare function restartHost(adapters: WakeCaseAdapters, slots: readonly SessionSlot[]): Promise<void>;
|
|
64
|
+
//# sourceMappingURL=wake-harness.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createWakeHost, } from "../wake-host.js";
|
|
2
|
+
import { openTestLoop } from "./wake-test-loop.js";
|
|
3
|
+
export const HEARTBEAT_MS = 60_000;
|
|
4
|
+
/** A simulated kill inside a wake case; every cut in these cases throws it. */
|
|
5
|
+
class WakeCut extends Error {
|
|
6
|
+
constructor(where) {
|
|
7
|
+
super(`cut at ${where}`);
|
|
8
|
+
this.name = "WakeCut";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const isCut = (error) => error instanceof WakeCut;
|
|
12
|
+
/** Throw a WakeCut the `nth` time `matches` sees a point (1-based). */
|
|
13
|
+
function cutNth(matches, nth) {
|
|
14
|
+
let seen = 0;
|
|
15
|
+
return (point) => {
|
|
16
|
+
if (matches(point) && ++seen === nth)
|
|
17
|
+
throw new WakeCut(JSON.stringify(point));
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export const cutStore = (op, at, nth = 1) => ({
|
|
21
|
+
onWritePoint: cutNth((point) => point.op === op && point.at === at, nth),
|
|
22
|
+
});
|
|
23
|
+
export const cutAlarm = (op, at, nth = 1) => ({
|
|
24
|
+
onAlarmPoint: cutNth((point) => point.op === op && point.at === at, nth),
|
|
25
|
+
});
|
|
26
|
+
export const cutWake = (op, nth = 1) => cutNth((point) => point.op === op, nth);
|
|
27
|
+
export async function expectWakeCut(operation, what) {
|
|
28
|
+
try {
|
|
29
|
+
await operation;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (isCut(error))
|
|
33
|
+
return;
|
|
34
|
+
throw new Error(`${what}: expected the cut to surface, got ${String(error)}`);
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`${what}: expected the cut to surface, but it resolved`);
|
|
37
|
+
}
|
|
38
|
+
export async function createSlot(adapters, epoch = 1) {
|
|
39
|
+
const { store: storeAdapter, wake } = adapters;
|
|
40
|
+
const medium = await storeAdapter.createMedium();
|
|
41
|
+
const reader = await storeAdapter.open(medium);
|
|
42
|
+
for (let next = 1; next <= epoch; next++)
|
|
43
|
+
await reader.claim(next - 1, next);
|
|
44
|
+
const base = wake.now();
|
|
45
|
+
const trace = [];
|
|
46
|
+
let live;
|
|
47
|
+
let alarm;
|
|
48
|
+
let fires = 0;
|
|
49
|
+
const record = (options) => ({
|
|
50
|
+
onAlarmPoint: (point) => {
|
|
51
|
+
options?.onAlarmPoint?.(point);
|
|
52
|
+
if (point.at === "durable")
|
|
53
|
+
trace.push(point.op === "clear" ? "clear" : `set:+${(point.time ?? 0) - base}`);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
async function boot(options = {}) {
|
|
57
|
+
const store = await storeAdapter.open(medium, options.store);
|
|
58
|
+
let host;
|
|
59
|
+
const loop = await openTestLoop(store, epoch, wake.now, async () => host?.update(loop.wake()));
|
|
60
|
+
alarm = await wake.openAlarm({
|
|
61
|
+
medium,
|
|
62
|
+
store: await storeAdapter.open(medium),
|
|
63
|
+
epoch,
|
|
64
|
+
onFire: dispatch,
|
|
65
|
+
options: record(options.alarm),
|
|
66
|
+
});
|
|
67
|
+
host = createWakeHost({
|
|
68
|
+
alarm,
|
|
69
|
+
now: wake.now,
|
|
70
|
+
wake: () => loop.wake(),
|
|
71
|
+
resume: async () => {
|
|
72
|
+
trace.push("resume");
|
|
73
|
+
await loop.resume();
|
|
74
|
+
},
|
|
75
|
+
heartbeatMs: HEARTBEAT_MS,
|
|
76
|
+
onWakePoint: options.onWakePoint,
|
|
77
|
+
});
|
|
78
|
+
const instance = { loop, host };
|
|
79
|
+
live = instance;
|
|
80
|
+
await host.start();
|
|
81
|
+
return instance;
|
|
82
|
+
}
|
|
83
|
+
async function dispatch() {
|
|
84
|
+
fires++;
|
|
85
|
+
try {
|
|
86
|
+
const instance = live ?? (await boot());
|
|
87
|
+
await instance.host.fire();
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (!isCut(error))
|
|
91
|
+
throw error;
|
|
92
|
+
live = undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
base,
|
|
97
|
+
trace,
|
|
98
|
+
async instance(options) {
|
|
99
|
+
if (live !== undefined)
|
|
100
|
+
return live;
|
|
101
|
+
try {
|
|
102
|
+
return await boot(options);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (isCut(error))
|
|
106
|
+
live = undefined;
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
kill: () => {
|
|
111
|
+
live = undefined;
|
|
112
|
+
},
|
|
113
|
+
fires: () => fires,
|
|
114
|
+
ran: async () => (await reader.scanRecords({ kind: "ran" })).map((record) => record.id.slice("ran:".length)),
|
|
115
|
+
alarmAt: async () => {
|
|
116
|
+
const at = await alarm?.read();
|
|
117
|
+
return at === undefined ? undefined : at - base;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** Restart the host process: every slot loses its live instance; durable state stays. */
|
|
122
|
+
export async function restartHost(adapters, slots) {
|
|
123
|
+
await adapters.wake.restart();
|
|
124
|
+
for (const slot of slots)
|
|
125
|
+
slot.kill();
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=wake-harness.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DurableStore } from "../store.ts";
|
|
2
|
+
import type { Wake } from "../wake-host.ts";
|
|
3
|
+
/** A minimal loop over the store: tasks due at a time, each run in one commit. Enough to observe wake ordering. */
|
|
4
|
+
export interface TestLoop {
|
|
5
|
+
/** Schedule task `id` at `due`, in one commit. */
|
|
6
|
+
schedule(id: string, due: number): Promise<void>;
|
|
7
|
+
/** Run every due task, one commit each: a `ran` record and the task's removal, atomically. */
|
|
8
|
+
resume(): Promise<void>;
|
|
9
|
+
/** `active` while a task is due now; `at`, the earliest task due later. */
|
|
10
|
+
wake(): Wake;
|
|
11
|
+
}
|
|
12
|
+
export declare function openTestLoop(store: DurableStore, epoch: number, now: () => number, committed: () => Promise<void>): Promise<TestLoop>;
|
|
13
|
+
//# sourceMappingURL=wake-test-loop.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createAdmissionLine } from "../store-admission.js";
|
|
2
|
+
const TASK_PREFIX = "task/";
|
|
3
|
+
export async function openTestLoop(store, epoch, now, committed) {
|
|
4
|
+
const line = createAdmissionLine();
|
|
5
|
+
const tasks = new Map();
|
|
6
|
+
for (const document of await store.scanDocuments(TASK_PREFIX)) {
|
|
7
|
+
if (typeof document.data === "number")
|
|
8
|
+
tasks.set(document.key.slice(TASK_PREFIX.length), document.data);
|
|
9
|
+
}
|
|
10
|
+
const commit = async (writes, adopt) => {
|
|
11
|
+
await store.commit(epoch, writes, adopt);
|
|
12
|
+
await committed();
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
schedule: (id, due) => line.admit(() => commit([{ op: "put", key: TASK_PREFIX + id, data: due }], () => void tasks.set(id, due))),
|
|
16
|
+
resume: () => line.admit(async () => {
|
|
17
|
+
const due = [...tasks].filter(([, at]) => at <= now()).sort(([a, x], [b, y]) => x - y || (a < b ? -1 : 1));
|
|
18
|
+
for (const [id] of due) {
|
|
19
|
+
const writes = [
|
|
20
|
+
{ op: "append", id: `ran:${id}`, kind: "ran", data: { id } },
|
|
21
|
+
{ op: "delete", key: TASK_PREFIX + id },
|
|
22
|
+
];
|
|
23
|
+
await commit(writes, () => void tasks.delete(id));
|
|
24
|
+
}
|
|
25
|
+
}),
|
|
26
|
+
wake: () => {
|
|
27
|
+
const time = now();
|
|
28
|
+
let active = false;
|
|
29
|
+
let at;
|
|
30
|
+
for (const due of tasks.values()) {
|
|
31
|
+
if (due <= time)
|
|
32
|
+
active = true;
|
|
33
|
+
else if (at === undefined || due < at)
|
|
34
|
+
at = due;
|
|
35
|
+
}
|
|
36
|
+
return at === undefined ? { active } : { active, at };
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=wake-test-loop.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { WakeAlarm, WakeAlarmOptions } from "./wake-host.ts";
|
|
2
|
+
import type { AlarmPersistence } from "./wake-host-store-alarm.ts";
|
|
3
|
+
/** Schedule `callback` after `ms`; returns its cancel. */
|
|
4
|
+
export type SetTimer = (callback: () => unknown, ms: number) => () => void;
|
|
5
|
+
export interface BunTimerHostOptions {
|
|
6
|
+
readonly now?: () => number;
|
|
7
|
+
readonly setTimer?: SetTimer;
|
|
8
|
+
/**
|
|
9
|
+
* The longest single timer. Monotonic timers stop while a machine sleeps, so a far alarm re-checks the wall clock
|
|
10
|
+
* at least this often and an alarm that fell due during a sleep fires within this bound of waking. Default 60 s.
|
|
11
|
+
*/
|
|
12
|
+
readonly maxDelayMs?: number;
|
|
13
|
+
/** A fire handler rejected. The alarm stays armed and fires again after `maxDelayMs`. */
|
|
14
|
+
readonly onFireError: (key: string, error: unknown) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The Bun process timer host: one instance serves every session of the process, each under its own key, with its
|
|
18
|
+
* alarm persisted (normally `storeAlarmPersistence`). After a process restart the embedding host opens its sessions'
|
|
19
|
+
* alarms again; an alarm that fell due meanwhile fires at once.
|
|
20
|
+
*/
|
|
21
|
+
export interface BunTimerHost {
|
|
22
|
+
/**
|
|
23
|
+
* The alarm of session `key`, read from `persistence`. Opening a key again replaces the earlier instance: fires go
|
|
24
|
+
* to the newest `onFire`, and the replaced instance's writes are refused.
|
|
25
|
+
*/
|
|
26
|
+
openAlarm(key: string, persistence: AlarmPersistence, onFire: () => Promise<void>, options?: WakeAlarmOptions): Promise<WakeAlarm>;
|
|
27
|
+
/** Cancel every timer. Persisted alarms stay. */
|
|
28
|
+
close(): void;
|
|
29
|
+
}
|
|
30
|
+
export declare function createBunTimerHost(options: BunTimerHostOptions): BunTimerHost;
|
|
31
|
+
//# sourceMappingURL=wake-host-bun-timer.d.ts.map
|