effect-distributed-lock 0.0.6 → 0.0.8

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 (95) hide show
  1. package/examples/push.ts +109 -0
  2. package/package.json +1 -1
  3. package/src/DistributedSemaphore.ts +7 -8
  4. package/src/RedisBacking.ts +4 -4
  5. package/redis-semaphore/.codeclimate.yml +0 -5
  6. package/redis-semaphore/.fossa.yml +0 -14
  7. package/redis-semaphore/.github/dependabot.yml +0 -6
  8. package/redis-semaphore/.github/workflows/branches.yml +0 -39
  9. package/redis-semaphore/.github/workflows/pull-requests.yml +0 -35
  10. package/redis-semaphore/.mocharc.yaml +0 -6
  11. package/redis-semaphore/.prettierrc +0 -6
  12. package/redis-semaphore/.snyk +0 -4
  13. package/redis-semaphore/.yarnrc.yml +0 -2
  14. package/redis-semaphore/CHANGELOG.md +0 -70
  15. package/redis-semaphore/Dockerfile +0 -5
  16. package/redis-semaphore/LICENSE +0 -21
  17. package/redis-semaphore/README.md +0 -445
  18. package/redis-semaphore/docker-compose.yml +0 -31
  19. package/redis-semaphore/eslint.config.mjs +0 -73
  20. package/redis-semaphore/package.json +0 -79
  21. package/redis-semaphore/setup-redis-servers.sh +0 -2
  22. package/redis-semaphore/src/Lock.ts +0 -172
  23. package/redis-semaphore/src/RedisMultiSemaphore.ts +0 -56
  24. package/redis-semaphore/src/RedisMutex.ts +0 -45
  25. package/redis-semaphore/src/RedisSemaphore.ts +0 -49
  26. package/redis-semaphore/src/RedlockMultiSemaphore.ts +0 -56
  27. package/redis-semaphore/src/RedlockMutex.ts +0 -52
  28. package/redis-semaphore/src/RedlockSemaphore.ts +0 -49
  29. package/redis-semaphore/src/errors/LostLockError.ts +0 -1
  30. package/redis-semaphore/src/errors/TimeoutError.ts +0 -1
  31. package/redis-semaphore/src/index.ts +0 -23
  32. package/redis-semaphore/src/misc.ts +0 -12
  33. package/redis-semaphore/src/multiSemaphore/acquire/index.ts +0 -53
  34. package/redis-semaphore/src/multiSemaphore/acquire/lua.ts +0 -31
  35. package/redis-semaphore/src/multiSemaphore/refresh/index.ts +0 -32
  36. package/redis-semaphore/src/multiSemaphore/refresh/lua.ts +0 -31
  37. package/redis-semaphore/src/multiSemaphore/release/index.ts +0 -22
  38. package/redis-semaphore/src/multiSemaphore/release/lua.ts +0 -17
  39. package/redis-semaphore/src/mutex/acquire.ts +0 -42
  40. package/redis-semaphore/src/mutex/refresh.ts +0 -37
  41. package/redis-semaphore/src/mutex/release.ts +0 -30
  42. package/redis-semaphore/src/redlockMultiSemaphore/acquire.ts +0 -56
  43. package/redis-semaphore/src/redlockMultiSemaphore/refresh.ts +0 -68
  44. package/redis-semaphore/src/redlockMultiSemaphore/release.ts +0 -19
  45. package/redis-semaphore/src/redlockMutex/acquire.ts +0 -54
  46. package/redis-semaphore/src/redlockMutex/refresh.ts +0 -53
  47. package/redis-semaphore/src/redlockMutex/release.ts +0 -19
  48. package/redis-semaphore/src/redlockSemaphore/acquire.ts +0 -55
  49. package/redis-semaphore/src/redlockSemaphore/refresh.ts +0 -60
  50. package/redis-semaphore/src/redlockSemaphore/release.ts +0 -18
  51. package/redis-semaphore/src/semaphore/acquire/index.ts +0 -52
  52. package/redis-semaphore/src/semaphore/acquire/lua.ts +0 -25
  53. package/redis-semaphore/src/semaphore/refresh/index.ts +0 -31
  54. package/redis-semaphore/src/semaphore/refresh/lua.ts +0 -25
  55. package/redis-semaphore/src/semaphore/release.ts +0 -14
  56. package/redis-semaphore/src/types.ts +0 -63
  57. package/redis-semaphore/src/utils/createEval.ts +0 -45
  58. package/redis-semaphore/src/utils/index.ts +0 -13
  59. package/redis-semaphore/src/utils/redlock.ts +0 -7
  60. package/redis-semaphore/test/init.test.ts +0 -9
  61. package/redis-semaphore/test/redisClient.ts +0 -82
  62. package/redis-semaphore/test/setup.ts +0 -6
  63. package/redis-semaphore/test/shell.test.ts +0 -15
  64. package/redis-semaphore/test/shell.ts +0 -48
  65. package/redis-semaphore/test/src/Lock.test.ts +0 -37
  66. package/redis-semaphore/test/src/RedisMultiSemaphore.test.ts +0 -425
  67. package/redis-semaphore/test/src/RedisMutex.test.ts +0 -334
  68. package/redis-semaphore/test/src/RedisSemaphore.test.ts +0 -367
  69. package/redis-semaphore/test/src/RedlockMultiSemaphore.test.ts +0 -671
  70. package/redis-semaphore/test/src/RedlockMutex.test.ts +0 -328
  71. package/redis-semaphore/test/src/RedlockSemaphore.test.ts +0 -579
  72. package/redis-semaphore/test/src/index.test.ts +0 -22
  73. package/redis-semaphore/test/src/multiSemaphore/acquire/index.test.ts +0 -51
  74. package/redis-semaphore/test/src/multiSemaphore/acquire/internal.test.ts +0 -67
  75. package/redis-semaphore/test/src/multiSemaphore/refresh/index.test.ts +0 -52
  76. package/redis-semaphore/test/src/multiSemaphore/release/index.test.ts +0 -18
  77. package/redis-semaphore/test/src/mutex/acquire.test.ts +0 -78
  78. package/redis-semaphore/test/src/mutex/refresh.test.ts +0 -22
  79. package/redis-semaphore/test/src/mutex/release.test.ts +0 -17
  80. package/redis-semaphore/test/src/redlockMutex/acquire.test.ts +0 -90
  81. package/redis-semaphore/test/src/redlockMutex/refresh.test.ts +0 -27
  82. package/redis-semaphore/test/src/redlockMutex/release.test.ts +0 -17
  83. package/redis-semaphore/test/src/semaphore/acquire/index.test.ts +0 -49
  84. package/redis-semaphore/test/src/semaphore/acquire/internal.test.ts +0 -65
  85. package/redis-semaphore/test/src/semaphore/refresh/index.test.ts +0 -44
  86. package/redis-semaphore/test/src/semaphore/release.test.ts +0 -18
  87. package/redis-semaphore/test/src/utils/eval.test.ts +0 -22
  88. package/redis-semaphore/test/src/utils/index.test.ts +0 -19
  89. package/redis-semaphore/test/src/utils/redlock.test.ts +0 -31
  90. package/redis-semaphore/test/unhandledRejection.ts +0 -28
  91. package/redis-semaphore/tsconfig.build-commonjs.json +0 -9
  92. package/redis-semaphore/tsconfig.build-es.json +0 -9
  93. package/redis-semaphore/tsconfig.json +0 -11
  94. package/redis-semaphore/yarn.lock +0 -5338
  95. /package/examples/{index.ts → kitchen-sink.ts} +0 -0
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Demonstrates concurrent effects competing for a distributed lock.
3
+ *
4
+ * This example runs two scenarios:
5
+ * 1. With push-based acquisition DISABLED (polling only)
6
+ * 2. With push-based acquisition ENABLED (pub/sub notifications)
7
+ *
8
+ * You'll see how push-based acquisition is faster because waiters are
9
+ * notified immediately when permits are released, rather than polling.
10
+ *
11
+ * Run with: bun run examples/concurrent.ts
12
+ * Requires REDIS_URL environment variable or local Redis at localhost:6379.
13
+ */
14
+ import { Console, Duration, Effect, Schedule } from "effect";
15
+ import Redis from "ioredis";
16
+ import { DistributedSemaphore, RedisBacking } from "../src/index.ts";
17
+
18
+ const redis = new Redis(process.env.REDIS_URL ?? "redis://localhost:6379");
19
+
20
+ // Helper to create a task that competes for the lock
21
+ const makeTask = (
22
+ id: number,
23
+ mutex: DistributedSemaphore.DistributedSemaphore
24
+ ) =>
25
+ Effect.gen(function* () {
26
+ yield* Console.log(`[Task ${id}] Starting, waiting for lock...`);
27
+ const startWait = Date.now();
28
+
29
+ yield* mutex.withPermits(1)(
30
+ Effect.gen(function* () {
31
+ const waitTime = Date.now() - startWait;
32
+ yield* Console.log(
33
+ `[Task ${id}] 🔒 Lock acquired! (waited ${waitTime}ms)`
34
+ );
35
+
36
+ // Simulate some work
37
+ yield* Effect.sleep(Duration.millis(200));
38
+
39
+ yield* Console.log(`[Task ${id}] 🔓 Releasing lock...`);
40
+ })
41
+ );
42
+
43
+ yield* Console.log(`[Task ${id}] Done`);
44
+ });
45
+
46
+ // Run a scenario with the given configuration
47
+ const runScenario = (name: string, pushEnabled: boolean) =>
48
+ Effect.gen(function* () {
49
+ yield* Console.log(`\n${"=".repeat(60)}`);
50
+ yield* Console.log(`${name}`);
51
+ yield* Console.log(`Push-based acquisition: ${pushEnabled ? "ON" : "OFF"}`);
52
+ yield* Console.log(`${"=".repeat(60)}\n`);
53
+
54
+ const startTime = Date.now();
55
+
56
+ // Create mutex with a unique key per scenario to avoid interference
57
+ const mutex = yield* DistributedSemaphore.make(
58
+ `concurrent-example-${pushEnabled ? "push" : "poll"}`,
59
+ {
60
+ acquireRetryPolicy: Schedule.spaced(Duration.millis(500)),
61
+ limit: 1, // Mutex - only one holder at a time
62
+ }
63
+ );
64
+
65
+ // Run 3 tasks concurrently, all competing for the same lock
66
+ yield* Effect.all(
67
+ [makeTask(1, mutex), makeTask(2, mutex), makeTask(3, mutex)],
68
+ { concurrency: 3 }
69
+ );
70
+
71
+ const totalTime = Date.now() - startTime;
72
+ yield* Console.log(`\n⏱️ Total time: ${totalTime}ms\n`);
73
+ });
74
+
75
+ // Run both scenarios
76
+ const main = Effect.gen(function* () {
77
+ yield* Console.log("🚀 Distributed Lock Concurrency Demo");
78
+ yield* Console.log(
79
+ "Showing 3 concurrent tasks competing for a mutex (limit=1)"
80
+ );
81
+
82
+ // Run WITHOUT push (polling only)
83
+ const RedisLayerNoPush = RedisBacking.layer(redis, {
84
+ keyPrefix: "concurrent-demo:",
85
+ pushBasedAcquireEnabled: false,
86
+ });
87
+ yield* runScenario("Scenario 1: Polling Only", false).pipe(
88
+ Effect.provide(RedisLayerNoPush)
89
+ );
90
+
91
+ // Run WITH push (pub/sub notifications)
92
+ const RedisLayerWithPush = RedisBacking.layer(redis, {
93
+ keyPrefix: "concurrent-demo:",
94
+ pushBasedAcquireEnabled: true,
95
+ });
96
+ yield* runScenario("Scenario 2: Push-Based (Pub/Sub)", true).pipe(
97
+ Effect.provide(RedisLayerWithPush)
98
+ );
99
+
100
+ yield* Console.log("✅ Demo complete!");
101
+ yield* Console.log(
102
+ "Notice how push-based acquisition completes faster because"
103
+ );
104
+ yield* Console.log(
105
+ "waiters are notified immediately instead of waiting for the next poll.\n"
106
+ );
107
+ }).pipe(Effect.ensuring(Effect.promise(() => redis.quit())));
108
+
109
+ Effect.runPromise(main).catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-distributed-lock",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "A distributed semaphore library for Effect with pluggable backends",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -44,7 +44,7 @@ export interface DistributedSemaphoreConfig {
44
44
  * How often to poll when waiting to acquire permits.
