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,19 +1,50 @@
1
- import { Effect, Layer, Option } from "effect";
1
+ import { Duration, Effect, Layer } from "effect";
2
2
  import { Redis } from "ioredis";
3
- import { DistributedMutexBacking } from "./DistributedMutex.js";
4
- import { BackingError } from "./Errors.js";
3
+ import {
4
+ DistributedSemaphoreBacking,
5
+ SemaphoreBackingError,
6
+ } from "./Backing.js";
5
7
 
6
8
  /**
7
- * Lua script for atomic lock acquisition.
8
- * SET key value NX PX ttl - set only if not exists, with TTL
9
+ * Lua script for atomic semaphore acquisition.
10
+ *
11
+ * Uses a sorted set where:
12
+ * - Each member is `holderId_permitIndex` (e.g., "abc123_0", "abc123_1")
13
+ * - Score is the acquisition timestamp
14
+ * - Expired entries are removed before checking capacity
15
+ *
16
+ * Arguments:
17
+ * - KEYS[1]: the semaphore key
18
+ * - ARGV[1]: limit (max permits)
19
+ * - ARGV[2]: permits to acquire
20
+ * - ARGV[3]: holderId
21
+ * - ARGV[4]: ttlMs (lock timeout in ms)
22
+ * - ARGV[5]: now (current timestamp in ms)
23
+ *
24
+ * Returns 1 if acquired, 0 if not enough permits available.
9
25
  */
10
26
  const ACQUIRE_SCRIPT = `
11
27
  local key = KEYS[1]
12
- local holderId = ARGV[1]
13
- local ttlMs = tonumber(ARGV[2])
28
+ local limit = tonumber(ARGV[1])
29
+ local permits = tonumber(ARGV[2])
30
+ local holderId = ARGV[3]
31
+ local ttlMs = tonumber(ARGV[4])
32
+ local now = tonumber(ARGV[5])
33
+ local expiredTimestamp = now - ttlMs
14
34
 
15
- local result = redis.call('SET', key, holderId, 'NX', 'PX', ttlMs)
16
- if result then
35
+ -- Remove expired entries
36
+ redis.call('zremrangebyscore', key, '-inf', expiredTimestamp)
37
+
38
+ -- Check if there's room for the requested permits
39
+ if (redis.call('zcard', key) + permits) <= limit then
40
+ -- Add all permits with current timestamp
41
+ local args = {}
42
+ for i = 0, permits - 1 do
43
+ table.insert(args, now)
44
+ table.insert(args, holderId .. '_' .. i)
45
+ end
46
+ redis.call('zadd', key, unpack(args))
47
+ redis.call('pexpire', key, ttlMs)
17
48
  return 1
18
49
  else
19
50
  return 0
@@ -21,34 +52,68 @@ end
21
52
  `;
22
53
 
23
54
  /**
24
- * Lua script for atomic lock release.
25
- * Only deletes if we are the current holder.
55
+ * Lua script for atomic release.
56
+ *
57
+ * Removes all permits held by this holder.
58
+ *
59
+ * Arguments:
60
+ * - KEYS[1]: the semaphore key
61
+ * - ARGV[1]: permits to release
62
+ * - ARGV[2]: holderId
63
+ *
64
+ * Returns the number of permits released.
26
65
  */
27
66
  const RELEASE_SCRIPT = `
28
67
  local key = KEYS[1]
29
- local holderId = ARGV[1]
68
+ local permits = tonumber(ARGV[1])
69
+ local holderId = ARGV[2]
70
+ local args = {}
30
71
 
31
- local currentHolder = redis.call('GET', key)
32
- if currentHolder == holderId then
33
- redis.call('DEL', key)
34
- return 1
35
- else
36
- return 0
72
+ for i = 0, permits - 1 do
73
+ table.insert(args, holderId .. '_' .. i)
37
74
  end
75
+
76
+ return redis.call('zrem', key, unpack(args))
38
77
  `;
39
78
 
40
79
  /**
41
80
  * Lua script for atomic TTL refresh.
42
- * Only refreshes if we are the current holder.
81
+ *
82
+ * Updates the timestamp (score) for all permits held by this holder.
83
+ * Returns 0 if the holder doesn't have any permits (lock was lost).
84
+ *
85
+ * Arguments:
86
+ * - KEYS[1]: the semaphore key
87
+ * - ARGV[1]: limit (for consistency, though not strictly needed for refresh)
88
+ * - ARGV[2]: permits
89
+ * - ARGV[3]: holderId
90
+ * - ARGV[4]: ttlMs
91
+ * - ARGV[5]: now
92
+ *
93
+ * Returns 1 if refreshed, 0 if permits were lost.
43
94
  */
