effect-distributed-lock 0.0.2 → 0.0.4

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.
Files changed (99) hide show
  1. package/README.md +97 -66
  2. package/examples/index.ts +49 -21
  3. package/package.json +2 -2
  4. package/redis-semaphore/.codeclimate.yml +5 -0
  5. package/redis-semaphore/.fossa.yml +14 -0
  6. package/redis-semaphore/.github/dependabot.yml +6 -0
  7. package/redis-semaphore/.github/workflows/branches.yml +39 -0
  8. package/redis-semaphore/.github/workflows/pull-requests.yml +35 -0
  9. package/redis-semaphore/.mocharc.yaml +6 -0
  10. package/redis-semaphore/.prettierrc +6 -0
  11. package/redis-semaphore/.snyk +4 -0
  12. package/redis-semaphore/.yarnrc.yml +2 -0
  13. package/redis-semaphore/CHANGELOG.md +70 -0
  14. package/redis-semaphore/Dockerfile +5 -0
  15. package/redis-semaphore/LICENSE +21 -0
  16. package/redis-semaphore/README.md +445 -0
  17. package/redis-semaphore/docker-compose.yml +31 -0
  18. package/redis-semaphore/eslint.config.mjs +73 -0
  19. package/redis-semaphore/package.json +79 -0
  20. package/redis-semaphore/setup-redis-servers.sh +2 -0
  21. package/redis-semaphore/src/Lock.ts +172 -0
  22. package/redis-semaphore/src/RedisMultiSemaphore.ts +56 -0
  23. package/redis-semaphore/src/RedisMutex.ts +45 -0
  24. package/redis-semaphore/src/RedisSemaphore.ts +49 -0
  25. package/redis-semaphore/src/RedlockMultiSemaphore.ts +56 -0
  26. package/redis-semaphore/src/RedlockMutex.ts +52 -0
  27. package/redis-semaphore/src/RedlockSemaphore.ts +49 -0
  28. package/redis-semaphore/src/errors/LostLockError.ts +1 -0
  29. package/redis-semaphore/src/errors/TimeoutError.ts +1 -0
  30. package/redis-semaphore/src/index.ts +23 -0
  31. package/redis-semaphore/src/misc.ts +12 -0
  32. package/redis-semaphore/src/multiSemaphore/acquire/index.ts +53 -0
  33. package/redis-semaphore/src/multiSemaphore/acquire/lua.ts +31 -0
  34. package/redis-semaphore/src/multiSemaphore/refresh/index.ts +32 -0
  35. package/redis-semaphore/src/multiSemaphore/refresh/lua.ts +31 -0
  36. package/redis-semaphore/src/multiSemaphore/release/index.ts +22 -0
  37. package/redis-semaphore/src/multiSemaphore/release/lua.ts +17 -0
  38. package/redis-semaphore/src/mutex/acquire.ts +42 -0
  39. package/redis-semaphore/src/mutex/refresh.ts +37 -0
  40. package/redis-semaphore/src/mutex/release.ts +30 -0
  41. package/redis-semaphore/src/redlockMultiSemaphore/acquire.ts +56 -0
  42. package/redis-semaphore/src/redlockMultiSemaphore/refresh.ts +68 -0
  43. package/redis-semaphore/src/redlockMultiSemaphore/release.ts +19 -0
  44. package/redis-semaphore/src/redlockMutex/acquire.ts +54 -0
  45. package/redis-semaphore/src/redlockMutex/refresh.ts +53 -0
  46. package/redis-semaphore/src/redlockMutex/release.ts +19 -0
  47. package/redis-semaphore/src/redlockSemaphore/acquire.ts +55 -0
  48. package/redis-semaphore/src/redlockSemaphore/refresh.ts +60 -0
  49. package/redis-semaphore/src/redlockSemaphore/release.ts +18 -0
  50. package/redis-semaphore/src/semaphore/acquire/index.ts +52 -0
  51. package/redis-semaphore/src/semaphore/acquire/lua.ts +25 -0
  52. package/redis-semaphore/src/semaphore/refresh/index.ts +31 -0
  53. package/redis-semaphore/src/semaphore/refresh/lua.ts +25 -0
  54. package/redis-semaphore/src/semaphore/release.ts +14 -0
  55. package/redis-semaphore/src/types.ts +63 -0
  56. package/redis-semaphore/src/utils/createEval.ts +45 -0
  57. package/redis-semaphore/src/utils/index.ts +13 -0
  58. package/redis-semaphore/src/utils/redlock.ts +7 -0
  59. package/redis-semaphore/test/init.test.ts +9 -0
  60. package/redis-semaphore/test/redisClient.ts +82 -0
  61. package/redis-semaphore/test/setup.ts +6 -0
  62. package/redis-semaphore/test/shell.test.ts +15 -0
  63. package/redis-semaphore/test/shell.ts +48 -0
  64. package/redis-semaphore/test/src/Lock.test.ts +37 -0
  65. package/redis-semaphore/test/src/RedisMultiSemaphore.test.ts +425 -0
  66. package/redis-semaphore/test/src/RedisMutex.test.ts +334 -0
  67. package/redis-semaphore/test/src/RedisSemaphore.test.ts +367 -0
  68. package/redis-semaphore/test/src/RedlockMultiSemaphore.test.ts +671 -0
  69. package/redis-semaphore/test/src/RedlockMutex.test.ts +328 -0
  70. package/redis-semaphore/test/src/RedlockSemaphore.test.ts +579 -0
  71. package/redis-semaphore/test/src/index.test.ts +22 -0
  72. package/redis-semaphore/test/src/multiSemaphore/acquire/index.test.ts +51 -0
  73. package/redis-semaphore/test/src/multiSemaphore/acquire/internal.test.ts +67 -0
  74. package/redis-semaphore/test/src/multiSemaphore/refresh/index.test.ts +52 -0
  75. package/redis-semaphore/test/src/multiSemaphore/release/index.test.ts +18 -0
  76. package/redis-semaphore/test/src/mutex/acquire.test.ts +78 -0
  77. package/redis-semaphore/test/src/mutex/refresh.test.ts +22 -0
  78. package/redis-semaphore/test/src/mutex/release.test.ts +17 -0
  79. package/redis-semaphore/test/src/redlockMutex/acquire.test.ts +90 -0
  80. package/redis-semaphore/test/src/redlockMutex/refresh.test.ts +27 -0
  81. package/redis-semaphore/test/src/redlockMutex/release.test.ts +17 -0
  82. package/redis-semaphore/test/src/semaphore/acquire/index.test.ts +49 -0
  83. package/redis-semaphore/test/src/semaphore/acquire/internal.test.ts +65 -0
  84. package/redis-semaphore/test/src/semaphore/refresh/index.test.ts +44 -0
  85. package/redis-semaphore/test/src/semaphore/release.test.ts +18 -0
  86. package/redis-semaphore/test/src/utils/eval.test.ts +22 -0
  87. package/redis-semaphore/test/src/utils/index.test.ts +19 -0
  88. package/redis-semaphore/test/src/utils/redlock.test.ts +31 -0
  89. package/redis-semaphore/test/unhandledRejection.ts +28 -0
  90. package/redis-semaphore/tsconfig.build-commonjs.json +9 -0
  91. package/redis-semaphore/tsconfig.build-es.json +9 -0
  92. package/redis-semaphore/tsconfig.json +11 -0
  93. package/redis-semaphore/yarn.lock +5338 -0
  94. package/src/Backing.ts +87 -0
  95. package/src/DistributedSemaphore.ts +384 -0
  96. package/src/Errors.ts +3 -39
  97. package/src/RedisBacking.ts +167 -165
  98. package/src/index.ts +28 -17
  99. package/src/DistributedMutex.ts +0 -304
