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 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. Errors from the keepalive (losing permits or backing store failure) are propagated through the fiber.
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"}
@@ -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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"}
@@ -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"}