45
45
  * @default Schedule.spaced(Duration.millis(100))
46
46
  */
47
- readonly acquireRetryPolicy?: Schedule.Schedule<void>;
47
+ readonly acquireRetryPolicy?: Schedule.Schedule<unknown>;
48
48
 
49
49
  /**
50
50
  * Retry policy when a backing failure occurs.
@@ -54,15 +54,13 @@ export interface DistributedSemaphoreConfig {
54
54
  * - Releasing permits
55
55
  * @default Schedule.recurs(3)
56
56
  */
57
- readonly backingFailureRetryPolicy?: Schedule.Schedule<void>;
57
+ readonly backingFailureRetryPolicy?: Schedule.Schedule<unknown>;
58
58
  }
59
59
 
60
60
  const DEFAULT_LIMIT = 1;
61
61
  const DEFAULT_TTL = Duration.seconds(30);
62
- const DEFAULT_ACQUIRE_RETRY_POLICY = Schedule.spaced(Duration.millis(100)).pipe(
63
- Schedule.asVoid
64
- );
65
- const DEFAULT_FAILURE_RETRY_POLICY = Schedule.recurs(3).pipe(Schedule.asVoid);
62
+ const DEFAULT_ACQUIRE_RETRY_POLICY = Schedule.spaced(Duration.millis(100));
63
+ const DEFAULT_FAILURE_RETRY_POLICY = Schedule.recurs(3);
66
64
 
