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
package/README.md CHANGED
@@ -1,13 +1,18 @@
1
1
  # effect-distributed-lock
2
2
 
3
- A distributed mutex library for [Effect](https://effect.website/) with pluggable backends.
3
+ *WARNING: This is still in active development, possibly has bugs (distributed systems are hard!), and is subject to change.*
4
+
5
+ A distributed semaphore library for [Effect](https://effect.website/) with pluggable backends.
6
+
7
+ It's like the built in `Effect.Semaphore`, but asynchronously distributed across multiple processes/services!
4
8
 
5
9
  ## Features
6
10
 
7
- - **Scope-based resource management** — locks are automatically released when the scope closes
8
- - **Automatic TTL refresh** — keeps locks alive while held, prevents deadlocks if holder crashes
9
- - **Pluggable backends** — ships with Redis, easy to implement others (etcd, DynamoDB, etc.)
10
- - **Configurable retry & timeout** — control polling interval, acquire timeout, and TTL
11
+ - **Distributed semaphore** — control concurrent access across multiple processes/services
12
+ - **Scope-based resource management** — permits are automatically released when the scope closes
13
+ - **Automatic TTL refresh** — keeps permits alive while held, prevents deadlocks if holder crashes
14
+ - **Pluggable backends** — ships with Redis (single-instance), easy to implement others
15
+ - **Configurable retry policies** — control polling interval, TTL, and backing failure retry behavior
11
16
  - **Type-safe errors** — tagged errors for precise error handling
12
17
 
13
18
  ## Installation
@@ -24,26 +29,25 @@ npm install ioredis
24
29
  ## Quick Start
25
30
 
26
31
  ```typescript
27
- import { Effect } from "effect";
32
+ import { Effect, Schedule } from "effect";
28
33
  import Redis from "ioredis";
29
- import { DistributedMutex, RedisBacking } from "effect-distributed-lock";
34
+ import { DistributedSemaphore, RedisBacking } from "effect-distributed-lock";
30
35
 
31
36
  const redis = new Redis(process.env.REDIS_URL);
32
37
  const RedisLayer = RedisBacking.layer(redis);
33
38
 
34
39
  const program = Effect.gen(function* () {
35
- const mutex = yield* DistributedMutex.make("my-resource", {
36
- ttl: "30 seconds",
37
- acquireTimeout: "10 seconds",
40
+ // Create a semaphore that allows 5 concurrent operations
41
+ const sem = yield* DistributedSemaphore.make("my-resource", {
42
+ limit: 5,
43
+ ttl: "10 seconds",
44
+ refreshInterval: "3 seconds",
45
+ acquireRetryInterval: "500 millis",
46
+ backingFailureRetryPolicy: Schedule.exponential("100 millis"),
38
47
  });
39
48
 
40
- // Lock is held while effect runs, released automatically after
41
- yield* mutex.withLock(
42
- Effect.gen(function* () {
43
- // Critical section - only one process can be here at a time
44
- yield* doExclusiveWork();
45
- })
46
- );
49
+ // Acquire 2 permits, run effect, release automatically after
50
+ yield* doWork.pipe(sem.withPermits(2));
47
51
  });
48
52
 
49
53
  program.pipe(Effect.provide(RedisLayer), Effect.runPromise);
@@ -51,60 +55,68 @@ program.pipe(Effect.provide(RedisLayer), Effect.runPromise);
51
55
 
52
56
  ## API
53
57
 
54
- ### Creating a Mutex
58
+ ### Creating a Semaphore
55
59
 
56
60
  ```typescript
57
- const mutex = yield* DistributedMutex.make(key, config);
61
+ const sem = yield* DistributedSemaphore.make(key, config);
58
62
  ```
59
63
 
60
- | Config Option | Type | Default | Description |
61
- | ----------------- | ---------------- | ------------ | ------------------------------------------------ |
62
- | `ttl` | `DurationInput` | `30 seconds` | Lock TTL (auto-releases if holder crashes) |
63
- | `refreshInterval` | `DurationInput` | `ttl / 3` | How often to refresh TTL while holding |
64
- | `retryInterval` | `DurationInput` | `100ms` | Polling interval when waiting to acquire |
65
- | `acquireTimeout` | `DurationInput` | `∞` | Max time to wait for lock (fails if exceeded) |
64
+ | Config Option | Type | Default | Description |
65
+ | ---------------------------- | ---------------- | ------------ | ------------------------------------------ |
66
+ | `limit` | `number` | `1` | Max permits (1 = mutex behavior) |
67
+ | `ttl` | `DurationInput` | `30 seconds` | Permit TTL (auto-releases if holder crashes) |
68
+ | `refreshInterval` | `DurationInput` | `ttl / 3` | How often to refresh TTL while holding |
69
+ | `acquireRetryInterval` | `DurationInput` | `100ms` | Polling interval when waiting to acquire |
70
+ | `backingFailureRetryPolicy` | `Schedule<void>` | `100ms` | Retry schedule for backing store failures |
66
71
 
67
- ### Using the Mutex
72
+ ### Using the Semaphore
68
73
 
69
- #### `withLock` — Acquire, run, release
74
+ #### `withPermits` — Acquire, run, release
70
75
 
71
- The simplest and recommended way. Acquires the lock, runs your effect, and releases when done:
76
+ The simplest and recommended way. Acquires permits (waiting if needed), runs your effect, and releases when done:
72
77
 
73
78
  ```typescript
74
- yield* mutex.withLock(myEffect);
79
+ // Acquire 2 permits out of limit
80
+ yield* myEffect.pipe(sem.withPermits(2));
81
+
82
+ // For mutex behavior (limit=1), use withPermits(1)
83
+ yield* criticalSection.pipe(mutex.withPermits(1));
75
84
  ```
76
85
 
77
- #### `withLockIfAvailable` — Non-blocking acquire
86
+ #### `withPermitsIfAvailable` — Non-blocking acquire
78
87
 
79
- Tries to acquire immediately without waiting. Returns `Option.some(result)` if successful, `Option.none()` if lock is held:
88
+ Tries to acquire immediately without waiting. Returns `Option.some(result)` if successful, `Option.none()` if not enough permits:
80
89
 
81
90
  ```typescript
82
- const result = yield* mutex.withLockIfAvailable(myEffect);
91
+ const result = yield* myEffect.pipe(sem.withPermitsIfAvailable(1));
83
92
  if (Option.isSome(result)) {
84
- console.log("Got the lock!", result.value);
93
+ console.log("Got the permit!", result.value);
85
94
  } else {
86
- console.log("Lock was busy, skipping");
95
+ console.log("No permits available, skipping");
87
96
  }
88
97
  ```
89
98
 
90
- #### `acquire` / `tryAcquire` — Manual scope control
99
+ #### `take` / `tryTake` — Manual scope control
91
100
 
92
- For advanced use cases where you need explicit control over the lock lifecycle:
101
+ For advanced use cases where you need explicit control over the permit lifecycle:
93
102
 
94
103
  ```typescript
95
104
  yield* Effect.scoped(
96
105
  Effect.gen(function* () {
97
- yield* mutex.acquire; // Lock held until scope closes
98
- yield* doWork();
99
- // Lock automatically released here
106
+ const keepAliveFiber = yield* sem.take(2); // 2 permits held until scope closes
107
+ yield* doWork;
108
+ // Permits automatically released + keepalive fiber interrupted here
100
109
  })
101
110
  );
102
111
  ```
103
112
 
104
- #### `isLocked` Check lock status
113
+ 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
+
115
+ #### `currentCount` — Check held permits
105
116
 
106
117
  ```typescript
107
- const locked = yield* mutex.isLocked;
118
+ const held = yield* sem.currentCount; // Number of permits currently held
119
+ const available = sem.limit - held; // Number of permits available
108
120
  ```
109
121
 
110
122
  ## Error Handling
@@ -112,48 +124,67 @@ const locked = yield* mutex.isLocked;
112
124
  All errors are tagged for precise handling with `Effect.catchTag`:
113
125
 
114
126
  ```typescript
115
- yield* mutex.withLock(myEffect).pipe(
116
- Effect.catchTag("AcquireTimeoutError", (e) =>
117
- Effect.log(`Timed out acquiring lock: ${e.key}`)
118
- ),
127
+ yield* myEffect.pipe(
128
+ sem.withPermits(1),
119
129
  Effect.catchTag("LockLostError", (e) =>
120
- Effect.log(`Lock was lost while held: ${e.key}`)
130
+ Effect.log(`Permits were lost: ${e.key}`)
121
131
  ),
122
- Effect.catchTag("BackingError", (e) =>
132
+ Effect.catchTag("SemaphoreBackingError", (e) =>
123
133
  Effect.log(`Redis error: ${e.message}`)
124
134
  )
125
135
  );
126
136
  ```
127
137
 
128
- | Error | Description |
129
- | --------------------- | ---------------------------------------------------- |
130
- | `AcquireTimeoutError` | Failed to acquire lock within the timeout period |
131
- | `LockLostError` | Lock TTL expired while we thought we held it |
132
- | `BackingError` | Error from the backing store (Redis connection, etc) |
138
+ | Error | Description |
139
+ | ----------------------- | ---------------------------------------------------- |
140
+ | `LockLostError` | Permit TTL expired while we thought we held it |
141
+ | `SemaphoreBackingError` | Error from the backing store (Redis connection, etc) |
142
+
143
+ ## Redis Backing (Single-Instance Only)
144
+
145
+ The provided Redis backing is designed for **single-instance Redis only**. It does not implement the [Redlock algorithm](https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/) and should not be used with Redis Cluster or Redis Sentinel when you need strong distributed locking guarantees.
146
+
147
+ ```typescript
148
+ import Redis from "ioredis";
149
+ import { RedisBacking } from "effect-distributed-lock";
150
+
151
+ // Single Redis instance
152
+ const redis = new Redis("redis://localhost:6379");
153
+ const RedisLayer = RedisBacking.layer(redis, "my-prefix:");
154
+ ```
155
+
156
+ For multi-instance Redis deployments requiring Redlock, you'll need to implement a custom backing.
133
157
 
134
158
  ## Custom Backends
135
159
 
136
- Implement the `DistributedMutexBacking` interface to use a different store:
160
+ Implement the `DistributedSemaphoreBacking` interface to use a different store:
137
161
 
138
162
  ```typescript
139
- import { Layer } from "effect";
140
- import { DistributedMutex } from "effect-distributed-lock";
141
-
142
- const MyCustomBacking = Layer.succeed(DistributedMutex.DistributedMutexBacking, {
143
- tryAcquire: (key, holderId, ttlMs) => /* ... */,
144
- release: (key, holderId) => /* ... */,
145
- refresh: (key, holderId, ttlMs) => /* ... */,
146
- isLocked: (key) => /* ... */,
147
- getHolder: (key) => /* ... */,
163
+ import { Duration, Effect, Layer, Option } from "effect";
164
+ import { Backing, DistributedSemaphoreBacking } from "effect-distributed-lock";
165
+
166
+ const MyCustomBacking = Layer.succeed(DistributedSemaphoreBacking, {
167
+ tryAcquire: (key, holderId, ttl, limit, permits) =>
168
+ Effect.succeed(true), // Try to acquire permits
169
+
170
+ release: (key, holderId, permits) =>
171
+ Effect.succeed(permits), // Release permits, return count released
172
+
173
+ refresh: (key, holderId, ttl, limit, permits) =>
174
+ Effect.succeed(true), // Refresh TTL, return false if lost
175
+
176
+ getCount: (key, ttl) =>
177
+ Effect.succeed(0), // Return number of permits currently held
148
178
  });
149
179
  ```
150
180
 
151
181
  ## How It Works
152
182
 
153
- 1. **Acquire**: Atomically sets the lock key if not exists (Redis: `SET key value NX PX ttl`)
154
- 2. **Keepalive**: A background fiber refreshes the TTL periodically while the lock is held
155
- 3. **Release**: Atomically deletes the key only if we're still the holder (Lua script for atomicity)
156
- 4. **Crash safety**: If the holder crashes, the TTL expires and another process can acquire
183
+ 1. **Acquire**: Atomically adds permits to a sorted set if there's room (Redis: Lua script with `ZADD`)
184
+ 2. **Keepalive**: A background fiber refreshes the TTL periodically by updating timestamps
185
+ 3. **Release**: Atomically removes permits from the sorted set (Lua script with `ZREM`)
186
+ 4. **Expiration**: Expired entries (based on TTL) are cleaned up on each operation
187
+ 5. **Crash safety**: If the holder crashes, permits expire and become available
157
188
 
158
189
  ## License
159
190
 
package/examples/index.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Example usage of the distributed mutex library.
2
+ * Example usage of the distributed semaphore library.
3
3
  *
4
4
  * Run with: bun run example.ts
5
5
  * Requires REDIS_URL environment variable.
6
6
  */
7
- import { Effect, Console, Duration } from "effect";
7
+ import { Effect, Console, Duration, Option } from "effect";
8
8
  import Redis from "ioredis";
9
- import { DistributedMutex, RedisBacking } from "../src/index.ts";
9
+ import { DistributedSemaphore, RedisBacking } from "../src/index.ts";
10
10
 
11
11
  // Create Redis client
12
12
  const redis = new Redis(process.env.REDIS_URL ?? "redis://localhost:6379");
@@ -14,16 +14,16 @@ const redis = new Redis(process.env.REDIS_URL ?? "redis://localhost:6379");
14
14
  // Create the Redis backing layer
15
15
  const RedisLayer = RedisBacking.layer(redis, "example:");
16
16
 
17
- // Example 1: Using withLock for a critical section
17
+ // Example 1: Using withPermits for a critical section (mutex behavior)
18
18
  const example1 = Effect.gen(function* () {
19
- yield* Console.log("=== Example 1: withLock ===");
19
+ yield* Console.log("=== Example 1: Mutex with withPermits(1) ===");
20
20
 
21
- const mutex = yield* DistributedMutex.make("my-resource", {
21
+ const mutex = yield* DistributedSemaphore.make("my-resource", {
22
+ limit: 1, // Acts as a mutex
22
23
  ttl: Duration.seconds(10),
23
- acquireTimeout: Duration.seconds(5),
24
24
  });
25
25
 
26
- yield* mutex.withLock(
26
+ yield* mutex.withPermits(1)(
27
27
  Effect.gen(function* () {
28
28
  yield* Console.log("Lock acquired! Doing critical work...");
29
29
  yield* Effect.sleep(2000);
@@ -34,45 +34,72 @@ const example1 = Effect.gen(function* () {
34
34
  yield* Console.log("Lock released automatically");
35
35
  });
36
36
 
37
- // Example 2: Using tryAcquire (non-blocking)
37
+ // Example 2: Using withPermitsIfAvailable (non-blocking)
38
38
  const example2 = Effect.gen(function* () {
39
- yield* Console.log("\n=== Example 2: withLockIfAvailable ===");
39
+ yield* Console.log("\n=== Example 2: withPermitsIfAvailable ===");
40
40
 
41
- const mutex = yield* DistributedMutex.make("another-resource", {
41
+ const mutex = yield* DistributedSemaphore.make("another-resource", {
42
+ limit: 1,
42
43
  ttl: Duration.seconds(10),
43
44
  });
44
45
 
45
- const result = yield* mutex.withLockIfAvailable(
46
+ const result = yield* mutex.withPermitsIfAvailable(1)(
46
47
  Effect.gen(function* () {
47
48
  yield* Console.log("Got the lock without waiting!");
48
49
  return "success";
49
50
  })
50
51
  );
51
52
 
52
- yield* Console.log(`Result: ${JSON.stringify(result)}`);
53
+ if (Option.isSome(result)) {
54
+ yield* Console.log(`Result: ${result.value}`);
55
+ } else {
56
+ yield* Console.log("Could not acquire lock immediately");
57
+ }
53
58
  });
54
59
 
55
- // Example 3: Manual scope management with acquire()
60
+ // Example 3: Semaphore with multiple permits
56
61
  const example3 = Effect.gen(function* () {
57
- yield* Console.log("\n=== Example 3: Manual acquire with Scope ===");
62
+ yield* Console.log("\n=== Example 3: Semaphore with limit=5 ===");
58
63
 
59
- const mutex = yield* DistributedMutex.make("manual-resource", {
64
+ const sem = yield* DistributedSemaphore.make("pool-resource", {
65
+ limit: 5, // Allow 5 concurrent operations
66
+ ttl: Duration.seconds(10),
67
+ });
68
+
69
+ // Acquire 2 permits out of 5
70
+ yield* sem.withPermits(2)(
71
+ Effect.gen(function* () {
72
+ yield* Console.log("Acquired 2 permits (3 still available)");
73
+ const count = yield* sem.currentCount;
74
+ yield* Console.log(`Current active permits: ${count}`);
75
+ yield* Effect.sleep(1000);
76
+ })
77
+ );
78
+
79
+ yield* Console.log("Released 2 permits");
80
+ });
81
+
82
+ // Example 4: Manual scope management with take()
83
+ const example4 = Effect.gen(function* () {
84
+ yield* Console.log("\n=== Example 4: Manual acquire with Scope ===");
85
+
86
+ const mutex = yield* DistributedSemaphore.make("manual-resource", {
87
+ limit: 1,
60
88
  ttl: Duration.seconds(10),
61
- acquireTimeout: Duration.seconds(5),
62
89
  });
63
90
 
64
91
  // Using Effect.scoped to manage the lock lifecycle
65
92
  yield* Effect.scoped(
66
93
  Effect.gen(function* () {
67
- yield* mutex.acquire;
68
- yield* Console.log("Lock acquired via acquire()");
94
+ yield* mutex.take(1);
95
+ yield* Console.log("Permit acquired via take(1)");
69
96
  yield* Effect.sleep(1000);
70
97
  yield* Console.log("About to exit scope...");
71
- // Lock is automatically released when scope closes
98
+ // Permits are automatically released when scope closes
72
99
  })
73
100
  );
74
101
 
75
- yield* Console.log("Scope closed, lock released");
102
+ yield* Console.log("Scope closed, permit released");
76
103
  });
77
104
 
78
105
  // Run all examples
@@ -80,6 +107,7 @@ const main = Effect.gen(function* () {
80
107
  yield* example1;
81
108
  yield* example2;
82
109
  yield* example3;
110
+ yield* example4;
83
111
  yield* Console.log("\n✓ All examples completed!");
84
112
  }).pipe(
85
113
  Effect.ensuring(Effect.promise(() => redis.quit())),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "effect-distributed-lock",
3
- "version": "0.0.2",
4
- "description": "A distributed mutex library for Effect with pluggable backends",
3
+ "version": "0.0.4",
4
+ "description": "A distributed semaphore library for Effect with pluggable backends",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -0,0 +1,5 @@
1
+ version: '2'
2
+ plugins:
3
+ eslint:
4
+ enabled: true
5
+ channel: 'eslint-6'
@@ -0,0 +1,14 @@
1
+ # Generated by FOSSA CLI (https://github.com/fossas/fossa-cli)
2
+ # Visit https://fossa.com to learn more
3
+
4
+ version: 2
5
+ cli:
6
+ server: https://app.fossa.com
7
+ fetcher: custom
8
+ project: git@github.com:swarthy/redis-semaphore.git
9
+ analyze:
10
+ modules:
11
+ - name: .
12
+ type: npm
13
+ target: .
14
+ path: .
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: 'npm'
4
+ directory: '/'
5
+ schedule:
6
+ interval: 'monthly'
@@ -0,0 +1,39 @@
1
+ name: CI (push)
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ integration-test:
11
+ runs-on: ubuntu-latest
12
+
13
+ strategy:
14
+ matrix:
15
+ node-version: [18.x, 20.x, 22.x]
16
+
17
+ env:
18
+ COVERALLS_REPO_TOKEN: '${{ secrets.COVERALLS_REPO_TOKEN }}'
19
+ COVERALLS_GIT_BRANCH: '${{ github.ref }}'
20
+
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - name: Enable Corepack
24
+ run: corepack enable
25
+
26
+ - name: Use Node.js ${{ matrix.node-version }}
27
+ uses: actions/setup-node@v4
28
+ with:
29
+ node-version: ${{ matrix.node-version }}
30
+ cache: 'yarn'
31
+
32
+ - run: yarn install --immutable
33
+
34
+ - run: docker compose up -d redis1 redis2 redis3
35
+ - run: docker compose run waiter
36
+
37
+ - run: yarn build
38
+ - run: yarn lint
39
+ - run: yarn test-ci-with-coverage
@@ -0,0 +1,35 @@
1
+ name: CI (PR)
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - master
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ integration-test:
11
+ runs-on: ubuntu-latest
12
+
13
+ strategy:
14
+ matrix:
15
+ node-version: [18.x, 20.x, 22.x]
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - name: Enable Corepack
20
+ run: corepack enable
21
+
22
+ - name: Use Node.js ${{ matrix.node-version }}
23
+ uses: actions/setup-node@v4
24
+ with:
25
+ node-version: ${{ matrix.node-version }}
26
+ cache: 'yarn'
27
+
28
+ - run: yarn install --immutable
29
+
30
+ - run: docker compose up -d redis1 redis2 redis3
31
+ - run: docker compose run waiter
32
+
33
+ - run: yarn build
34
+ - run: yarn lint
35
+ - run: yarn test
@@ -0,0 +1,6 @@
1
+ extension: ts
2
+ recursive: true
3
+ timeout: 5s
4
+ require:
5
+ - '@swc-node/register'
6
+ - test/setup.ts
@@ -0,0 +1,6 @@
1
+ {
2
+ "semi": false,
3
+ "singleQuote": true,
4
+ "trailingComma": "none",
5
+ "arrowParens": "avoid"
6
+ }
@@ -0,0 +1,4 @@
1
+ # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
2
+ version: v1.13.1
3
+ ignore: {}
4
+ patch: {}
@@ -0,0 +1,2 @@
1
+ nodeLinker: node-modules
2
+ defaultSemverRangePrefix: ''
@@ -0,0 +1,70 @@
1
+ ### redis-semaphore@5.6.2
2
+ - Fixed implicit import from `src`
3
+ - Removed `src` folder from NPM package
4
+
5
+ ### redis-semaphore@5.6.1
6
+ - Removed `module` field from `package.json`
7
+
8
+ ### redis-semaphore@5.6.0
9
+ - Added interface compatible client support (ex. `ioredis-mock`)
10
+ - Removed `instanceof Redis` validation in constructor
11
+ - `ioredis` marked as optional peerDependency, explicit `ioredis` install is required now
12
+
13
+ ### redis-semaphore@5.5.1
14
+ - Fix race condition for refresh started before release and finished after release
15
+
16
+ ### redis-semaphore@5.5.0
17
+
18
+ - Added `identifier` constructor option.
19
+ - Added `acquiredExternally` constructor option.
20
+ - Option `externallyAcquiredIdentifier` **DEPRECATED**.
21
+ - Option `identifierSuffix` **DEPRECATED**.
22
+
23
+ ### redis-semaphore@5.4.0
24
+
25
+ - Added `identifierSuffix` option, usefull for tracing app instance which locked resource
26
+
27
+ ### redis-semaphore@5.3.1
28
+
29
+ - Fixed reacquire expired resource in refresh
30
+
31
+ ### redis-semaphore@5.3.0
32
+
33
+ - Added `stopRefresh` method
34
+ - Added `externallyAcquiredIdentifier` optional constructor option
35
+ - Removed `uuid` dependency
36
+
37
+ ### redis-semaphore@5.2.0
38
+
39
+ - Added `acquireAttemptsLimit` method
40
+
41
+ ### redis-semaphore@5.1.0
42
+
43
+ - Added `tryAcquire`
44
+
45
+ ### redis-semaphore@5.0.0
46
+
47
+ - **Breadking change:** Drop Node.js v10.x, v12.x support
48
+ - Added `ioredis@5` support
49
+
50
+ ### redis-semaphore@4.1.0
51
+
52
+ - Added `.isAcquired` property on all locks
53
+ - Added `onLostLock` constructor option. By default throws unhandled error.
54
+
55
+ ### redis-semaphore@4.0.0
56
+
57
+ - **Breaking change:** `Mutex`, `Semaphore`, `MultiSemaphore` not longer support `Cluster`. For multi-node case use `Redlock*` instead.
58
+ - Added `RedlockMutex`, `RedlockSemaphore`, `RedlockMultiSemaphore`
59
+ - Internals refactored
60
+
61
+ ### redis-semaphore@3.2.0
62
+
63
+ - Added `MultiSemaphore`
64
+
65
+ ### redis-semaphore@3.0.0
66
+
67
+ - **Breaking change:** `FairSemaphore` has been removed. Use `Semaphore` instead (has the same "fairness")
68
+ - the `acquire` method in `Semaphore` no longer returns a boolean. Instead, it throws an error if it cannot acquire, and if it doesn't throw, you can assume it worked.
69
+ - Internal code has been cleaned up
70
+ - Added more test, include synthetic node unsynchroned clocks
@@ -0,0 +1,5 @@
1
+ FROM node:alpine
2
+ RUN npm i -g @swarthy/wait-for@2.0.2
3
+ VOLUME /app
4
+ WORKDIR /app
5
+ USER node
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Alexander Mochalin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.