1import type * as Duration from "effect/Duration";
2
3export interface ProviderReplayGate {
4 readonly beforeEmit: (label: string | undefined, signal?: AbortSignal) => Promise<void>;
5 readonly waitForReached: (label: string) => Promise<boolean>;
6 readonly hasReached: (label: string) => boolean;
7 readonly release: (label: string) => boolean;
8 readonly releaseAll: () => void;
9 /** Replay scenarios must advance TestClock by the recorded debounce after finish is armed. */
10 readonly recordFinishArmed: (debounce: Duration.Input) => void;
11 readonly finishArmedCount: () => number;
12 readonly waitForFinishArmed: (seen: number) => Promise<Duration.Input>;
13}
14
15interface GateState {
16 reached: boolean;
17 released: boolean;
18 readonly reachedPromise: Promise<void>;
19 readonly resolveReached: () => void;
20 readonly promise: Promise<void>;
21 readonly resolve: () => void;
22}
23
24export function makeProviderReplayGate(labels: ReadonlyArray<string>): ProviderReplayGate {
25 const states = new Map<string, GateState>();
26 for (const label of labels) {
27 if (states.has(label)) {
28 throw new Error(`Duplicate provider replay gate label ${label}.`);
29 }
30 let resolve = () => {};
31 const promise = new Promise<void>((resume) => {
32 resolve = resume;
33 });
34 let resolveReached = () => {};
35 const reachedPromise = new Promise<void>((resume) => {
36 resolveReached = resume;
37 });
38 states.set(label, {
39 reached: false,
40 released: false,
41 reachedPromise,
42 resolveReached,
43 promise,
44 resolve,
45 });
46 }
47
48 const finishArmed: Array<Duration.Input> = [];
49 const finishArmedWaiters: Array<() => void> = [];
50
51 return {
52 recordFinishArmed: (debounce) => {
53 finishArmed.push(debounce);
54 for (const wake of finishArmedWaiters.splice(0)) wake();
55 },
56 finishArmedCount: () => finishArmed.length,
57 waitForFinishArmed: (seen) =>
58 new Promise((resolve) => {
59 const check = () => {
60 if (finishArmed.length > seen) resolve(finishArmed[finishArmed.length - 1]!);
61 else finishArmedWaiters.push(check);
62 };
63 check();
64 }),
65 beforeEmit: (label, signal) => {
66 if (label === undefined) {
67 return Promise.resolve();
68 }
69 const state = states.get(label);
70 if (state === undefined) {
71 return Promise.resolve();
72 }
73 state.reached = true;
74 state.resolveReached();
75 if (signal === undefined) {
76 return state.promise;
77 }
78 if (signal.aborted) {
79 return Promise.resolve();
80 }
81 return new Promise<void>((resolve) => {
82 const stopWaiting = () => resolve();
83 signal.addEventListener("abort", stopWaiting, { once: true });
84 void state.promise.then(() => {
85 signal.removeEventListener("abort", stopWaiting);
86 resolve();
87 });
88 });
89 },
90 waitForReached: (label) => {
91 const state = states.get(label);
92 return state === undefined ? Promise.resolve(false) : state.reachedPromise.then(() => true);
93 },
94 hasReached: (label) => states.get(label)?.reached ?? false,
95 release: (label) => {
96 const state = states.get(label);
97 if (state === undefined || state.released) {
98 return false;
99 }
100 state.released = true;
101 state.resolve();
102 return true;
103 },
104 releaseAll: () => {
105 for (const state of states.values()) {
106 if (state.released) {
107 continue;
108 }
109 state.released = true;
110 state.resolve();
111 }
112 },
113 };
114}