67
65
  // =============================================================================
68
66
  // Acquire Options
@@ -222,8 +220,8 @@ type FullyResolvedConfig = {
222
220
  limit: number;
223
221
  ttl: Duration.Duration;
224
222
  refreshInterval: Duration.Duration;
225
- acquireRetryPolicy: Schedule.Schedule<void>;
226
- backingFailureRetryPolicy: Schedule.Schedule<void>;
223
+ acquireRetryPolicy: Schedule.Schedule<unknown>;
224
+ backingFailureRetryPolicy: Schedule.Schedule<unknown>;
227
225
  };
228
226
 
229
227
  function fullyResolveConfig(
@@ -445,6 +443,7 @@ export const make = (
445
443
  })
446
444
  : Effect.never;
447
445
 
446
+ // first to succeed (acquire permits) wins
448
447
  return yield* Effect.race(pollBasedAcquire, pushBasedAcquire);
449
448
  });
450
449
 
@@ -177,7 +177,7 @@ export interface RedisBackingOptions {
177
177
  * How often to retry the stream of notifications when permits are released.
178
178
  * @default Schedule.forever
179
179
  */
180
- readonly pushStreamRetrySchedule?: Schedule.Schedule<void>;
180
+ readonly pushStreamRetrySchedule?: Schedule.Schedule<unknown>;
181
181
  }
