| ... | ... | @@ -1,13 +1,15 @@ |
| 1 | 1 | /** |
| 2 | | * DrainableWorker - A queue-based worker with deterministic `drain()`. |
| 2 | * DrainableWorker - A queue-based worker that exposes a `drain()` effect. |
| 3 | 3 | * |
| 4 | | * Tracks outstanding work in STM so `drain()` resolves only when no items |
| 5 | | * are queued or in flight. Useful in tests instead of timing-based waits. |
| 4 | * Wraps the common `Queue.unbounded` + `Effect.forever` pattern and adds |
| 5 | * a signal that resolves when the queue is empty **and** the current item |
| 6 | * has finished processing. This lets tests replace timing-sensitive |
| 7 | * `Effect.sleep` calls with deterministic `drain()`. |
| 6 | 8 | * |
| 7 | 9 | * @module DrainableWorker |
| 8 | 10 | */ |
| 9 | | import { Effect, TxQueue, TxRef } from "effect"; |
| 10 | 11 | import type { Scope } from "effect"; |
| 12 | import { Effect, TxQueue, TxRef } from "effect"; |
| 11 | 13 | |
| 12 | 14 | export interface DrainableWorker<A> { |
| 13 | 15 | /** |
| ... | ... | @@ -37,40 +39,30 @@ export const makeDrainableWorker = <A, E, R>( |
| 37 | 39 | process: (item: A) => Effect.Effect<void, E, R>, |
| 38 | 40 | ): Effect.Effect<DrainableWorker<A>, never, Scope.Scope | R> => |
| 39 | 41 | Effect.gen(function* () { |
| 40 | | const ref = yield* TxRef.make(0); |
| 41 | | |
| 42 | | const queue = yield* Effect.acquireRelease(TxQueue.unbounded<A>(), (queue) => |
| 43 | | TxQueue.shutdown(queue), |
| 44 | | ); |
| 42 | const queue = yield* Effect.acquireRelease(TxQueue.unbounded<A>(), TxQueue.shutdown); |
| 43 | const outstanding = yield* TxRef.make(0); |
| 45 | 44 | |
| 46 | | const takeItem = Effect.tx( |
| 47 | | Effect.gen(function* () { |
| 48 | | const item = yield* TxQueue.take(queue); |
| 49 | | yield* TxRef.update(ref, (n) => n + 1); |
| 50 | | return item; |
| 51 | | }), |
| 52 | | ); |
| 53 | | |
| 54 | | yield* takeItem.pipe( |
| 55 | | Effect.flatMap((item) => |
| 56 | | process(item).pipe(Effect.ensuring(TxRef.update(ref, (n) => n - 1))), |
| 45 | yield* TxQueue.take(queue).pipe( |
| 46 | Effect.tap((a) => |
| 47 | Effect.ensuring( |
| 48 | process(a), |
| 49 | TxRef.update(outstanding, (n) => n - 1), |
| 50 | ), |
| 57 | 51 | ), |
| 58 | 52 | Effect.forever, |
| 59 | 53 | Effect.forkScoped, |
| 60 | 54 | ); |
| 61 | 55 | |
| 62 | | const drain: DrainableWorker<A>["drain"] = Effect.tx( |
| 63 | | Effect.gen(function* () { |
| 64 | | const inFlight = yield* TxRef.get(ref); |
| 65 | | const isEmpty = yield* TxQueue.isEmpty(queue); |
| 66 | | if (inFlight > 0 || !isEmpty) { |
| 67 | | return yield* Effect.txRetry; |
| 68 | | } |
| 69 | | }), |
| 56 | const drain: DrainableWorker<A>["drain"] = TxRef.get(outstanding).pipe( |
| 57 | Effect.tap((n) => (n > 0 ? Effect.txRetry : Effect.void)), |
| 58 | Effect.tx, |
| 70 | 59 | ); |
| 71 | 60 | |
| 72 | | return { |
| 73 | | enqueue: (item) => TxQueue.offer(queue, item), |
| 74 | | drain, |
| 75 | | } satisfies DrainableWorker<A>; |
| 61 | const enqueue = (element: A): Effect.Effect<boolean, never, never> => |
| 62 | TxQueue.offer(queue, element).pipe( |
| 63 | Effect.tap(() => TxRef.update(outstanding, (n) => n + 1)), |
| 64 | Effect.tx, |
| 65 | ); |
| 66 | |
| 67 | return { enqueue, drain } satisfies DrainableWorker<A>; |
| 76 | 68 | }); |