effect-distributed-lock 0.0.3 → 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 +90 -56
  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 -15
  97. package/src/RedisBacking.ts +165 -59
  98. package/src/index.ts +28 -12
  99. package/src/DistributedMutex.ts +0 -356
package/README.md CHANGED
@@ -1,14 +1,17 @@
1
1
  # effect-distributed-lock
2
2
 
3
- *WARNING: This is still in active development, likely has bugs and is subject to change.*
3
+ *WARNING: This is still in active development, possibly has bugs (distributed systems are hard!), and is subject to change.*
4
4
 
5
- A distributed mutex library for [Effect](https://effect.website/) with pluggable backends.
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!
6
8
 
7
9
  ## Features
8
10
 
9
- - **Scope-based resource management** — locks are automatically released when the scope closes
10
- - **Automatic TTL refresh** — keeps locks alive while held, prevents deadlocks if holder crashes
11
- - **Pluggable backends** — ships with Redis, easy to implement others (etcd, DynamoDB, etc.)
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
12
15
  - **Configurable retry policies** — control polling interval, TTL, and backing failure retry behavior
13
16
  - **Type-safe errors** — tagged errors for precise error handling
14
17
 
@@ -26,23 +29,25 @@ npm install ioredis
26
29
  ## Quick Start
27
30
 
28
31
  ```typescript
29
- import { Effect } from "effect";
32
+ import { Effect, Schedule } from "effect";
30
33
  import Redis from "ioredis";
31
- import { DistributedMutex, RedisBacking } from "effect-distributed-lock";
34
+ import { DistributedSemaphore, RedisBacking } from "effect-distributed-lock";
32
35
 
33
36
  const redis = new Redis(process.env.REDIS_URL);
34
37
  const RedisLayer = RedisBacking.layer(redis);
35
38
 
36
39
  const program = Effect.gen(function* () {
37
- const mutex = yield* DistributedMutex.make("my-resource", {
40
+ // Create a semaphore that allows 5 concurrent operations
41
+ const sem = yield* DistributedSemaphore.make("my-resource", {
42
+ limit: 5,
38
43
  ttl: "10 seconds",
39
44
  refreshInterval: "3 seconds",
40
45
  acquireRetryInterval: "500 millis",
41
- backingFailureRetryPolicy: Schedule.exponential("100 millis")),
46
+ backingFailureRetryPolicy: Schedule.exponential("100 millis"),
42
47
  });
43
48
 
44
- // Lock is held while effect runs, released automatically after
45
- yield* mutex.withLock(doExclusiveWork);
49
+ // Acquire 2 permits, run effect, release automatically after
50
+ yield* doWork.pipe(sem.withPermits(2));
46
51
  });
47
52
 
48
53
  program.pipe(Effect.provide(RedisLayer), Effect.runPromise);
@@ -50,62 +55,68 @@ program.pipe(Effect.provide(RedisLayer), Effect.runPromise);
50
55
 
51
56
  ## API
52
57
 
53
- ### Creating a Mutex
58
+ ### Creating a Semaphore
54
59
 
55
60
  ```typescript
56
- const mutex = yield* DistributedMutex.make(key, config);
61
+ const sem = yield* DistributedSemaphore.make(key, config);
57
62
  ```
58
63
 
59
- | Config Option | Type | Default | Description |
60
- | ---------------------- | ------------------ | ------------ | --------------------------------------------------- |
61
- | `ttl` | `DurationInput` | `30 seconds` | Lock TTL (auto-releases if holder crashes) |
62
- | `refreshInterval` | `DurationInput` | `ttl / 3` | How often to refresh TTL while holding |
63
- | `acquireRetryInterval` | `DurationInput` | `100ms` | Polling interval when waiting to acquire |
64
- | `backingFailureRetryPolicy` | `Schedule<void>` | `100ms` | Retry schedule for backing store failures |
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 |
65
71
 
66
- ### Using the Mutex
72
+ ### Using the Semaphore
67
73
 
68
- #### `withLock` — Acquire, run, release
74
+ #### `withPermits` — Acquire, run, release
69
75
 
70
- The simplest and recommended way. Acquires the lock (waiting indefinitely if needed), 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:
71
77
 
72
78
  ```typescript
73
- 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));
74
84
  ```
75
85
 
76
- #### `withLockIfAvailable` — Non-blocking acquire
86
+ #### `withPermitsIfAvailable` — Non-blocking acquire
77
87
 
78
- 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:
79
89
 
80
90
  ```typescript