44
95
  const REFRESH_SCRIPT = `
45
96
  local key = KEYS[1]
46
- local holderId = ARGV[1]
47
- local ttlMs = tonumber(ARGV[2])
97
+ local limit = tonumber(ARGV[1])
98
+ local permits = tonumber(ARGV[2])
99
+ local holderId = ARGV[3]
100
+ local ttlMs = tonumber(ARGV[4])
101
+ local now = tonumber(ARGV[5])
102
+ local expiredTimestamp = now - ttlMs
103
+
104
+ -- Remove expired entries
105
+ redis.call('zremrangebyscore', key, '-inf', expiredTimestamp)
48
106
 
49
- local currentHolder = redis.call('GET', key)
50
- if currentHolder == holderId then
51
- redis.call('PEXPIRE', key, ttlMs)
107
+ -- Check if we still hold the first permit (indicator that we still own it)
108
+ if redis.call('zscore', key, holderId .. '_0') then
109
+ -- Update all permits with new timestamp
110
+ local args = {}
111
+ for i = 0, permits - 1 do
112
+ table.insert(args, now)
113
+ table.insert(args, holderId .. '_' .. i)
114
+ end
115
+ redis.call('zadd', key, unpack(args))
116
+ redis.call('pexpire', key, ttlMs)
52
117
  return 1
53
118
  else
54
119
  return 0
@@ -56,203 +121,140 @@ end
56
121
  `;
57
122
 
58
123
  /**
59
- * Create a Redis-backed distributed mutex backing layer.
124
+ * Lua script to get the current count of active permits.
125
+ *
126
+ * Arguments:
127
+ * - KEYS[1]: the semaphore key
128
+ * - ARGV[1]: ttlMs
129
+ * - ARGV[2]: now
130
+ *
131
+ * Returns the number of active (non-expired) permits.
132
+ */
133
+ const GET_COUNT_SCRIPT = `
134
+ local key = KEYS[1]
135
+ local ttlMs = tonumber(ARGV[1])
136
+ local now = tonumber(ARGV[2])
137
+ local expiredTimestamp = now - ttlMs
138
+
139
+ -- Remove expired entries
140
+ redis.call('zremrangebyscore', key, '-inf', expiredTimestamp)
141
+
142
+ return redis.call('zcard', key)
143
+ `;
144
+
145
+ /**
146
+ * Create a Redis-backed distributed semaphore backing layer.
60
147
  *
61
- * @param redis - An ioredis client instance
62
- * @param keyPrefix - Optional prefix for all keys (default: "dmutex:")
148
+ * **Important:** This implementation is for single-instance Redis only.
149
+ * It does not implement the Redlock algorithm and should not be used with
150
+ * Redis Cluster or Redis Sentinel for distributed locking guarantees.
151
+ * For multi-instance Redis, consider implementing a Redlock-based backing.
152
+ *
153
+ * @param redis - An ioredis client instance (single instance, not cluster)
154
+ * @param keyPrefix - Optional prefix for all keys (default: "dsem:")
63
155
  */
64
156
  export const layer = (
65
157
  redis: Redis,
66
- keyPrefix = "dmutex:"
67
- ): Layer.Layer<DistributedMutexBacking> => {
158
+ keyPrefix = "dsem:"
159
+ ): Layer.Layer<DistributedSemaphoreBacking> => {
68
160
  const prefixKey = (key: string) => `${keyPrefix}${key}`;
69
161
 
70
162
  const tryAcquire = (
71
163
  key: string,
72
164
  holderId: string,
73
- ttlMs: number
74
- ): Effect.Effect<boolean, BackingError> =>
165
+ ttl: Duration.Duration,
166
+ limit: number,
167
+ permits: number
168
+ ): Effect.Effect<boolean, SemaphoreBackingError> =>
75
169
  Effect.tryPromise({
76
170
  try: async () => {
171
+ const now = Date.now();
77
172
  const result = await redis.eval(
78
173
  ACQUIRE_SCRIPT,
79
174
  1,
80
175
  prefixKey(key),
176
+ limit.toString(),
177
+ permits.toString(),
81
178
  holderId,
82
- ttlMs.toString()
179
+ Duration.toMillis(ttl).toString(),
180
+ now.toString()
83
181
  );
84
182
  return result === 1;
85
183
  },
86
- catch: (cause) => new BackingError({ operation: "tryAcquire", cause }),
184
+ catch: (cause) =>
185
+ new SemaphoreBackingError({ operation: "tryAcquire", cause }),
87
186
  });
88
187
 
89
188
  const release = (
90
189
  key: string,
91
- holderId: string
92
- ): Effect.Effect<boolean, BackingError> =>
190
+ holderId: string,
191
+ permits: number
192
+ ): Effect.Effect<number, SemaphoreBackingError> =>
93
193
  Effect.tryPromise({
94
194
  try: async () => {
95
195
  const result = await redis.eval(
96
196
  RELEASE_SCRIPT,
97
197
  1,
98
198
  prefixKey(key),
199
+ permits.toString(),
99
200
  holderId
100
201
  );
101
- return result === 1;
202
+ return result as number;
102
203
  },
103
- catch: (cause) => new BackingError({ operation: "release", cause }),
204
+ catch: (cause) =>
205
+ new SemaphoreBackingError({ operation: "release", cause }),
104
206
  });
