Repository navigation
Expand file tree
/
Copy pathDrainableWorker.ts
More file actions
64 lines (57 loc) · 2.44 KB
/
Copy pathDrainableWorker.ts
File metadata and controls
64 lines (57 loc) · 2.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import * as Cause from "effect/Cause";
import * as Scope from "effect/Scope";
import * as Effect from "effect/Effect";
import * as TxQueue from "effect/TxQueue";
import * as TxRef from "effect/TxRef";
export interface DrainableWorker<A> {
readonly enqueue: (item: A) => Effect.Effect<void>;
readonly drain: Effect.Effect<void>;
}
/**
* A finalizer shuts down the queue and drops queued items, so `drain` resolves after the scope
* closes instead of waiting on them. An item that fails or dies is logged and skipped; the worker
* keeps processing later items and `drain` still resolves.
*/
export const makeDrainableWorker = <A, E, R>(
process: (item: A) => Effect.Effect<void, E, R>,
): Effect.Effect<DrainableWorker<A>, never, Scope.Scope | R> =>
Effect.gen(function* () {
const outstanding = yield* TxRef.make(0);
const queue = yield* Effect.acquireRelease(TxQueue.unbounded<A>(), (queue) =>
// Uncount only the dropped items: an item still running uncounts itself,
// even when a parallel scope closes it after this finalizer.
TxQueue.clear(queue).pipe(
Effect.flatMap((dropped) => TxRef.update(outstanding, (n) => n - dropped.length)),
Effect.andThen(TxQueue.shutdown(queue)),
Effect.tx,
),
);
yield* TxQueue.take(queue).pipe(
Effect.flatMap((a) =>
// `suspend` turns a `process` that throws while building its effect
// into this item's defect instead of the loop's.
Effect.suspend(() => process(a)).pipe(
// Only the item's own failure, defect, or interruption lands here and
// the loop continues; interrupting the worker fiber still stops it.
Effect.catchCause((cause) =>
Cause.hasInterruptsOnly(cause)
? Effect.void
: Effect.logError("DrainableWorker item failed", cause),
),
Effect.ensuring(TxRef.update(outstanding, (n) => n - 1)),
),
),
Effect.forever,
Effect.forkScoped,
);
const drain: DrainableWorker<A>["drain"] = TxRef.get(outstanding).pipe(
Effect.flatMap((n) => (n > 0 ? Effect.txRetry : Effect.void)),
Effect.tx,
);
const enqueue = (element: A): Effect.Effect<boolean, never, never> =>
TxQueue.offer(queue, element).pipe(
Effect.tap((offered) => (offered ? TxRef.update(outstanding, (n) => n + 1) : Effect.void)),
Effect.tx,
);
return { enqueue, drain } satisfies DrainableWorker<A>;
});