@usagekit/store 0.1.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.
Files changed (47) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +2 -0
  3. package/dist/clock.d.ts +8 -0
  4. package/dist/clock.js +12 -0
  5. package/dist/conformance/admission.d.ts +2 -0
  6. package/dist/conformance/admission.js +124 -0
  7. package/dist/conformance/authority.d.ts +2 -0
  8. package/dist/conformance/authority.js +285 -0
  9. package/dist/conformance/capabilities.d.ts +2 -0
  10. package/dist/conformance/capabilities.js +66 -0
  11. package/dist/conformance/dispatch.d.ts +2 -0
  12. package/dist/conformance/dispatch.js +80 -0
  13. package/dist/conformance/expiration.d.ts +2 -0
  14. package/dist/conformance/expiration.js +131 -0
  15. package/dist/conformance/factory.d.ts +17 -0
  16. package/dist/conformance/factory.js +1 -0
  17. package/dist/conformance/helpers.d.ts +18 -0
  18. package/dist/conformance/helpers.js +49 -0
  19. package/dist/conformance/index.d.ts +4 -0
  20. package/dist/conformance/index.js +2 -0
  21. package/dist/conformance/properties.d.ts +2 -0
  22. package/dist/conformance/properties.js +220 -0
  23. package/dist/conformance/reads.d.ts +2 -0
  24. package/dist/conformance/reads.js +181 -0
  25. package/dist/conformance/recovery.d.ts +2 -0
  26. package/dist/conformance/recovery.js +126 -0
  27. package/dist/conformance/reserve.d.ts +2 -0
  28. package/dist/conformance/reserve.js +94 -0
  29. package/dist/conformance/scaling.d.ts +13 -0
  30. package/dist/conformance/scaling.js +67 -0
  31. package/dist/conformance/suite.d.ts +2 -0
  32. package/dist/conformance/suite.js +27 -0
  33. package/dist/index.d.ts +31 -0
  34. package/dist/index.js +3 -0
  35. package/dist/memory/admission.d.ts +44 -0
  36. package/dist/memory/admission.js +105 -0
  37. package/dist/memory/commands.d.ts +22 -0
  38. package/dist/memory/commands.js +249 -0
  39. package/dist/memory/memory-store.d.ts +7 -0
  40. package/dist/memory/memory-store.js +29 -0
  41. package/dist/memory/reads.d.ts +5 -0
  42. package/dist/memory/reads.js +131 -0
  43. package/dist/memory/state.d.ts +42 -0
  44. package/dist/memory/state.js +48 -0
  45. package/dist/reference.d.ts +6 -0
  46. package/dist/reference.js +5 -0
  47. package/package.json +53 -0