105
207
 
106
208
  const refresh = (
107
209
  key: string,
108
210
  holderId: string,
109
- ttlMs: number
110
- ): Effect.Effect<boolean, BackingError> =>
211
+ ttl: Duration.Duration,
212
+ limit: number,
213
+ permits: number
214
+ ): Effect.Effect<boolean, SemaphoreBackingError> =>
111
215
  Effect.tryPromise({
112
216
  try: async () => {
217
+ const now = Date.now();
113
218
  const result = await redis.eval(
114
219
  REFRESH_SCRIPT,
115
220
  1,
116
221
  prefixKey(key),
222
+ limit.toString(),
223
+ permits.toString(),
117
224
  holderId,
118
- ttlMs.toString()
225
+ Duration.toMillis(ttl).toString(),
226
+ now.toString()
119
227
  );
120
228
  return result === 1;
121
229
  },
122
- catch: (cause) => new BackingError({ operation: "refresh", cause }),
123
- });
124
-
125
- const isLocked = (key: string): Effect.Effect<boolean, BackingError> =>
126
- Effect.tryPromise({
127
- try: async () => {
128
- const exists = await redis.exists(prefixKey(key));
129
- return exists === 1;
130
- },
131
- catch: (cause) => new BackingError({ operation: "isLocked", cause }),
230
+ catch: (cause) =>
231
+ new SemaphoreBackingError({ operation: "refresh", cause }),
132
232
  });
133
233
 
134
- const getHolder = (
135
- key: string
136
- ): Effect.Effect<Option.Option<string>, BackingError> =>
234
+ const getCount = (
235
+ key: string,
236
+ ttl: Duration.Duration
237
+ ): Effect.Effect<number, SemaphoreBackingError> =>
137
238
  Effect.tryPromise({
138
239
  try: async () => {
139
- const holder = await redis.get(prefixKey(key));
140
- return holder ? Option.some(holder) : Option.none();
240
+ const now = Date.now();
241
+ const result = await redis.eval(
242
+ GET_COUNT_SCRIPT,
243
+ 1,
244
+ prefixKey(key),
245
+ Duration.toMillis(ttl).toString(),
246
+ now.toString()
247
+ );
248
+ return result as number;
141
249
  },
142
- catch: (cause) => new BackingError({ operation: "getHolder", cause }),
250
+ catch: (cause) =>
251
+ new SemaphoreBackingError({ operation: "getCount", cause }),
143
252
  });
144
253
 