81
- const result = yield* mutex.withLockIfAvailable(myEffect);
91
+ const result = yield* myEffect.pipe(sem.withPermitsIfAvailable(1));
82
92
  if (Option.isSome(result)) {
83
- console.log("Got the lock!", result.value);
93
+ console.log("Got the permit!", result.value);
84
94
  } else {
85
- console.log("Lock was busy, skipping");
95
+ console.log("No permits available, skipping");
86
96
  }
87
97
  ```
88
98
 
89
- #### `acquire` / `tryAcquire` — Manual scope control
99
+ #### `take` / `tryTake` — Manual scope control
90
100
 
91
- 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:
92
102
 
93
103
  ```typescript
94
104
  yield* Effect.scoped(
95
105
  Effect.gen(function* () {
96
- const keepAliveFiber = yield* mutex.acquire; // Lock held until scope closes
106
+ const keepAliveFiber = yield* sem.take(2); // 2 permits held until scope closes
97
107
  yield* doWork;
98
- // Lock automatically released + keepalive fiber interrupted here
108
+ // Permits automatically released + keepalive fiber interrupted here
99
109
  })
100
110
  );
101
111
  ```
102
112
 
103
- Both `acquire` and `tryAcquire` return the keepalive fiber that refreshes the lock TTL. Errors related to the keep alive (losing the lock or backing store failure) are propagated to the fiber.
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.
104
114
 
105
- #### `isLocked` — Check lock status
115
+ #### `currentCount` — Check held permits
106
116
 
107
117
  ```typescript
108
- 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
109
120
  ```
110
121
 
111
122
  ## Error Handling
@@ -113,44 +124,67 @@ const locked = yield* mutex.isLocked;
113
124
  All errors are tagged for precise handling with `Effect.catchTag`:
114
125
 
115
126
  ```typescript
116
- yield* mutex.withLock(myEffect).pipe(
127
+ yield* myEffect.pipe(
128
+ sem.withPermits(1),
117
129
  Effect.catchTag("LockLostError", (e) =>
118
- Effect.log(`Lock was lost while held: ${e.key}`)
130
+ Effect.log(`Permits were lost: ${e.key}`)
119
131
  ),
120
- Effect.catchTag("MutexBackingError", (e) =>
132
+ Effect.catchTag("SemaphoreBackingError", (e) =>
121
133
  Effect.log(`Redis error: ${e.message}`)
122
134
  )
123
135
  );
124
136
  ```
125
137
 
126
- | Error | Description |
127
- | ------------------- | ---------------------------------------------------- |
128
- | `LockLostError` | Lock TTL expired while we thought we held it |
129
- | `MutexBackingError` | 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.
130
157
 
131
158
  ## Custom Backends
132
159
 
133
- Implement the `DistributedMutexBacking` interface to use a different store:
160
+ Implement the `DistributedSemaphoreBacking` interface to use a different store:
134
161
 
135
162
  ```typescript
136
- import { Duration, Layer } from "effect";
137
- import { DistributedMutex } from "effect-distributed-lock";
138
-
139
- const MyCustomBacking = Layer.succeed(DistributedMutex.DistributedMutexBacking, {
140
- tryAcquire: (key, holderId, ttl: Duration.Duration) => /* ... */,
141
- release: (key, holderId) => /* ... */,
142
- refresh: (key, holderId, ttl: Duration.Duration) => /* ... */,
143
- isLocked: (key) => /* ... */,
144
- 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
145
178
  });
146
179
  ```
147
180
 
148
181
  ## How It Works
149
182
 
150
- 1. **Acquire**: Atomically sets the lock key if not exists (Redis: `SET key value NX PX ttl`)
151
- 2. **Keepalive**: A background fiber refreshes the TTL periodically while the lock is held
152
- 3. **Release**: Atomically deletes the key only if we're still the holder (Lua script for atomicity)
153
- 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
154
188
 
155
189
  ## License
156
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.3",
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.