182
182
 
183
183
  /**
@@ -198,7 +198,7 @@ export const layer = (
198
198
  const keyPrefix = options.keyPrefix ?? "semaphore:";
199
199
  const pushBasedAcquireEnabled = options.pushBasedAcquireEnabled ?? true;
200
200
  const pushStreamRetrySchedule =
201
- options.pushStreamRetrySchedule ?? Schedule.forever.pipe(Schedule.asVoid);
201
+ options.pushStreamRetrySchedule ?? Schedule.forever;
202
202
  const prefixKey = (key: string) => `${keyPrefix}${key}`;
203
203
  const releaseChannel = (key: string) => `${keyPrefix}${key}:released`;
204
204
 
@@ -325,9 +325,9 @@ export const layer = (
325
325
  return { subscriber, messageHandler };
326
326
  }),
327
327
  ({ subscriber, messageHandler }) =>
328
- Effect.sync(() => {
328
+ Effect.promise(async () => {
329
329
  subscriber.off("message", messageHandler);
330
- subscriber.unsubscribe(channel);
330
+ await subscriber.unsubscribe(channel);
331
331
  subscriber.disconnect();
332
332
  })
333
333
  );
@@ -1,5 +0,0 @@
1
- version: '2'
2
- plugins:
3
- eslint:
4
- enabled: true
5
- channel: 'eslint-6'
@@ -1,14 +0,0 @@
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: .
@@ -1,6 +0,0 @@
1
- version: 2
2
- updates:
3
- - package-ecosystem: 'npm'
4
- directory: '/'
5
- schedule:
6
- interval: 'monthly'
@@ -1,39 +0,0 @@
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
@@ -1,35 +0,0 @@
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
@@ -1,6 +0,0 @@
1
- extension: ts
2
- recursive: true
3
- timeout: 5s
4
- require:
5
- - '@swc-node/register'
6
- - test/setup.ts
@@ -1,6 +0,0 @@
1
- {
2
- "semi": false,
3
- "singleQuote": true,
4
- "trailingComma": "none",
5
- "arrowParens": "avoid"
6
- }
@@ -1,4 +0,0 @@
1
- # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
2
- version: v1.13.1
3
- ignore: {}
4
- patch: {}
@@ -1,2 +0,0 @@
1
- nodeLinker: node-modules
2
- defaultSemverRangePrefix: ''
@@ -1,70 +0,0 @@
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
@@ -1,5 +0,0 @@
1
- FROM node:alpine
2
- RUN npm i -g @swarthy/wait-for@2.0.2
3
- VOLUME /app
4
- WORKDIR /app
5
- USER node
@@ -1,21 +0,0 @@
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.