Repository navigation
Expand file tree
/
Copy pathKeyedLock.ts
More file actions
62 lines (56 loc) · 2.21 KB
/
Copy pathKeyedLock.ts
File metadata and controls
62 lines (56 loc) · 2.21 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
import * as Effect from "effect/Effect";
import * as Ref from "effect/Ref";
import * as Semaphore from "effect/Semaphore";
export interface KeyedLock<Key> {
/** Runs `effect` once no other holder of `key` is running. Queued waiters go first-in, first-out. */
readonly withLock: <A, E, R>(key: Key, effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
/** The keys someone holds or waits on right now. */
readonly activeKeys: Effect.Effect<ReadonlyArray<Key>>;
}
interface LockEntry {
readonly semaphore: Semaphore.Semaphore;
readonly users: number;
}
/**
* Mutual exclusion per key. A key's lock exists only while someone holds or
* waits on it, so locks for keys that come and go never accumulate. Not
* reentrant: taking a key while already holding it deadlocks.
*
* Keys compare like `Map` keys (by value for strings and numbers). The lock is
* not tied to a scope, so it keeps working for as long as anyone references it.
*/
export const make = <Key>(): Effect.Effect<KeyedLock<Key>> =>
Effect.gen(function* () {
const locks = yield* Ref.make<ReadonlyMap<Key, LockEntry>>(new Map());
const acquire = (key: Key) =>
Effect.flatMap(Semaphore.make(1), (candidate) =>
Ref.modify(locks, (current) => {
const existing = current.get(key);
const semaphore = existing?.semaphore ?? candidate;
const next = new Map(current);
next.set(key, { semaphore, users: (existing?.users ?? 0) + 1 });
return [semaphore, next] as const;
}),
);
const release = (key: Key) =>
Ref.update(locks, (current) => {
const existing = current.get(key);
if (existing === undefined) return current;
const next = new Map(current);
if (existing.users === 1) {
next.delete(key);
} else {
next.set(key, { ...existing, users: existing.users - 1 });
}
return next;
});
return {
withLock: (key, effect) =>
Effect.acquireUseRelease(
acquire(key),
(semaphore) => semaphore.withPermit(effect),
() => release(key),
),
activeKeys: Effect.map(Ref.get(locks), (current) => Array.from(current.keys())),
} satisfies KeyedLock<Key>;
});