authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-28 22:52:39-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-28 22:52:39-07:00
log00f3efb95b485a40380af8295a5119b03095858b
tree7ead71298554910a6be14c691ee52e58732af391
parent7f3387a86cf6ed2e9bb7318db56b1ab19de80e69
signature Signed by PGP key B5690EEEBB952194

Make DrainableWorker drain off outstanding count (#1514)


1 files changed, 24 insertions(+), 32 deletions(-)

packages/shared/src/DrainableWorker.ts+24-32
......@@ -1,13 +1,15 @@
11/**
2 * DrainableWorker - A queue-based worker with deterministic `drain()`.
2 * DrainableWorker - A queue-based worker that exposes a `drain()` effect.
33 *
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()`.
68 *
79 * @module DrainableWorker
810 */
9import { Effect, TxQueue, TxRef } from "effect";
1011import type { Scope } from "effect";
12import { Effect, TxQueue, TxRef } from "effect";
1113
1214export interface DrainableWorker<A> {
1315 /**
......@@ -37,40 +39,30 @@ export const makeDrainableWorker = <A, E, R>(
3739 process: (item: A) => Effect.Effect<void, E, R>,
3840): Effect.Effect<DrainableWorker<A>, never, Scope.Scope | R> =>
3941 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);
4544
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 ),
5751 ),
5852 Effect.forever,
5953 Effect.forkScoped,
6054 );
6155
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,
7059 );
7160
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>;
7668 });