effect-distributed-lock 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/Backing.d.ts +54 -0
- package/dist/Backing.d.ts.map +1 -0
- package/dist/Backing.js +14 -0
- package/dist/Backing.js.map +1 -0
- package/dist/DistributedSemaphore.d.ts +162 -0
- package/dist/DistributedSemaphore.d.ts.map +1 -0
- package/dist/DistributedSemaphore.js +163 -0
- package/dist/DistributedSemaphore.js.map +1 -0
- package/dist/Errors.d.ts +24 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Errors.js +18 -0
- package/dist/Errors.js.map +1 -0
- package/dist/RedisBacking.d.ts +40 -0
- package/dist/RedisBacking.d.ts.map +1 -0
- package/dist/RedisBacking.js +233 -0
- package/dist/RedisBacking.js.map +1 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/package.json +1 -1
- package/src/Backing.ts +1 -2
- package/src/DistributedSemaphore.ts +36 -27
- package/src/RedisBacking.ts +3 -3
- package/bun.lock +0 -64
- package/examples/kitchen-sink.ts +0 -119
- package/examples/push.ts +0 -109
- package/tsconfig.json +0 -35
package/README.md
CHANGED
|
@@ -111,7 +111,11 @@ yield* Effect.scoped(
|
|
|
111
111
|
);
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
Both `take` and `tryTake` return the keepalive fiber that refreshes the permit TTL.
|
|
114
|
+
Both `take` and `tryTake` return the keepalive fiber that refreshes the permit TTL.
|
|
115
|
+
|
|
116
|
+
⚠️ **CRITICAL**: Errors from the keepalive fiber (losing permits or backing store failure) mean **the lock is effectively lost**. You **must** join this fiber at some point in your program to detect these failures. If the keepalive fiber errors and you don't join it, your program will continue running without holding the lock, potentially leading to race conditions or data corruption.
|
|
117
|
+
|
|
118
|
+
**It is highly recommended to use `withPermits` or `withPermitsIfAvailable` instead**, which automatically manage the keepalive fiber lifecycle and propagate errors for you. Only use `take`/`tryTake` if you need explicit scope control and understand the responsibility of managing the keepalive fiber.
|
|
115
119
|
|
|
116
120
|
### Acquire Options
|
|
117
121
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Context, Duration, Effect, Stream } from "effect";
|
|
2
|
+
declare const SemaphoreBackingError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
|
|
3
|
+
readonly _tag: "SemaphoreBackingError";
|
|
4
|
+
} & Readonly<A>;
|
|
5
|
+
/**
|
|
6
|
+
* Error from the backing store (Redis, etc.)
|
|
7
|
+
*/
|
|
8
|
+
export declare class SemaphoreBackingError extends SemaphoreBackingError_base<{
|
|
9
|
+
readonly operation: string;
|
|
10
|
+
readonly cause: unknown;
|
|
11
|
+
}> {
|
|
12
|
+
get message(): string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Low-level backing store interface for distributed semaphore operations.
|
|
16
|
+
*/
|
|
17
|
+
export interface DistributedSemaphoreBacking {
|
|
18
|
+
/**
|
|
19
|
+
* Try to acquire `permits` from a semaphore with the given `limit`.
|
|
20
|
+
*
|
|
21
|
+
* @returns `true` if acquired, `false` if not enough permits available.
|
|
22
|
+
*/
|
|
23
|
+
readonly tryAcquire: (key: string, holderId: string, ttl: Duration.Duration, limit: number, permits: number) => Effect.Effect<boolean, SemaphoreBackingError>;
|
|
24
|
+
/**
|
|
25
|
+
* Release `permits` held by the given holder.
|
|
26
|
+
*
|
|
27
|
+
* @returns The number of permits actually released.
|
|
28
|
+
*/
|
|
29
|
+
readonly release: (key: string, holderId: string, permits: number) => Effect.Effect<number, SemaphoreBackingError>;
|
|
30
|
+
/**
|
|
31
|
+
* Refresh the TTL on permits held by this holder.
|
|
32
|
+
*
|
|
33
|
+
* @returns `true` if refreshed, `false` if permits were lost (e.g., expired).
|
|
34
|
+
*/
|
|
35
|
+
readonly refresh: (key: string, holderId: string, ttl: Duration.Duration, limit: number, permits: number) => Effect.Effect<boolean, SemaphoreBackingError>;
|
|
36
|
+
/**
|
|
37
|
+
* Get the number of permits currently held (in use).
|
|
38
|
+
*/
|
|
39
|
+
readonly getCount: (key: string, ttl: Duration.Duration) => Effect.Effect<number, SemaphoreBackingError>;
|
|
40
|
+
/**
|
|
41
|
+
* Optional: Stream of notifications when permits MAY be available.
|
|
42
|
+
*
|
|
43
|
+
* If provided, the semaphore layer uses this in addition to polling. The stream emits a signal whenever permits are released.
|
|
44
|
+
*
|
|
45
|
+
* Notes:
|
|
46
|
+
* - Multiple waiters may race for permits after a notification
|
|
47
|
+
* - The semaphore still calls `tryAcquire` after each notification
|
|
48
|
+
* - Implementations should handle reconnection internally (hence why the stream does not have an error type)
|
|
49
|
+
*/
|
|
50
|
+
readonly onPermitsReleased?: (key: string) => Stream.Stream<void>;
|
|
51
|
+
}
|
|
52
|
+
export declare const DistributedSemaphoreBacking: Context.Tag<DistributedSemaphoreBacking, DistributedSemaphoreBacking>;
|
|
53
|
+
export {};
|
|
54
|
+
//# sourceMappingURL=Backing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Backing.d.ts","sourceRoot":"","sources":["../src/Backing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAQ,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;;;;AAMjE;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,2BAEzC;IACA,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB,CAAC;IACA,IAAI,OAAO,WAEV;CACF;AAMD;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,CACnB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,QAAQ,CAAC,QAAQ,EACtB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,KACZ,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,qBAAqB,CAAC,CAAC;IAEnD;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,CAChB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,KACZ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC;IAElD;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,CAChB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,QAAQ,CAAC,QAAQ,EACtB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,KACZ,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,qBAAqB,CAAC,CAAC;IAEnD;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,CACjB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,QAAQ,CAAC,QAAQ,KACnB,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC;IAElD;;;;;;;;;OASG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CACnE;AAED,eAAO,MAAM,2BAA2B,uEAGrC,CAAC"}
|
package/dist/Backing.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Context, Data, Duration, Effect, Stream } from "effect";
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// Errors
|
|
4
|
+
// =============================================================================
|
|
5
|
+
/**
|
|
6
|
+
* Error from the backing store (Redis, etc.)
|
|
7
|
+
*/
|
|
8
|
+
export class SemaphoreBackingError extends Data.TaggedError("SemaphoreBackingError") {
|
|
9
|
+
get message() {
|
|
10
|
+
return `Backing store error during "${this.operation}": ${this.cause}`;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export const DistributedSemaphoreBacking = Context.GenericTag("@effect-distributed-lock/DistributedSemaphoreBacking");
|
|
14
|
+
//# sourceMappingURL=Backing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Backing.js","sourceRoot":"","sources":["../src/Backing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEjE,gFAAgF;AAChF,SAAS;AACT,gFAAgF;AAEhF;;GAEG;AACH,MAAM,OAAO,qBAAsB,SAAQ,IAAI,CAAC,WAAW,CACzD,uBAAuB,CAIvB;IACA,IAAI,OAAO;QACT,OAAO,+BAA+B,IAAI,CAAC,SAAS,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;IACzE,CAAC;CACF;AAoED,MAAM,CAAC,MAAM,2BAA2B,GACtC,OAAO,CAAC,UAAU,CAChB,sDAAsD,CACvD,CAAC"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Duration, Effect, Fiber, Option, Schedule, Scope } from "effect";
|
|
2
|
+
import { DistributedSemaphoreBacking, SemaphoreBackingError } from "./Backing.js";
|
|
3
|
+
import { LockLostError, LockNotAcquiredError } from "./Errors.js";
|
|
4
|
+
export interface DistributedSemaphoreConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Maximum number of permits available.
|
|
7
|
+
* This is the total capacity of the semaphore.
|
|
8
|
+
* @default 1 (mutex behavior)
|
|
9
|
+
*/
|
|
10
|
+
readonly limit?: number;
|
|
11
|
+
/**
|
|
12
|
+
* TTL for held permits. If the holder crashes, permits auto-release after this.
|
|
13
|
+
* @default 30 seconds
|
|
14
|
+
*/
|
|
15
|
+
readonly ttl?: Duration.DurationInput;
|
|
16
|
+
/**
|
|
17
|
+
* How often to refresh the TTL while holding permits.
|
|
18
|
+
* Should be less than TTL to avoid losing the permits.
|
|
19
|
+
* @default 1/3 of TTL
|
|
20
|
+
*/
|
|
21
|
+
readonly refreshInterval?: Duration.DurationInput;
|
|
22
|
+
/**
|
|
23
|
+
* How often to poll when waiting to acquire permits.
|
|
24
|
+
* @default Schedule.spaced(Duration.millis(100))
|
|
25
|
+
*/
|
|
26
|
+
readonly acquireRetryPolicy?: Schedule.Schedule<unknown>;
|
|
27
|
+
/**
|
|
28
|
+
* Retry policy when a backing failure occurs.
|
|
29
|
+
* This could happen when:
|
|
30
|
+
* - Trying to acquire permits
|
|
31
|
+
* - Refreshing the TTL
|
|
32
|
+
* - Releasing permits
|
|
33
|
+
* @default Schedule.recurs(3)
|
|
34
|
+
*/
|
|
35
|
+
readonly backingFailureRetryPolicy?: Schedule.Schedule<unknown>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Options for acquire operations (take, tryTake, withPermits, etc.)
|
|
39
|
+
*/
|
|
40
|
+
export interface AcquireOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Unique identifier for this permit holder.
|
|
43
|
+
*
|
|
44
|
+
* By default, a random UUID is generated per-acquire. Override this if you need:
|
|
45
|
+
* - Predictable identifiers for debugging/observability
|
|
46
|
+
* - Cross-process lock handoff (acquire in one process, release in another)
|
|
47
|
+
*
|
|
48
|
+
* ⚠️ **Warning**: Must be unique across concurrent holders, otherwise locks with the same
|
|
49
|
+
* identifier may be treated as the same holder.
|
|
50
|
+
*
|
|
51
|
+
* @default crypto.randomUUID()
|
|
52
|
+
*/
|
|
53
|
+
readonly identifier?: string;
|
|
54
|
+
/**
|
|
55
|
+
* If true, assumes the permits were already acquired externally with the given identifier.
|
|
56
|
+
* Instead of acquiring, uses refresh to verify ownership.
|
|
57
|
+
*
|
|
58
|
+
* **Requires `identifier` to be provided.**
|
|
59
|
+
*
|
|
60
|
+
* This is useful for cross-process lock handoff:
|
|
61
|
+
* 1. Process A acquires permits with a known identifier
|
|
62
|
+
* 2. Process A passes the identifier to Process B (via message queue, etc.)
|
|
63
|
+
* 3. Process B calls take/withPermits with `{ identifier, acquiredExternally: true }`
|
|
64
|
+
* 4. Process B now owns the permits (refreshing and releasing)
|
|
65
|
+
*
|
|
66
|
+
* ⚠️ **Unsafe**: If the identifier is wrong or the lock expired, this will fail immediately.
|
|
67
|
+
*
|
|
68
|
+
* @default false
|
|
69
|
+
*/
|
|
70
|
+
readonly acquiredExternally?: boolean;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A distributed semaphore that can be used across multiple processes/services.
|
|
74
|
+
*
|
|
75
|
+
* Similar to Effect's built-in Semaphore, but distributed across processes
|
|
76
|
+
* using a backing store like Redis.
|
|
77
|
+
*
|
|
78
|
+
* A semaphore manages a pool of permits. Tasks can acquire one or more permits,
|
|
79
|
+
* and must release them when done. If not enough permits are available, tasks
|
|
80
|
+
* wait until permits are released.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* // Create a semaphore that allows 5 concurrent operations
|
|
85
|
+
* const sem = yield* DistributedSemaphore.make("my-resource", { limit: 5 });
|
|
86
|
+
*
|
|
87
|
+
* // Acquire 2 permits, run effect, then release
|
|
88
|
+
* yield* sem.withPermits(2)(myEffect);
|
|
89
|
+
*
|
|
90
|
+
* // Create a mutex (limit=1) for exclusive access
|
|
91
|
+
* const mutex = yield* DistributedSemaphore.make("my-lock", { limit: 1 });
|
|
92
|
+
* yield* mutex.withPermits(1)(criticalSection);
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export interface DistributedSemaphore {
|
|
96
|
+
/**
|
|
97
|
+
* The key identifying this semaphore in the backing store.
|
|
98
|
+
*/
|
|
99
|
+
readonly key: string;
|
|
100
|
+
/**
|
|
101
|
+
* The maximum number of permits available.
|
|
102
|
+
*/
|
|
103
|
+
readonly limit: number;
|
|
104
|
+
/**
|
|
105
|
+
* Run an effect with the specified number of permits.
|
|
106
|
+
* Acquires the permits, runs the effect, then releases.
|
|
107
|
+
* If not enough permits are available, waits until they are.
|
|
108
|
+
*
|
|
109
|
+
* The permit TTL is refreshed automatically while the effect runs.
|
|
110
|
+
*/
|
|
111
|
+
readonly withPermits: (permits: number, options?: AcquireOptions) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | LockLostError | SemaphoreBackingError | LockNotAcquiredError, R>;
|
|
112
|
+
/**
|
|
113
|
+
* Run an effect only if the specified permits are immediately available.
|
|
114
|
+
* Returns Some(result) if permits were acquired and effect ran,
|
|
115
|
+
* None if permits were not available.
|
|
116
|
+
*/
|
|
117
|
+
readonly withPermitsIfAvailable: (permits: number, options?: AcquireOptions) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<Option.Option<A>, E | LockLostError | SemaphoreBackingError, R>;
|
|
118
|
+
/**
|
|
119
|
+
* Acquire the specified number of permits, waiting if necessary.
|
|
120
|
+
* The permits are held until the scope is closed.
|
|
121
|
+
* The permit TTL is refreshed automatically while held.
|
|
122
|
+
*
|
|
123
|
+
* @returns The fiber that refreshes the permit hold. Its lifetime is tied to the scope.
|
|
124
|
+
* When the scope closes, the fiber is interrupted and permits are released.
|
|
125
|
+
*/
|
|
126
|
+
readonly take: (permits: number, options?: AcquireOptions) => Effect.Effect<Fiber.Fiber<never, LockLostError | SemaphoreBackingError>, LockLostError | SemaphoreBackingError | LockNotAcquiredError, Scope.Scope>;
|
|
127
|
+
/**
|
|
128
|
+
* Try to acquire permits immediately without waiting.
|
|
129
|
+
* Returns Some(fiber) if acquired (permits held until scope closes),
|
|
130
|
+
* None if permits were not available.
|
|
131
|
+
*
|
|
132
|
+
* @returns The fiber that refreshes the permit hold. Its lifetime is tied to the scope.
|
|
133
|
+
* When the scope closes, the fiber is interrupted and permits are released.
|
|
134
|
+
*/
|
|
135
|
+
readonly tryTake: (permits: number, options?: AcquireOptions) => Effect.Effect<Option.Option<Fiber.Fiber<never, LockLostError | SemaphoreBackingError>>, LockLostError | SemaphoreBackingError, Scope.Scope>;
|
|
136
|
+
/**
|
|
137
|
+
* Get the current number of active permits (for debugging/introspection).
|
|
138
|
+
* Note: This is eventually consistent due to TTL-based expiration.
|
|
139
|
+
*/
|
|
140
|
+
readonly currentCount: Effect.Effect<number, SemaphoreBackingError>;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Create a distributed semaphore for the given key.
|
|
144
|
+
*
|
|
145
|
+
* @param key - Unique identifier for this semaphore in the backing store
|
|
146
|
+
* @param config - Configuration options
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```ts
|
|
150
|
+
* // Create a semaphore that allows 5 concurrent operations
|
|
151
|
+
* const sem = yield* DistributedSemaphore.make("my-resource", { limit: 5 });
|
|
152
|
+
*
|
|
153
|
+
* // Use withPermits to run an effect with acquired permits
|
|
154
|
+
* yield* sem.withPermits(2)(Effect.log("I have 2 permits"));
|
|
155
|
+
*
|
|
156
|
+
* // Use withPermitsIfAvailable to try without waiting
|
|
157
|
+
* const result = yield* sem.withPermitsIfAvailable(1)(Effect.succeed(42));
|
|
158
|
+
* // result: Option<number>
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export declare const make: (key: string, config?: DistributedSemaphoreConfig) => Effect.Effect<DistributedSemaphore, never, DistributedSemaphoreBacking>;
|
|
162
|
+
//# sourceMappingURL=DistributedSemaphore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DistributedSemaphore.d.ts","sourceRoot":"","sources":["../src/DistributedSemaphore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Deferred, Duration, Effect, Fiber, Function, Option, Schedule, Scope, Stream, } from "effect";
|
|
2
|
+
import { DistributedSemaphoreBacking, SemaphoreBackingError, } from "./Backing.js";
|
|
3
|
+
import { LockLostError, LockNotAcquiredError } from "./Errors.js";
|
|
4
|
+
const DEFAULT_LIMIT = 1;
|
|
5
|
+
const DEFAULT_TTL = Duration.seconds(30);
|
|
6
|
+
const DEFAULT_ACQUIRE_RETRY_POLICY = Schedule.spaced(Duration.millis(100));
|
|
7
|
+
const DEFAULT_FAILURE_RETRY_POLICY = Schedule.recurs(3);
|
|
8
|
+
function fullyResolveConfig(config) {
|
|
9
|
+
const limit = config.limit ?? DEFAULT_LIMIT;
|
|
10
|
+
const ttl = config.ttl ? Duration.decode(config.ttl) : DEFAULT_TTL;
|
|
11
|
+
const refreshInterval = config.refreshInterval
|
|
12
|
+
? Duration.decode(config.refreshInterval)
|
|
13
|
+
: Duration.millis(Duration.toMillis(ttl) / 3);
|
|
14
|
+
const acquireRetryPolicy = config.acquireRetryPolicy
|
|
15
|
+
? config.acquireRetryPolicy
|
|
16
|
+
: DEFAULT_ACQUIRE_RETRY_POLICY;
|
|
17
|
+
const backingFailureRetryPolicy = config.backingFailureRetryPolicy
|
|
18
|
+
? config.backingFailureRetryPolicy
|
|
19
|
+
: DEFAULT_FAILURE_RETRY_POLICY;
|
|
20
|
+
return {
|
|
21
|
+
limit,
|
|
22
|
+
ttl,
|
|
23
|
+
refreshInterval,
|
|
24
|
+
acquireRetryPolicy,
|
|
25
|
+
backingFailureRetryPolicy,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create a distributed semaphore for the given key.
|
|
30
|
+
*
|
|
31
|
+
* @param key - Unique identifier for this semaphore in the backing store
|
|
32
|
+
* @param config - Configuration options
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* // Create a semaphore that allows 5 concurrent operations
|
|
37
|
+
* const sem = yield* DistributedSemaphore.make("my-resource", { limit: 5 });
|
|
38
|
+
*
|
|
39
|
+
* // Use withPermits to run an effect with acquired permits
|
|
40
|
+
* yield* sem.withPermits(2)(Effect.log("I have 2 permits"));
|
|
41
|
+
*
|
|
42
|
+
* // Use withPermitsIfAvailable to try without waiting
|
|
43
|
+
* const result = yield* sem.withPermitsIfAvailable(1)(Effect.succeed(42));
|
|
44
|
+
* // result: Option<number>
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export const make = (key, config = {}) => Effect.gen(function* () {
|
|
48
|
+
const backing = yield* DistributedSemaphoreBacking;
|
|
49
|
+
// Resolve config with defaults
|
|
50
|
+
const { limit, ttl, refreshInterval, acquireRetryPolicy, backingFailureRetryPolicy, } = fullyResolveConfig(config);
|
|
51
|
+
const withBackingErrorRetry = (effect) => effect.pipe(Effect.retry({
|
|
52
|
+
while: (e) => e._tag === "SemaphoreBackingError",
|
|
53
|
+
schedule: backingFailureRetryPolicy,
|
|
54
|
+
}));
|
|
55
|
+
// Keep the permits alive by refreshing TTL periodically.
|
|
56
|
+
// This effect runs forever until interrupted (when scope closes).
|
|
57
|
+
const keepAlive = (identifier, permits) => Effect.repeat(Effect.gen(function* () {
|
|
58
|
+
const refreshed = yield* backing
|
|
59
|
+
.refresh(key, identifier, ttl, limit, permits)
|
|
60
|
+
.pipe(withBackingErrorRetry);
|
|
61
|
+
if (!refreshed) {
|
|
62
|
+
return yield* new LockLostError({ key });
|
|
63
|
+
}
|
|
64
|
+
}), Schedule.spaced(refreshInterval)).pipe(Effect.andThen(Effect.dieMessage("Invariant violated: `keepAlive` should never return a value")));
|
|
65
|
+
// Try to acquire permits immediately, returns Option
|
|
66
|
+
const tryTake = (permits, options) => Effect.gen(function* () {
|
|
67
|
+
// Generate identifier per-acquire if not provided
|
|
68
|
+
const identifier = options?.identifier ?? crypto.randomUUID();
|
|
69
|
+
const acquiredExternally = options?.acquiredExternally ?? false;
|
|
70
|
+
// If acquiredExternally, use refresh to verify ownership instead of acquire
|
|
71
|
+
const acquired = acquiredExternally
|
|
72
|
+
? yield* backing
|
|
73
|
+
.refresh(key, identifier, ttl, limit, permits)
|
|
74
|
+
.pipe(withBackingErrorRetry)
|
|
75
|
+
: yield* backing
|
|
76
|
+
.tryAcquire(key, identifier, ttl, limit, permits)
|
|
77
|
+
.pipe(withBackingErrorRetry);
|
|
78
|
+
if (!acquired) {
|
|
79
|
+
return Option.none();
|
|
80
|
+
}
|
|
81
|
+
// Start keepalive fiber, tied to this scope
|
|
82
|
+
const keepAliveFiber = yield* Effect.forkScoped(keepAlive(identifier, permits));
|
|
83
|
+
// Add finalizer to release permits when scope closes
|
|
84
|
+
yield* Effect.addFinalizer(() => backing
|
|
85
|
+
.release(key, identifier, permits)
|
|
86
|
+
.pipe(withBackingErrorRetry, Effect.ignore));
|
|
87
|
+
return Option.some(keepAliveFiber);
|
|
88
|
+
});
|
|
89
|
+
// Acquire permits with retry, returns fiber when acquired
|
|
90
|
+
const take = (permits, options) => Effect.gen(function* () {
|
|
91
|
+
// Generate identifier once for all retry attempts (outside the retry loop)
|
|
92
|
+
const identifier = options?.identifier ?? crypto.randomUUID();
|
|
93
|
+
const resolvedOptions = {
|
|
94
|
+
identifier,
|
|
95
|
+
acquiredExternally: options?.acquiredExternally,
|
|
96
|
+
};
|
|
97
|
+
// We use a semaphore to ensure that only one acquire attempt is made at a time.
|
|
98
|
+
// With `withPermitsIfAvailable`, if both the poll-based and push-based attempts "trigger" at the same time,
|
|
99
|
+
// one will succeed and the other will simple be a no-op.
|
|
100
|
+
const acquireSemaphore = yield* Effect.makeSemaphore(1);
|
|
101
|
+
const pushBasedAcquireEnabled = backing.onPermitsReleased
|
|
102
|
+
? true
|
|
103
|
+
: false;
|
|
104
|
+
const pollBasedAcquire = Effect.gen(function* () {
|
|
105
|
+
const maybeAcquired = yield* tryTake(permits, resolvedOptions).pipe(
|
|
106
|
+
// only apply the semaphore if push-based acquire is supported
|
|
107
|
+
pushBasedAcquireEnabled
|
|
108
|
+
? Function.compose(acquireSemaphore.withPermitsIfAvailable(1), Effect.map(Option.flatten))
|
|
109
|
+
: Function.identity);
|
|
110
|
+
if (Option.isNone(maybeAcquired)) {
|
|
111
|
+
return yield* new LockNotAcquiredError({ key });
|
|
112
|
+
}
|
|
113
|
+
return maybeAcquired.value;
|
|
114
|
+
}).pipe(Effect.retry({
|
|
115
|
+
while: (e) => e._tag === "LockNotAcquiredError" && !resolvedOptions.acquiredExternally,
|
|
116
|
+
schedule: acquireRetryPolicy,
|
|
117
|
+
}));
|
|
118
|
+
if (!pushBasedAcquireEnabled) {
|
|
119
|
+
return yield* pollBasedAcquire;
|
|
120
|
+
}
|
|
121
|
+
// Push-based acquire: run both poll-based and push-based acquire in parallel, and return the first one to complete
|
|
122
|
+
const pushBasedAcquire = Effect.gen(function* () {
|
|
123
|
+
if (!backing.onPermitsReleased) {
|
|
124
|
+
// SAFETY: We know that onPermitsReleased is provided because we checked it above
|
|
125
|
+
return yield* Effect.dieMessage("Invariant violated: `onPermitsReleased` is not provided when it was expected");
|
|
126
|
+
}
|
|
127
|
+
return yield* backing.onPermitsReleased(key).pipe(Stream.runFoldWhileEffect(Option.none(), Option.isNone, // keep folding while we haven't acquired
|
|
128
|
+
() => tryTake(permits, resolvedOptions).pipe(acquireSemaphore.withPermitsIfAvailable(1), Effect.map(Option.flatten))), Effect.flatMap(Option.match({
|
|
129
|
+
onSome: Effect.succeed,
|
|
130
|
+
onNone: () => Effect.dieMessage("Invariant violated: the stream should never return `None`")
|
|
131
|
+
})));
|
|
132
|
+
});
|
|
133
|
+
// first to succeed (acquire permits) wins
|
|
134
|
+
return yield* Effect.race(pollBasedAcquire, pushBasedAcquire);
|
|
135
|
+
});
|
|
136
|
+
// Convenience: acquire permits, run effect, release when done
|
|
137
|
+
const withPermits = (permits, options) => (effect) => Effect.scoped(Effect.gen(function* () {
|
|
138
|
+
const keepAliveFiber = yield* take(permits, options);
|
|
139
|
+
return yield* Effect.raceFirst(effect, Fiber.join(keepAliveFiber));
|
|
140
|
+
}));
|
|
141
|
+
// Convenience: try to acquire permits, run effect if successful
|
|
142
|
+
const withPermitsIfAvailable = (permits, options) => (effect) => Effect.scoped(Effect.gen(function* () {
|
|
143
|
+
const maybeAcquired = yield* tryTake(permits, options);
|
|
144
|
+
if (Option.isNone(maybeAcquired)) {
|
|
145
|
+
return Option.none();
|
|
146
|
+
}
|
|
147
|
+
const keepAliveFiber = maybeAcquired.value;
|
|
148
|
+
return yield* Effect.raceFirst(effect.pipe(Effect.asSome), Fiber.join(keepAliveFiber));
|
|
149
|
+
}));
|
|
150
|
+
const currentCount = backing
|
|
151
|
+
.getCount(key, ttl)
|
|
152
|
+
.pipe(withBackingErrorRetry);
|
|
153
|
+
return {
|
|
154
|
+
key,
|
|
155
|
+
limit,
|
|
156
|
+
withPermits,
|
|
157
|
+
withPermitsIfAvailable,
|
|
158
|
+
take,
|
|
159
|
+
tryTake,
|
|
160
|
+
currentCount,
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
//# sourceMappingURL=DistributedSemaphore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DistributedSemaphore.js","sourceRoot":"","sources":["../src/DistributedSemaphore.ts"],"names":[],"mappings":"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"}
|
package/dist/Errors.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
declare const LockLostError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
|
|
2
|
+
readonly _tag: "LockLostError";
|
|
3
|
+
} & Readonly<A>;
|
|
4
|
+
/**
|
|
5
|
+
* The permits were lost (TTL expired while we thought we held them)
|
|
6
|
+
*/
|
|
7
|
+
export declare class LockLostError extends LockLostError_base<{
|
|
8
|
+
readonly key: string;
|
|
9
|
+
}> {
|
|
10
|
+
get message(): string;
|
|
11
|
+
}
|
|
12
|
+
declare const LockNotAcquiredError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
|
|
13
|
+
readonly _tag: "LockNotAcquiredError";
|
|
14
|
+
} & Readonly<A>;
|
|
15
|
+
/**
|
|
16
|
+
* The lock failed to be acquired. This occurs when the lock is not aquired within the provided schedule.
|
|
17
|
+
*/
|
|
18
|
+
export declare class LockNotAcquiredError extends LockNotAcquiredError_base<{
|
|
19
|
+
readonly key: string;
|
|
20
|
+
}> {
|
|
21
|
+
get message(): string;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=Errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Errors.d.ts","sourceRoot":"","sources":["../src/Errors.ts"],"names":[],"mappings":";;;AAEA;;GAEG;AACH,qBAAa,aAAc,SAAQ,mBAAkC;IACnE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB,CAAC;IACA,IAAI,OAAO,WAEV;CACF;;;;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,0BAExC;IACA,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB,CAAC;IACA,IAAI,OAAO,WAEV;CACF"}
|
package/dist/Errors.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Data } from "effect";
|
|
2
|
+
/**
|
|
3
|
+
* The permits were lost (TTL expired while we thought we held them)
|
|
4
|
+
*/
|
|
5
|
+
export class LockLostError extends Data.TaggedError("LockLostError") {
|
|
6
|
+
get message() {
|
|
7
|
+
return `Permits for "${this.key}" were lost (TTL expired or taken by another holder)`;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The lock failed to be acquired. This occurs when the lock is not aquired within the provided schedule.
|
|
12
|
+
*/
|
|
13
|
+
export class LockNotAcquiredError extends Data.TaggedError("LockNotAcquiredError") {
|
|
14
|
+
get message() {
|
|
15
|
+
return `Lock for "${this.key}" was not acquired`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=Errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Errors.js","sourceRoot":"","sources":["../src/Errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAE9B;;GAEG;AACH,MAAM,OAAO,aAAc,SAAQ,IAAI,CAAC,WAAW,CAAC,eAAe,CAEjE;IACA,IAAI,OAAO;QACT,OAAO,gBAAgB,IAAI,CAAC,GAAG,sDAAsD,CAAC;IACxF,CAAC;CACF;AAED;;GAEG;AACH,MAAM,OAAO,oBAAqB,SAAQ,IAAI,CAAC,WAAW,CACxD,sBAAsB,CAGtB;IACA,IAAI,OAAO;QACT,OAAO,aAAa,IAAI,CAAC,GAAG,oBAAoB,CAAC;IACnD,CAAC;CACF"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Layer, Schedule } from "effect";
|
|
2
|
+
import { Redis } from "ioredis";
|
|
3
|
+
import { DistributedSemaphoreBacking } from "./Backing.js";
|
|
4
|
+
export interface RedisBackingOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Prefix for all keys in Redis.
|
|
7
|
+
* @default "semaphore:"
|
|
8
|
+
*/
|
|
9
|
+
readonly keyPrefix?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Enable push-based acquisition using Redis pub/sub.
|
|
12
|
+
*
|
|
13
|
+
* When enabled, waiters subscribe to a channel and get notified immediately
|
|
14
|
+
* when permits are released, instead of polling. This reduces latency and
|
|
15
|
+
* load on Redis.
|
|
16
|
+
*
|
|
17
|
+
* Requires an additional Redis connection per waiting semaphore.
|
|
18
|
+
*
|
|
19
|
+
* @default true
|
|
20
|
+
*/
|
|
21
|
+
readonly pushBasedAcquireEnabled?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* How often to retry the stream of notifications when permits are released.
|
|
24
|
+
* @default Schedule.forever
|
|
25
|
+
*/
|
|
26
|
+
readonly pushStreamRetrySchedule?: Schedule.Schedule<unknown>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create a Redis-backed distributed semaphore backing layer.
|
|
30
|
+
*
|
|
31
|
+
* **Important:** This implementation is for single-instance Redis only.
|
|
32
|
+
* It does not implement the Redlock algorithm and should not be used with
|
|
33
|
+
* Redis Cluster or Redis Sentinel for distributed locking guarantees.
|
|
34
|
+
* For multi-instance Redis, consider implementing a Redlock-based backing.
|
|
35
|
+
*
|
|
36
|
+
* @param redis - An ioredis client instance (single instance, not cluster)
|
|
37
|
+
* @param options - Configuration options
|
|
38
|
+
*/
|
|
39
|
+
export declare const layer: (redis: Redis, options?: RedisBackingOptions) => Layer.Layer<DistributedSemaphoreBacking>;
|
|
40
|
+
//# sourceMappingURL=RedisBacking.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RedisBacking.d.ts","sourceRoot":"","sources":["../src/RedisBacking.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,EAAE,QAAQ,EAAU,MAAM,QAAQ,CAAC;AACnE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EACL,2BAA2B,EAE5B,MAAM,cAAc,CAAC;AAsJtB,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAE5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAE3C;;;OAGG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;CAC/D;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,OAAO,KAAK,EACZ,UAAS,mBAAwB,KAChC,KAAK,CAAC,KAAK,CAAC,2BAA2B,CAyJzC,CAAC"}
|