1import { assert, describe, it } from "@effect/vitest";
2import * as Deferred from "effect/Deferred";
3import * as Effect from "effect/Effect";
4import * as Fiber from "effect/Fiber";
5import * as Ref from "effect/Ref";
6
7import * as ResetCreditCoordinator from "./resetCreditCoordinator.ts";
8
9describe("ResetCreditCoordinator", () => {
10 it.effect("re-sends the same idempotency key after a failed attempt, then clears it", () =>
11 Effect.gen(function* () {
12 const { redeem } = yield* ResetCreditCoordinator.ResetCreditCoordinator;
13 const keys = yield* Ref.make<ReadonlyArray<string>>([]);
14 const attempts = yield* Ref.make(0);
15 const consume = (key: string) =>
16 Effect.gen(function* () {
17 yield* Ref.update(keys, (seen) => [...seen, key]);
18 const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1);
19 if (attempt === 1) return yield* Effect.fail("timed out" as const);
20 return "reset" as const;
21 });
22
23 const first = yield* redeem("acct", consume).pipe(Effect.result);
24 assert.isTrue(first._tag === "Failure");
25 const second = yield* redeem("acct", consume);
26 assert.strictEqual(second, "reset");
27 // A fresh redemption after success must be a fresh attempt.
28 yield* redeem("acct", consume);
29
30 const seen = yield* Ref.get(keys);
31 assert.strictEqual(seen.length, 3);
32 assert.strictEqual(seen[0], seen[1]);
33 assert.notStrictEqual(seen[1], seen[2]);
34 }).pipe(Effect.provide(ResetCreditCoordinator.layerTest)),
35 );
36
37 it.effect("starts a fresh attempt after a settled failure", () =>
38 Effect.gen(function* () {
39 const { redeem } = yield* ResetCreditCoordinator.ResetCreditCoordinator;
40 const keys = yield* Ref.make<ReadonlyArray<string>>([]);
41 const consume = (key: string) =>
42 Ref.update(keys, (seen) => [...seen, key]).pipe(
43 Effect.andThen(Effect.fail("cooldown" as const)),
44 );
45 const isSettled = (error: "cooldown") => error === "cooldown";
46
47 yield* redeem("acct", consume, isSettled).pipe(Effect.result);
48 yield* redeem("acct", consume, isSettled).pipe(Effect.result);
49
50 const seen = yield* Ref.get(keys);
51 assert.strictEqual(seen.length, 2);
52 assert.notStrictEqual(seen[0], seen[1]);
53 }).pipe(Effect.provide(ResetCreditCoordinator.layerTest)),
54 );
55
56 it.effect("serialises concurrent redemptions on the same account, not per caller", () =>
57 Effect.gen(function* () {
58 const { redeem } = yield* ResetCreditCoordinator.ResetCreditCoordinator;
59 const release = yield* Deferred.make<void>();
60 const inFlight = yield* Ref.make(0);
61 const peak = yield* Ref.make(0);
62 const consume = () =>
63 Effect.gen(function* () {
64 const now = yield* Ref.updateAndGet(inFlight, (n) => n + 1);
65 yield* Ref.update(peak, (p) => Math.max(p, now));
66 yield* Deferred.await(release);
67 yield* Ref.update(inFlight, (n) => n - 1);
68 return "reset" as const;
69 });
70
71 // Two instances of the same account redeem at once.
72 const a = yield* redeem("acct", consume).pipe(Effect.forkChild);
73 const b = yield* redeem("acct", consume).pipe(Effect.forkChild);
74 yield* Effect.yieldNow;
75 yield* Deferred.succeed(release, undefined);
76 yield* Fiber.join(a);
77 yield* Fiber.join(b);
78
79 assert.strictEqual(yield* Ref.get(peak), 1);
80 }).pipe(Effect.provide(ResetCreditCoordinator.layerTest)),
81 );
82
83 it.effect("keeps different accounts independent", () =>
84 Effect.gen(function* () {
85 const { redeem } = yield* ResetCreditCoordinator.ResetCreditCoordinator;
86 const release = yield* Deferred.make<void>();
87 const peak = yield* Ref.make(0);
88 const inFlight = yield* Ref.make(0);
89 const consume = () =>
90 Effect.gen(function* () {
91 const now = yield* Ref.updateAndGet(inFlight, (n) => n + 1);
92 yield* Ref.update(peak, (p) => Math.max(p, now));
93 yield* Deferred.await(release);
94 return "reset" as const;
95 });
96 const a = yield* redeem("acct-a", consume).pipe(Effect.forkChild);
97 const b = yield* redeem("acct-b", consume).pipe(Effect.forkChild);
98 yield* Effect.yieldNow;
99 yield* Deferred.succeed(release, undefined);
100 yield* Fiber.join(a);
101 yield* Fiber.join(b);
102 assert.strictEqual(yield* Ref.get(peak), 2);
103 }).pipe(Effect.provide(ResetCreditCoordinator.layerTest)),
104 );
105});