1import * as KeyedLock from "@t3tools/shared/KeyedLock";
2import * as Effect from "effect/Effect";
3import * as Ref from "effect/Ref";
4
5export interface ProviderMaintenanceCommandCoordinatorShape<E> {
6 readonly withCommandLock: <A, R>(input: {
7 readonly targetKey: string;
8 readonly lockKey: string;
9 readonly onQueued?: Effect.Effect<void, E, R>;
10 readonly run: Effect.Effect<A, E, R>;
11 }) => Effect.Effect<A, E, R>;
12}
13
14export const makeProviderMaintenanceCommandCoordinator = Effect.fn(
15 "makeProviderMaintenanceCommandCoordinator",
16)(function* <E>(input: { readonly makeAlreadyRunningError: (targetKey: string) => E }) {
17 const runningTargetsRef = yield* Ref.make<ReadonlySet<string>>(new Set());
18 const locks = yield* KeyedLock.make<string>();
19
20 const acquireTarget = Effect.fn("acquireTarget")(function* (targetKey: string) {
21 return yield* Ref.modify(runningTargetsRef, (runningTargets) => {
22 if (runningTargets.has(targetKey)) {
23 return [false, runningTargets] as const;
24 }
25 const next = new Set(runningTargets);
26 next.add(targetKey);
27 return [true, next] as const;
28 });
29 });
30
31 const releaseTarget = (targetKey: string) =>
32 Ref.update(runningTargetsRef, (runningTargets) => {
33 const next = new Set(runningTargets);
34 next.delete(targetKey);
35 return next;
36 });
37
38 const withCommandLock: ProviderMaintenanceCommandCoordinatorShape<E>["withCommandLock"] = ({
39 targetKey,
40 lockKey,
41 onQueued,
42 run,
43 }) =>
44 Effect.gen(function* () {
45 const acquired = yield* acquireTarget(targetKey);
46 if (!acquired) {
47 return yield* Effect.fail(input.makeAlreadyRunningError(targetKey));
48 }
49
50 return yield* (onQueued ?? Effect.void).pipe(
51 Effect.andThen(locks.withLock(lockKey, run)),
52 Effect.ensuring(releaseTarget(targetKey)),
53 );
54 });
55
56 return {
57 withCommandLock,
58 } satisfies ProviderMaintenanceCommandCoordinatorShape<E>;
59});