@@ -0,0 +1,66 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { input, ref, command, budget, quantity } from "./helpers.js";
3
+ export function capabilityTests(factory, caps) {
4
+ describe("capability guarantees", () => {
5
+ const durable = caps.durable ? test : test.skip;
6
+ durable("requires durable", async () => {
7
+ const f = await factory();
8
+ try {
9
+ const r = await f.store.reserve(input());
10
+ if (r.outcome !== "reserved")
11
+ throw new Error("fixture");
12
+ const g = await f.store.markDispatchIntent({
13
+ ...command(r.operation),
14
+ holder: "process1",
15
+ leaseTtlMs: 1000,
16
+ });
17
+ expect(g).toMatchObject({ granted: true });
18
+ if (!f.restart)
19
+ throw new Error("durable factory requires restart");
20
+ const restarted = await f.restart();
21
+ f.clock.advance(1000);
22
+ const op = await restarted.getOperation(ref(r.operation));
23
+ expect(op?.state).toBe("dispatch_intended");
24
+ const recovery = await restarted.claimForRecovery({
25
+ ...ref(r.operation),
26
+ holder: "process2",
27
+ leaseTtlMs: 1000,
28
+ });
29
+ expect(recovery).toMatchObject({ claimed: true });
30
+ expect(await restarted.markDispatchIntent({
31
+ ...command(op),
32
+ holder: "process2",
33
+ leaseTtlMs: 1000,
34
+ })).toMatchObject({ granted: false });
35
+ }
36
+ finally {
37
+ await f.close();
38
+ }
39
+ });
40
+ const rolling = caps.rollingWindows ? test : test.skip;
41
+ rolling("requires rollingWindows", async () => {
42
+ const f = await factory();
43
+ try {
44
+ f.budgets.push(budget({ limit: quantity(1n), window: { kind: "rolling", days: 1 } }));
45
+ expect(await f.store.reserve(input())).toMatchObject({ outcome: "reserved" });
46
+ f.clock.advance(86400000 - 1);
47
+ expect(await f.store.reserve(input())).toMatchObject({ outcome: "exceeded" });
48
+ f.clock.advance(1);
49
+ expect(await f.store.reserve(input())).toMatchObject({ outcome: "reserved" });
50
+ }
51
+ finally {
52
+ await f.close();
53
+ }
54
+ });
55
+ test("quantity precision obeys advertised capability", async () => {
56
+ const f = await factory();
57
+ try {
58
+ expect(await f.store.reserve(input({ estimate: [quantity(1n, "units", caps.maxQuantityScale)] }))).toMatchObject({ outcome: "reserved" });
59
+ await expect(f.store.reserve(input({ estimate: [quantity(1n, "units", caps.maxQuantityScale + 1)] }))).rejects.toThrow("InvalidInput");
60
+ }
61
+ finally {
62
+ await f.close();
63
+ }
64
+ });
65
+ });
66
+ }
@@ -0,0 +1,2 @@
1
+ import type { StoreFactory } from "./factory.js";
2
+ export declare function dispatchTests(factory: StoreFactory): void;
@@ -0,0 +1,80 @@
1
+ import { beforeEach, afterEach, describe, expect, test } from "vitest";
2
+ import { input, command, ref } from "./helpers.js";
3
+ export function dispatchTests(factory) {
4
+ describe("dispatch and leases", () => {
5
+ let f;
6
+ beforeEach(async () => {
7
+ f = await factory();
8
+ });
9
+ afterEach(async () => f.close());
10
+ const reserved = async () => {
11
+ const r = await f.store.reserve(input());
12
+ if (r.outcome !== "reserved")
13
+ throw new Error("fixture");
14
+ return r.operation;
15
+ };
16
+ test("first intent, identical and different replay never grant again", async () => {
17
+ const op = await reserved();
18
+ const cmd = { ...command(op), holder: "h1", leaseTtlMs: 1000 };
19
+ const grant = await f.store.markDispatchIntent(cmd);
20
+ expect(grant).toMatchObject({
21
+ granted: true,
22
+ operation: { state: "dispatch_intended", version: 2 },
23
+ lease: { holder: "h1", expiresAt: "2026-09-23T12:00:01.000Z" },
24
+ });
25
+ for (const commandId of [cmd.commandId, crypto.randomUUID()])
26
+ expect(await f.store.markDispatchIntent({ ...cmd, commandId })).toMatchObject({
27
+ granted: false,
28
+ reason: "already_dispatched",
29
+ });
30
+ });
31
+ test("two concurrent intents grant once", async () => {
32
+ const op = await reserved();
33
+ const results = await Promise.all(["a", "b"].map((holder) => f.store.markDispatchIntent({ ...command(op), holder, leaseTtlMs: 1000 })));
34
+ expect(results.filter((r) => "granted" in r && r.granted)).toHaveLength(1);
35
+ });
36
+ test("version conflict leaves state unchanged; unknown operation is representable", async () => {
37
+ const op = await reserved();
38
+ expect(await f.store.markDispatchIntent({
39
+ ...command(op),
40
+ expectedVersion: 0,
41
+ holder: "h",
42
+ leaseTtlMs: 1,
43
+ })).toMatchObject({ granted: false, reason: "version_conflict" });
44
+ expect(await f.store.getOperation(ref(op))).toEqual(op);
45
+ expect(await f.store.markDispatchIntent({
46
+ ...command(op),
47
+ operationId: "missing",
48
+ holder: "h",
49
+ leaseTtlMs: 1,
50
+ })).toEqual({ granted: false, reason: "not_reserved", operation: null });
51
+ });
52
+ test("renewal retains version/id; foreign, expired and missing fail", async () => {
53
+ const op = await reserved();
54
+ const g = await f.store.markDispatchIntent({ ...command(op), holder: "h", leaseTtlMs: 1000 });
55
+ if (!("granted" in g) || !g.granted)
56
+ throw new Error("fixture");
57
+ const renew = { ...ref(op), leaseId: g.lease.leaseId, leaseTtlMs: 2000 };
58
+ f.clock.advance(500);
59
+ expect(await f.store.renewLease(renew)).toMatchObject({
60
+ renewed: true,
61
+ lease: { leaseId: g.lease.leaseId, expiresAt: "2026-09-23T12:00:02.500Z" },
62
+ });
63
+ expect((await f.store.getOperation(ref(op)))?.version).toBe(2);
64
+ expect(await f.store.renewLease({ ...renew, leaseId: "foreign" })).toEqual({
65
+ renewed: false,
66
+ reason: "not_holder",
67
+ });
68
+ f.clock.advance(2000);
69
+ expect(await f.store.renewLease(renew)).toEqual({ renewed: false, reason: "expired" });
70
+ expect(await f.store.getOperation(ref(op))).toMatchObject({
71
+ state: "dispatch_intended",
72
+ lease: { leaseId: g.lease.leaseId },
73
+ });
74
+ expect(await f.store.renewLease({ ...renew, operationId: "missing" })).toEqual({
75
+ renewed: false,
76
+ reason: "not_active",
77
+ });
78
+ });
79
+ });
80
+ }
@@ -0,0 +1,2 @@
1
+ import type { StoreFactory } from "./factory.js";
2
+ export declare function expirationTests(factory: StoreFactory): void;
@@ -0,0 +1,131 @@
1
+ import { beforeEach, afterEach, describe, test, expect } from "vitest";
2
+ import { input, budget, quantity, ref, command } from "./helpers.js";
3
+ export function expirationTests(factory) {
4
+ describe("undispatched reservation expiry", () => {
5
+ let f;
6
+ beforeEach(async () => {
7
+ f = await factory();
8
+ });
9
+ afterEach(async () => f.close());
10
+ test("finite default deadline is snapshotted; replay cannot extend it", async () => {
11
+ const i = input(), r = await f.store.reserve(i);
12
+ if (r.outcome !== "reserved")
13
+ throw new Error("fixture");
14
+ expect(r.operation.reservationExpiresAt).toBe(new Date(f.clock.now().getTime() + 300000).toISOString());
15
+ f.clock.advance(1000);
16
+ expect(await f.store.reserve(i)).toEqual({ ...r, replayed: true });
17
+ });
18
+ test("process loss before intent releases headroom on the next reserve", async () => {
19
+ f.budgets.push(budget({ limit: quantity(1n) }));
20
+ const i = input({ reservationTtlMs: 1000 });
21
+ await f.store.reserve(i);
22
+ f.clock.advance(1000);
23
+ expect(await f.store.reserve(input())).toMatchObject({ outcome: "reserved" });
24
+ expect(await f.store.getOperation(ref(i))).toMatchObject({ state: "released", version: 2 });
25
+ expect(await f.store.reserve(i)).toMatchObject({
26
+ replayed: true,
27
+ operation: { state: "released" },
28
+ });
29
+ });
30
+ test("expiry fences intent, sweeps are bounded and repeatable", async () => {
31
+ const i = input({ reservationTtlMs: 1 }), r = await f.store.reserve(i);
32
+ if (r.outcome !== "reserved")
33
+ throw new Error("fixture");
34
+ await f.store.reserve(input({ reservationTtlMs: 1 }));
35
+ await f.store.reserve(input({ reservationTtlMs: 1 }));
36
+ f.clock.advance(1);
37
+ expect(await f.store.markDispatchIntent({
38
+ ...command(r.operation),
39
+ holder: "h",
40
+ leaseTtlMs: 1000,
41
+ })).toMatchObject({
42
+ granted: false,
43
+ reason: "reservation_expired",
44
+ operation: { state: "released", version: 2 },
45
+ });
46
+ expect(await f.store.expireReservations({ namespace: "test", limit: 1 })).toEqual({
47
+ outcome: "expired",
48
+ count: 1,
49
+ hasMore: true,
50
+ });
51
+ expect(await f.store.expireReservations({ namespace: "test", limit: 1 })).toEqual({
52
+ outcome: "expired",
53
+ count: 1,
54
+ hasMore: false,
55
+ });
56
+ expect(await f.store.expireReservations({ namespace: "test" })).toEqual({
57
+ outcome: "expired",
58
+ count: 0,
59
+ hasMore: false,
60
+ });
61
+ });
62
+ test("dispatch intent is never released by reservation expiry, even after its lease expires", async () => {
63
+ f.budgets.push(budget({ limit: quantity(1n) }));
64
+ const i = input({ reservationTtlMs: 1 }), r = await f.store.reserve(i);
65
+ if (r.outcome !== "reserved")
66
+ throw new Error("fixture");
67
+ expect(await f.store.markDispatchIntent({ ...command(r.operation), holder: "h", leaseTtlMs: 10 })).toMatchObject({ granted: true });
68
+ f.clock.advance(11);
69
+ expect(await f.store.expireReservations({ namespace: "test" })).toMatchObject({ count: 0 });
70
+ expect(await f.store.reserve(input())).toMatchObject({ outcome: "exceeded" });
71
+ expect(await f.store.claimForRecovery({ ...ref(i), holder: "recovery", leaseTtlMs: 1000 })).toMatchObject({ claimed: true });
72
+ });
73
+ test("concurrent sweeps release once and keep other namespaces isolated", async () => {
74
+ const i = input({ reservationTtlMs: 1 });
75
+ await f.store.reserve(i);
76
+ const other = {
77
+ ...input({ reservationTtlMs: 1 }),
78
+ scope: { ...i.scope, namespace: "other" },
79
+ };
80
+ await f.store.reserve(other);
81
+ f.clock.advance(1);
82
+ const results = await Promise.all([
83
+ f.store.expireReservations({ namespace: "test" }),
84
+ f.store.expireReservations({ namespace: "test" }),
85
+ ]);
86
+ expect(results.map((r) => ("count" in r ? r.count : -1)).sort()).toEqual([0, 1]);
87
+ expect(await f.store.getOperation(ref(other))).toMatchObject({ state: "reserved" });
88
+ });
89
+ test("admission sweeps at most one default batch of expired reservations", async () => {
90
+ for (let n = 0; n < 105; n++)
91
+ await f.store.reserve(input({ operationId: `expired-${n}`, reservationTtlMs: 1 }));
92
+ f.clock.advance(1);
93
+ await f.store.reserve(input());
94
+ expect(await f.store.expireReservations({ namespace: "test" })).toEqual({
95
+ outcome: "expired",
96
+ count: 5,
97
+ hasMore: false,
98
+ });
99
+ });
100
+ test("expired intent releases its own hold atomically without sweeping another operation", async () => {
101
+ f.budgets.push(budget({ limit: quantity(2n) }));
102
+ const i = input({ reservationTtlMs: 1 }), r = await f.store.reserve(i);
103
+ if (r.outcome !== "reserved")
104
+ throw new Error("fixture");
105
+ const other = input({ reservationTtlMs: 1 });
106
+ await f.store.reserve(other);
107
+ f.clock.advance(1);
108
+ expect(await f.store.markDispatchIntent({
109
+ ...command(r.operation),
110
+ holder: "h",
111
+ leaseTtlMs: 1000,
112
+ })).toMatchObject({
113
+ granted: false,
114
+ reason: "reservation_expired",
115
+ operation: { state: "released", version: 2 },
116
+ });
117
+ expect(await f.store.getOperation(ref(other))).toMatchObject({ state: "reserved" });
118
+ expect(await f.store.applicableBudgets({
119
+ scope: i.scope,
120
+ surface: i.surface,
121
+ units: ["requests"],
122
+ })).toMatchObject([{ reserved: quantity(1n), remaining: quantity(1n) }]);
123
+ });
124
+ test("invalid lifetimes and batch sizes are rejected", async () => {
125
+ for (const ttl of [0, -1, 86400001, 1.5])
126
+ await expect(f.store.reserve(input({ reservationTtlMs: ttl }))).rejects.toThrow("InvalidInput");
127
+ for (const limit of [0, 1001])
128
+ await expect(f.store.expireReservations({ namespace: "test", limit })).rejects.toThrow("InvalidInput");
129
+ });
130
+ });
131
+ }
@@ -0,0 +1,17 @@
1
+ import type { Budget } from "@usagekit/core";
2
+ import type { Store, ManualClock } from "../index.js";
3
+ export type StoreCapabilities = {
4
+ durable: boolean;
5
+ rollingWindows: boolean;
6
+ maxMoneyUnits: bigint;
7
+ maxQuantityScale: number;
8
+ };
9
+ export type StoreFixture = {
10
+ store: Store;
11
+ clock: ManualClock;
12
+ budgets: Budget[];
13
+ close(): Promise<void>;
14
+ /** Persistent adapters must terminate and reopen their backing process here. */
15
+ restart?: () => Promise<Store>;
16
+ };
17
+ export type StoreFactory = () => Promise<StoreFixture>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import type { ReserveInput, Operation, Receipt, Budget, Quantity } from "@usagekit/core";
2
+ export declare const quantity: (value?: bigint, unit?: string, scale?: number) => Quantity;
3
+ export declare const input: (overrides?: Partial<ReserveInput>) => ReserveInput;
4
+ export declare const ref: (op: ReserveInput) => {
5
+ namespace: string;
6
+ principal: string;
7
+ operationId: string;
8
+ };
9
+ export declare const command: (op: Operation) => {
10
+ commandId: `${string}-${string}-${string}-${string}-${string}`;
11
+ expectedVersion: number;
12
+ namespace: string;
13
+ principal: string;
14
+ operationId: string;
15
+ };
16
+ export declare const receipt: (overrides?: Partial<Receipt>) => Receipt;
17
+ export declare const budget: (overrides?: Partial<Budget>) => Budget;
18
+ export declare const unknownReceipt: () => Receipt;
@@ -0,0 +1,49 @@
1
+ export const quantity = (value = 1n, unit = "requests", scale = 0) => ({
2
+ value,
3
+ unit,
4
+ scale,
5
+ });
6
+ export const input = (overrides = {}) => ({
7
+ operationId: crypto.randomUUID(),
8
+ scope: { namespace: "test", principal: "u1", connection: "c1" },
9
+ fundingSource: "byok",
10
+ costOwner: "u1",
11
+ surface: "app",
12
+ source: "app",
13
+ provider: "search",
14
+ operation: "search",
15
+ estimate: [quantity()],
16
+ ...overrides,
17
+ });
18
+ export const ref = (op) => ({
19
+ namespace: op.scope.namespace,
20
+ principal: op.scope.principal,
21
+ operationId: op.operationId,
22
+ });
23
+ export const command = (op) => ({
24
+ ...ref(op),
25
+ commandId: crypto.randomUUID(),
26
+ expectedVersion: op.version,
27
+ });
28
+ export const receipt = (overrides = {}) => ({
29
+ id: crypto.randomUUID(),
30
+ measurements: [{ unit: "requests", quantity: quantity(), certainty: "measured" }],
31
+ cost: { certainty: "measured", money: { units: 1n, currency: "USD" } },
32
+ occurredAt: "2026-09-23T12:00:00.000Z",
33
+ recordedAt: "2026-09-23T12:00:00.000Z",
34
+ cached: false,
35
+ failed: false,
36
+ ...overrides,
37
+ });
38
+ export const budget = (overrides = {}) => ({
39
+ id: crypto.randomUUID(),
40
+ version: 1,
41
+ scope: { kind: "principal", namespace: "test", principal: "u1" },
42
+ surface: "any",
43
+ unit: "requests",
44
+ limit: quantity(2n),
45
+ window: { kind: "calendar_month", timezone: "UTC" },
46
+ onExceed: "block",
47
+ ...overrides,
48
+ });
49
+ export const unknownReceipt = () => receipt({ cost: { certainty: "unknown", money: null } });
@@ -0,0 +1,4 @@
1
+ export { runStoreConformance } from "./suite.js";
2
+ export type { StoreFactory, StoreCapabilities } from "./factory.js";
3
+ export { runStoreScalingConformance } from "./scaling.js";
4
+ export type { ScalingFixture } from "./scaling.js";
@@ -0,0 +1,2 @@
1
+ export { runStoreConformance } from "./suite.js";
2
+ export { runStoreScalingConformance } from "./scaling.js";
@@ -0,0 +1,2 @@
1
+ import type { StoreFactory } from "./factory.js";
2
+ export declare function propertyTests(factory: StoreFactory): void;
@@ -0,0 +1,220 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import fc from "fast-check";
3
+ import { input, command, ref, receipt, unknownReceipt, budget, quantity } from "./helpers.js";
4
+ const numRuns = globalThis.process?.env?.CI === "true" ? 1000 : 200;
5
+ export function propertyTests(factory) {
6
+ describe("generated command interleavings", () => {
7
+ test("holders never reacquire dispatch and mutations preserve state invariants", async () => {
8
+ await fc.assert(fc.asyncProperty(fc.integer({ min: 2, max: 5 }), fc.array(fc.record({
9
+ kind: fc.constantFrom("intent", "renew", "settle", "correct", "release", "claim", "advanceClock"),
10
+ holder: fc.nat(4),
11
+ advance: fc.integer({ min: 1, max: 1500 }),
12
+ stale: fc.boolean(),
13
+ unknown: fc.boolean(),
14
+ }), { minLength: 1, maxLength: 70 }), async (k, sequence) => {
15
+ const f = await factory();
16
+ try {
17
+ const i = input({ reservationTtlMs: 1000 }), r = await f.store.reserve(i);
18
+ if (r.outcome !== "reserved")
19
+ throw new Error("fixture");
20
+ let grants = 0;
21
+ const leases = [];
22
+ for (const step of sequence) {
23
+ const before = (await f.store.getOperation(ref(i)));
24
+ const cmd = {
25
+ ...command(before),
26
+ expectedVersion: step.stale ? 0 : before.version,
27
+ };
28
+ let result;
29
+ const leaseId = step.stale
30
+ ? (leases[0] ?? "foreign")
31
+ : (before.lease?.leaseId ?? "foreign");
32
+ const authority = {
33
+ kind: (before.lease?.holder.startsWith("recovery") ? "recovery" : "lease"),
34
+ leaseId,
35
+ };
36
+ if (step.kind === "advanceClock")
37
+ f.clock.advance(step.advance);
38
+ if (step.kind === "intent") {
39
+ result = await f.store.markDispatchIntent({
40
+ ...cmd,
41
+ holder: `h${step.holder % k}`,
42
+ leaseTtlMs: 1000,
43
+ });
44
+ if (result &&
45
+ typeof result === "object" &&
46
+ "granted" in result &&
47
+ result.granted) {
48
+ grants++;
49
+ leases.push(result.lease.leaseId);
50
+ }
51
+ }
52
+ if (step.kind === "renew")
53
+ result = await f.store.renewLease({ ...ref(i), leaseId, leaseTtlMs: 1000 });
54
+ if (step.kind === "claim") {
55
+ result = await f.store.claimForRecovery({
56
+ ...ref(i),
57
+ holder: `recovery${step.holder % k}`,
58
+ leaseTtlMs: 1000,
59
+ });
60
+ if (result && typeof result === "object" && "claimed" in result && result.claimed)
61
+ leases.push(result.lease.leaseId);
62
+ }
63
+ if (step.kind === "settle") {
64
+ const c = {
65
+ ...cmd,
66
+ authority,
67
+ receipt: step.unknown ? unknownReceipt() : receipt(),
68
+ };
69
+ const settled = await f.store.settle(c);
70
+ result = settled;
71
+ if (settled.outcome === "settled")
72
+ expect(await f.store.settle(c)).toEqual({ ...settled, replayed: true });
73
+ }
74
+ if (step.kind === "correct") {
75
+ const c = {
76
+ ...cmd,
77
+ authority: before.state === "settled"
78
+ ? { kind: "late_evidence", source: "provider" }
79
+ : authority,
80
+ receipt: receipt(),
81
+ replacesReceiptId: before.receipts.at(-1)?.id ?? "missing",
82
+ reason: "adjust",
83
+ };
84
+ const corrected = await f.store.correct(c);
85
+ result = corrected;
86
+ if (corrected.outcome === "settled")
87
+ expect(await f.store.correct(c)).toEqual({ ...corrected, replayed: true });
88
+ }
89
+ if (step.kind === "release") {
90
+ const c = { ...cmd, reason: "cancel" };
91
+ const released = await f.store.releaseUndispatched(c);
92
+ result = released;
93
+ if (released.outcome === "released")
94
+ expect(await f.store.releaseUndispatched(c)).toEqual({
95
+ ...released,
96
+ replayed: true,
97
+ });
98
+ }
99
+ const after = (await f.store.getOperation(ref(i)));
100
+ const mutated = result &&
101
+ typeof result === "object" &&
102
+ (("reason" in result && result.reason === "reservation_expired") ||
103
+ ("granted" in result && result.granted) ||
104
+ ("claimed" in result && result.claimed) ||
105
+ ("outcome" in result &&
106
+ ["settled", "released"].includes(String(result.outcome))));
107
+ expect(grants).toBeLessThanOrEqual(1);
108
+ expect(after.version).toBe(before.version + (mutated ? 1 : 0));
109
+ expect(after.receipts.length).toBeGreaterThanOrEqual(before.receipts.length);
110
+ const allowed = {
111
+ reserved: ["reserved", "dispatch_intended", "released"],
112
+ dispatch_intended: ["dispatch_intended", "pending", "settled"],
113
+ pending: ["pending", "settled"],
114
+ settled: ["settled"],
115
+ released: ["released"],
116
+ };
117
+ expect(allowed[before.state]).toContain(after.state);
118
+ const live = leases.filter((id) => id === after.lease?.leaseId &&
119
+ Date.parse(after.lease.expiresAt) > f.clock.now().getTime());
120
+ expect(new Set(live).size).toBeLessThanOrEqual(1);
121
+ }
122
+ }
123
+ finally {
124
+ await f.close();
125
+ }
126
+ }), { numRuns, verbose: 2 });
127
+ });
128
+ test("many operations never over-admit any blocking budget", async () => {
129
+ await fc.assert(fc.asyncProperty(fc.array(fc.record({
130
+ kind: fc.constantFrom("principal", "connection", "platform_pool"),
131
+ limit: fc.integer({ min: 0, max: 30 }),
132
+ warn: fc.boolean(),
133
+ }), { minLength: 1, maxLength: 4 }), fc.array(fc.record({
134
+ owner: fc.integer({ min: 1, max: 3 }),
135
+ amount: fc.integer({ min: 0, max: 5 }),
136
+ settled: fc.boolean(),
137
+ }), { minLength: 1, maxLength: 30 }), async (bounds, operations) => {
138
+ const f = await factory();
139
+ try {
140
+ for (const spec of bounds)
141
+ f.budgets.push(budget({
142
+ scope: spec.kind === "principal"
143
+ ? { kind: "principal", namespace: "test", principal: "u1" }
144
+ : spec.kind === "connection"
145
+ ? { kind: "connection", namespace: "test", connection: "c1" }
146
+ : { kind: "platform_pool", namespace: "test", poolId: "pool" },
147
+ limit: quantity(BigInt(spec.limit)),
148
+ onExceed: spec.warn ? "warn" : "block",
149
+ }));
150
+ const accepted = [];
151
+ for (const spec of operations) {
152
+ const i = input({
153
+ scope: { namespace: "test", principal: `u${spec.owner}`, connection: "c1" },
154
+ platformPools: ["pool"],
155
+ estimate: [quantity(BigInt(spec.amount))],
156
+ });
157
+ const r = await f.store.reserve(i);
158
+ if (r.outcome === "reserved") {
159
+ accepted.push(r.operation);
160
+ if (spec.settled) {
161
+ const g = await f.store.markDispatchIntent({
162
+ ...command(r.operation),
163
+ holder: "h",
164
+ leaseTtlMs: 1000,
165
+ });
166
+ if (!("granted" in g) || !g.granted)
167
+ throw new Error("fixture");
168
+ await f.store.settle({
169
+ ...command(g.operation),
170
+ authority: { kind: "lease", leaseId: g.lease.leaseId },
171
+ receipt: receipt({
172
+ measurements: [
173
+ {
174
+ unit: "requests",
175
+ certainty: "measured",
176
+ quantity: quantity(BigInt(spec.amount)),
177
+ },
178
+ ],
179
+ }),
180
+ });
181
+ }
182
+ }
183
+ for (const b of f.budgets.filter((b) => b.onExceed === "block")) {
184
+ const total = accepted
185
+ .filter((op) => op.budgetEpochs.some((e) => e.budgetId === b.id))
186
+ .reduce((n, op) => n + op.estimate[0].value, 0n);
187
+ expect(total).toBeLessThanOrEqual(b.limit.value);
188
+ }
189
+ }
190
+ }
191
+ finally {
192
+ await f.close();
193
+ }
194
+ }), { numRuns, verbose: 2 });
195
+ });
196
+ test("accepted reserve and release replay without state mutation", async () => {
197
+ await fc.assert(fc.asyncProperty(fc.bigInt({ min: 0n, max: 100000n }), fc.boolean(), async (amount, release) => {
198
+ const f = await factory();
199
+ try {
200
+ const i = input({ estimate: [quantity(amount)] });
201
+ const r = await f.store.reserve(i);
202
+ if (r.outcome !== "reserved")
203
+ throw new Error("fixture");
204
+ expect(await f.store.reserve(i)).toEqual({ ...r, replayed: true });
205
+ if (release) {
206
+ const c = { ...command(r.operation), reason: "cancel" };
207
+ const result = await f.store.releaseUndispatched(c);
208
+ expect(await f.store.releaseUndispatched(c)).toEqual({ ...result, replayed: true });
209
+ }
210
+ const before = await f.store.getOperation(ref(i));
211
+ await f.store.reserve(i);
212
+ expect(await f.store.getOperation(ref(i))).toEqual(before);
213
+ }
214
+ finally {
215
+ await f.close();
216
+ }
217
+ }), { numRuns, verbose: 2 });
218
+ });
219
+ });
220
+ }
@@ -0,0 +1,2 @@
1
+ import type { StoreFactory, StoreCapabilities } from "./factory.js";
2
+ export declare function readsTests(factory: StoreFactory, capabilities: StoreCapabilities): void;