@@ -1,304 +0,0 @@
1
- import { Context, Duration, Effect, Option, Schedule, Scope } from "effect";
2
- import {
3
- AcquireTimeoutError,
4
- BackingError,
5
- LockLostError,
6
- NotYetAcquiredError,
7
- } from "./Errors.js";
8
-
9
- // =============================================================================
10
- // Backing Service
11
- // =============================================================================
12
-
13
- /**
14
- * Low-level backing store interface for distributed mutex operations.
15
- * Implementations handle the actual storage (Redis, etcd, DynamoDB, etc.)
16
- */
17
- export interface DistributedMutexBacking {
18
- /**
19
- * Try to acquire the lock. Returns true if acquired, false if already held.
20
- * Must set TTL on the lock.
21
- */
22
- readonly tryAcquire: (
23
- key: string,
24
- holderId: string,
25
- ttlMs: number
26
- ) => Effect.Effect<boolean, BackingError>;
27
-
28
- /**
29
- * Release the lock. Only succeeds if we are the current holder.
30
- * Returns true if released, false if we weren't the holder.
31
- */
32
- readonly release: (
33
- key: string,
34
- holderId: string
35
- ) => Effect.Effect<boolean, BackingError>;
36
-
37
- /**
38
- * Refresh the TTL on a lock we hold.
39
- * Returns true if refreshed, false if lock was lost.
40
- */
41
- readonly refresh: (
42
- key: string,
43
- holderId: string,
44
- ttlMs: number
45
- ) => Effect.Effect<boolean, BackingError>;
46
-
47
- /**
48
- * Check if the lock is currently held (by anyone).
49
- */
50
- readonly isLocked: (key: string) => Effect.Effect<boolean, BackingError>;
51
-
52
- /**
53
- * Get the current holder ID, if any.
54
- */
55
- readonly getHolder: (
56
- key: string
57
- ) => Effect.Effect<Option.Option<string>, BackingError>;
58
- }
59
-
60
- export const DistributedMutexBacking =
61
- Context.GenericTag<DistributedMutexBacking>(
62
- "@effect-distributed-lock/DistributedMutexBacking"
63
- );
64
-
65
- // =============================================================================
66
- // Mutex Configuration
67
- // =============================================================================
68
-
69
- export interface DistributedMutexConfig {
70
- /**
71
- * TTL for the lock. If the holder crashes, the lock auto-releases after this.
72
- * @default 30 seconds
73
- */
74
- readonly ttl?: Duration.DurationInput;
75
-
76
- /**
77
- * How often to refresh the TTL while holding the lock.
78
- * Should be less than TTL to avoid losing the lock.
79
- * @default 1/3 of TTL
80
- */
81
- readonly refreshInterval?: Duration.DurationInput;
82
-
83
- /**
84
- * How often to poll when waiting to acquire the lock.
85
- * @default 100ms
86
- */
87
- readonly retryInterval?: Duration.DurationInput;
88
-
89
- /**
90
- * Maximum time to wait when acquiring the lock.
91
- * If not set, will wait forever.
92
- */
93
- readonly acquireTimeout?: Duration.DurationInput;
94
- }
95
-
96
- const DEFAULT_TTL = Duration.seconds(30);
97
- const DEFAULT_RETRY_INTERVAL = Duration.millis(100);
98
-
99
- // =============================================================================
100
- // Distributed Mutex Interface
101
- // =============================================================================
102
-
103
- /**
104
- * A distributed mutex that can be used across multiple processes/services.
105
- */
106
- export interface DistributedMutex {
107
- /**
108
- * The key identifying this mutex in the backing store.
109
- */
110
- readonly key: string;
111
-
112
- /**
113
- * Acquire the lock, run the effect, then release.
114
- * If acquisition fails within timeout, returns AcquireTimeoutError.
115
- * The lock TTL is refreshed automatically while the effect runs.
116
- */
117
- readonly withLock: <A, E, R>(
118
- effect: Effect.Effect<A, E, R>
119
- ) => Effect.Effect<
120
- A,
121
- E | AcquireTimeoutError | LockLostError | BackingError,
122
- R
123
- >;
124
-
125
- /**
126
- * Try to acquire the lock immediately without waiting.
127
- * Returns Some(result) if lock was acquired and effect ran,
128
- * None if lock was not available.
129
- */
130
- readonly withLockIfAvailable: <A, E, R>(
131
- effect: Effect.Effect<A, E, R>
132
- ) => Effect.Effect<Option.Option<A>, E | LockLostError | BackingError, R>;
133
-
134
- /**
135
- * Acquire the lock, waiting if necessary.
136
- * The lock is held until the scope is closed.
137
- * The lock TTL is refreshed automatically while held.
138
- */
139
- readonly acquire: Effect.Effect<
140
- void,
141
- AcquireTimeoutError | LockLostError | BackingError,
142
- Scope.Scope
143
- >;
144
-
145
- /**
146
- * Try to acquire immediately without waiting.
147
- * Returns Some(void) if acquired (lock held until scope closes),
148
- * None if lock was not available.
149
- */
150
- readonly tryAcquire: Effect.Effect<
151
- Option.Option<void>,
152
- LockLostError | BackingError,
153
- Scope.Scope
154
- >;
155
-
156
- /**
157
- * Check if the lock is currently held.
158
- */
159
- readonly isLocked: Effect.Effect<boolean, BackingError>;
160
- }
161
-
162
- // =============================================================================
163
- // Factory
164
- // =============================================================================
165
-
166
- /**
167
- * Create a distributed mutex for the given key.
168
- */
169
- export const make = (
170
- key: string,
171
- config: DistributedMutexConfig = {}
172
- ): Effect.Effect<DistributedMutex, never, DistributedMutexBacking> =>
173
- Effect.gen(function* () {
174
- const backing = yield* DistributedMutexBacking;
175
-
176
- // Generate unique holder ID for this instance
177
- const holderId = crypto.randomUUID();
178
-
179
- // Resolve config with defaults
180
- const ttl = config.ttl ? Duration.decode(config.ttl) : DEFAULT_TTL;
181
- const ttlMs = Duration.toMillis(ttl);
182
- const refreshInterval = config.refreshInterval
183
- ? Duration.decode(config.refreshInterval)
184
- : Duration.millis(ttlMs / 3);
185
- const retryInterval = config.retryInterval
186
- ? Duration.decode(config.retryInterval)
187
- : DEFAULT_RETRY_INTERVAL;
188
- const acquireTimeout = config.acquireTimeout
189
- ? Option.some(Duration.decode(config.acquireTimeout))
190
- : Option.none<Duration.Duration>();
191
-
192
- // Keep the lock alive by refreshing TTL periodically.
193
- // This effect runs forever until interrupted (when scope closes).
194
- const keepAlive = Effect.repeat(
195
- Effect.gen(function* () {
196
- const refreshed = yield* backing.refresh(key, holderId, ttlMs);
197
- if (!refreshed) {
198
- return yield* new LockLostError({ key });
199
- }
200
- }),
201
- Schedule.spaced(refreshInterval)
202
- );
203
-
204
- // Try to acquire immediately, returns Option
205
- const tryAcquire: Effect.Effect<
206
- Option.Option<void>,
207
- LockLostError | BackingError,
208
- Scope.Scope
209
- > = Effect.gen(function* () {
210
- const acquired = yield* backing.tryAcquire(key, holderId, ttlMs);
211
- if (!acquired) {
212
- return Option.none();
213
- }
214
-
215
- // Start keepalive fiber, tied to this scope
216
- yield* Effect.forkScoped(keepAlive);
217
-
218
- // Add finalizer to release lock when scope closes
219
- yield* Effect.addFinalizer(() =>
220
- backing.release(key, holderId).pipe(Effect.ignore)
221
- );
222
-
223
- return Option.some(undefined as void);
224
- });
225
-
226
- // Acquire with retry/timeout, returns void when acquired
227
- const acquire: Effect.Effect<
228
- void,
229
- AcquireTimeoutError | LockLostError | BackingError,
230
- Scope.Scope
231
- > = Effect.gen(function* () {
232
- // Build retry schedule with optional timeout
233
- const schedule = Option.match(acquireTimeout, {
234
- onNone: () => Schedule.spaced(retryInterval),
235
- onSome: (timeout) =>
236
- Schedule.spaced(retryInterval).pipe(Schedule.upTo(timeout)),
237
- });
238
-
239
- // Retry until we acquire the lock
240
- yield* Effect.retry(
241
- Effect.gen(function* () {
242
- const maybeAcquired = yield* tryAcquire;
243
- if (Option.isNone(maybeAcquired)) {
244
- return yield* new NotYetAcquiredError();
245
- }
246
- }),
247
- schedule
248
- ).pipe(
249
- Effect.catchTag(
250
- "NotYetAcquiredError",
251
- () =>
252
- new AcquireTimeoutError({
253
- key,
254
- timeoutMs: Option.match(acquireTimeout, {
255
- onNone: () => -1,
256
- onSome: Duration.toMillis,
257
- }),
258
- })
259
- )
260
- );
261
- });
262
-
263
- // Convenience: acquire lock, run effect, release when done
264
- const withLock = <A, E, R>(
265
- effect: Effect.Effect<A, E, R>
266
- ): Effect.Effect<
267
- A,
268
- E | AcquireTimeoutError | LockLostError | BackingError,
269
- R
270
- > =>
271
- Effect.scoped(
272
- Effect.gen(function* () {
273
- yield* acquire;
274
- return yield* effect;
275
- })
276
- );
277
-
278
- // Convenience: try to acquire, run effect if successful
279
- const withLockIfAvailable = <A, E, R>(
280
- effect: Effect.Effect<A, E, R>
281
- ): Effect.Effect<Option.Option<A>, E | LockLostError | BackingError, R> =>
282
- Effect.scoped(
283
- Effect.gen(function* () {
284
- const maybeAcquired = yield* tryAcquire;
285
- if (Option.isNone(maybeAcquired)) {
286
- return Option.none<A>();
287
- }
288
- const result = yield* effect;
289
- return Option.some(result);
290
- })
291
- );
292
-
293
- const isLocked: Effect.Effect<boolean, BackingError> =
294
- backing.isLocked(key);
295
-
296
- return {
297
- key,
298
- withLock,
299
- withLockIfAvailable,
300
- acquire,
301
- tryAcquire,
302
- isLocked,
303
- } satisfies DistributedMutex;
304
- });