145
- return Layer.succeed(DistributedMutexBacking, {
254
+ return Layer.succeed(DistributedSemaphoreBacking, {
146
255
  tryAcquire,
147
256
  release,
148
257
  refresh,
149
- isLocked,
150
- getHolder,
258
+ getCount,
151
259
  });
152
260
  };
153
-
154
- /**
155
- * Create a Redis backing from a connection URL.
156
- * This creates and manages the Redis connection lifecycle.
157
- */
158
- export const layerFromUrl = (
159
- url: string,
160
- keyPrefix = "dmutex:"
161
- ): Layer.Layer<DistributedMutexBacking, BackingError> =>
162
- Layer.scoped(
163
- DistributedMutexBacking,
164
- Effect.gen(function* () {
165
- const redis = new Redis(url);
166
-
167
- // Ensure cleanup on scope close
168
- yield* Effect.addFinalizer(() =>
169
- Effect.promise(() => redis.quit().catch(() => {}))
170
- );
171
-
172
- const prefixKey = (key: string) => `${keyPrefix}${key}`;
173
-
174
- const tryAcquire = (
175
- key: string,
176
- holderId: string,
177
- ttlMs: number
178
- ): Effect.Effect<boolean, BackingError> =>
179
- Effect.tryPromise({
180
- try: async () => {
181
- const result = await redis.eval(
182
- ACQUIRE_SCRIPT,
183
- 1,
184
- prefixKey(key),
185
- holderId,
186
- ttlMs.toString()
187
- );
188
- return result === 1;
189
- },
190
- catch: (cause) =>
191
- new BackingError({ operation: "tryAcquire", cause }),
192
- });
193
-
194
- const release = (
195
- key: string,
196
- holderId: string
197
- ): Effect.Effect<boolean, BackingError> =>
198
- Effect.tryPromise({
199
- try: async () => {
200
- const result = await redis.eval(
201
- RELEASE_SCRIPT,
202
- 1,
203
- prefixKey(key),
204
- holderId
205
- );
206
- return result === 1;
207
- },
208
- catch: (cause) => new BackingError({ operation: "release", cause }),
209
- });
210
-
211
- const refresh = (
212
- key: string,
213
- holderId: string,
214
- ttlMs: number
215
- ): Effect.Effect<boolean, BackingError> =>
216
- Effect.tryPromise({
217
- try: async () => {
218
- const result = await redis.eval(
219
- REFRESH_SCRIPT,
220
- 1,
221
- prefixKey(key),
222
- holderId,
223
- ttlMs.toString()
224
- );
225
- return result === 1;
226
- },
227
- catch: (cause) => new BackingError({ operation: "refresh", cause }),
228
- });
229
-
230
- const isLocked = (key: string): Effect.Effect<boolean, BackingError> =>
231
- Effect.tryPromise({
232
- try: async () => {
233
- const exists = await redis.exists(prefixKey(key));
234
- return exists === 1;
235
- },
236
- catch: (cause) => new BackingError({ operation: "isLocked", cause }),
237
- });
238
-
239
- const getHolder = (
240
- key: string
241
- ): Effect.Effect<Option.Option<string>, BackingError> =>
242
- Effect.tryPromise({
243
- try: async () => {
244
- const holder = await redis.get(prefixKey(key));
245
- return holder ? Option.some(holder) : Option.none();
246
- },
247
- catch: (cause) => new BackingError({ operation: "getHolder", cause }),
248
- });
249
-
250
- return {
251
- tryAcquire,
252
- release,
253
- refresh,
254
- isLocked,
255
- getHolder,
256
- } satisfies DistributedMutexBacking;
257
- })
258
- );
package/src/index.ts CHANGED
@@ -1,30 +1,38 @@
1
1
  /**
2
- * Effect Distributed Lock
2
+ * Effect Distributed Semaphore
3
3
  *
4
- * A distributed mutex library for Effect with pluggable backends.
4
+ * A distributed semaphore library for Effect with pluggable backends.
5
+ * Implements a multi-semaphore that can be used to implement:
6
+ * - Mutex (limit=1, permits=1)
7
+ * - Semaphore (limit=N, permits=1)
8
+ * - Multi-semaphore (limit=N, permits=M)
5
9
  *
6
10
  * @example
7
11
  * ```ts
8
- * import { DistributedMutex, RedisBacking } from "effect-distributed-lock";
12
+ * import { DistributedSemaphore, RedisBacking } from "effect-distributed-lock";
9
13
  * import { Effect } from "effect";
10
14
  * import Redis from "ioredis";
11
15
  *
12
16
  * const redis = new Redis(process.env.REDIS_URL);
13
17
  *
14
18
  * const program = Effect.gen(function* () {
15
- * // Create a mutex for a specific resource
16
- * const mutex = yield* DistributedMutex.make("my-resource-lock", {
19
+ * // Create a semaphore that allows 5 concurrent operations
20
+ * const sem = yield* DistributedSemaphore.make("my-resource", {
21
+ * limit: 5,
17
22
  * ttl: "30 seconds",
18
- * acquireTimeout: "10 seconds",
19
23
  * });
20
24
  *
21
- * // Use the lock
22
- * yield* mutex.withLock(
25
+ * // Acquire 2 permits, run effect, release when done
26
+ * yield* sem.withPermits(2)(
23
27
  * Effect.gen(function* () {
24
- * // Critical section - only one process can be here at a time
25
- * yield* doSomethingExclusive();
28
+ * // Only 2 of the 5 slots are used
29
+ * yield* doSomethingLimited();
26
30
  * })
27
31
  * );
32
+ *
33
+ * // For mutex behavior, use limit=1 and withPermits(1)
34
+ * const mutex = yield* DistributedSemaphore.make("my-lock", { limit: 1 });
35
+ * yield* mutex.withPermits(1)(criticalSection);
28
36
  * });
29
37
  *
30
38
  * program.pipe(
@@ -36,15 +44,18 @@
36
44
  * @module
37
45
  */
38
46
 
47
+ // Backing interface
48
+ export * as Backing from "./Backing.js";
49
+ export {
50
+ DistributedSemaphoreBacking,
51
+ SemaphoreBackingError,
52
+ } from "./Backing.js";
53
+
39
54
  // Core module (namespace with types and functions)
40
- export * as DistributedMutex from "./DistributedMutex.js";
55
+ export * as DistributedSemaphore from "./DistributedSemaphore.js";
56
+
41
57
  // Errors
42
- export {
43
- AcquireTimeoutError,
44
- BackingError,
45
- DistributedMutexError,
46
- LockLostError,
47
- } from "./Errors.js";
58
+ export { LockLostError } from "./Errors.js";
48
59
 
49
60
  // Redis backing
50
61
  export * as RedisBacking from "./RedisBacking.js";