effect-distributed-lock 0.0.1 → 0.0.2

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.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ethan Niser
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.
22
+
package/README.md CHANGED
@@ -1,15 +1,160 @@
1
1
  # effect-distributed-lock
2
2
 
3
- To install dependencies:
3
+ A distributed mutex library for [Effect](https://effect.website/) with pluggable backends.
4
+
5
+ ## Features
6
+
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
+ - **Type-safe errors** — tagged errors for precise error handling
12
+
13
+ ## Installation
4
14
 
5
15
  ```bash
6
- bun install
16
+ npm install effect-distributed-lock effect
17
+ # or
18
+ bun add effect-distributed-lock effect
19
+
20
+ # For Redis backing (optional)
21
+ npm install ioredis
7
22
  ```
8
23
 
9
- To run:
24
+ ## Quick Start
10
25
 
11
- ```bash
12
- bun run index.ts
26
+ ```typescript
27
+ import { Effect } from "effect";
28
+ import Redis from "ioredis";
29
+ import { DistributedMutex, RedisBacking } from "effect-distributed-lock";
30
+
31
+ const redis = new Redis(process.env.REDIS_URL);
32
+ const RedisLayer = RedisBacking.layer(redis);
33
+
34
+ const program = Effect.gen(function* () {
35
+ const mutex = yield* DistributedMutex.make("my-resource", {
36
+ ttl: "30 seconds",
37
+ acquireTimeout: "10 seconds",
38
+ });
39
+
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
+ );
47
+ });
48
+
49
+ program.pipe(Effect.provide(RedisLayer), Effect.runPromise);
50
+ ```
51
+
52
+ ## API
53
+
54
+ ### Creating a Mutex
55
+
56
+ ```typescript
57
+ const mutex = yield* DistributedMutex.make(key, config);
13
58
  ```
14
59
 
15
- This project was created using `bun init` in bun v1.2.12. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
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) |
66
+
67
+ ### Using the Mutex
68
+
69
+ #### `withLock` — Acquire, run, release
70
+
71
+ The simplest and recommended way. Acquires the lock, runs your effect, and releases when done:
72
+
73
+ ```typescript
74
+ yield* mutex.withLock(myEffect);
75
+ ```
76
+
77
+ #### `withLockIfAvailable` — Non-blocking acquire
78
+
79
+ Tries to acquire immediately without waiting. Returns `Option.some(result)` if successful, `Option.none()` if lock is held:
80
+
81
+ ```typescript
82
+ const result = yield* mutex.withLockIfAvailable(myEffect);
83
+ if (Option.isSome(result)) {
84
+ console.log("Got the lock!", result.value);
85
+ } else {
86
+ console.log("Lock was busy, skipping");
87
+ }
88
+ ```
89
+
90
+ #### `acquire` / `tryAcquire` — Manual scope control
91
+
92
+ For advanced use cases where you need explicit control over the lock lifecycle:
93
+
94
+ ```typescript
95
+ yield* Effect.scoped(
96
+ Effect.gen(function* () {
97
+ yield* mutex.acquire; // Lock held until scope closes
98
+ yield* doWork();
99
+ // Lock automatically released here
100
+ })
101
+ );
102
+ ```
103
+
104
+ #### `isLocked` — Check lock status
105
+
106
+ ```typescript
107
+ const locked = yield* mutex.isLocked;
108
+ ```
109
+
110
+ ## Error Handling
111
+
112
+ All errors are tagged for precise handling with `Effect.catchTag`:
113
+
114
+ ```typescript
115
+ yield* mutex.withLock(myEffect).pipe(
116
+ Effect.catchTag("AcquireTimeoutError", (e) =>
117
+ Effect.log(`Timed out acquiring lock: ${e.key}`)
118
+ ),
119
+ Effect.catchTag("LockLostError", (e) =>
120
+ Effect.log(`Lock was lost while held: ${e.key}`)
121
+ ),
122
+ Effect.catchTag("BackingError", (e) =>
123
+ Effect.log(`Redis error: ${e.message}`)
124
+ )
125
+ );
126
+ ```
127
+
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) |
133
+
134
+ ## Custom Backends
135
+
136
+ Implement the `DistributedMutexBacking` interface to use a different store:
137
+
138
+ ```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) => /* ... */,
148
+ });
149
+ ```
150
+
151
+ ## How It Works
152
+
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
157
+
158
+ ## License
159
+
160
+ MIT
package/package.json CHANGED
@@ -1,7 +1,16 @@
1
1
  {
2
2
  "name": "effect-distributed-lock",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "A distributed mutex library for Effect with pluggable backends",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/ethanniser/effect-distributed-lock.git"
9
+ },
10
+ "homepage": "https://github.com/ethanniser/effect-distributed-lock",
11
+ "bugs": {
12
+ "url": "https://github.com/ethanniser/effect-distributed-lock/issues"
13
+ },
5
14
  "type": "module",
6
15
  "scripts": {
7
16
  "build": "tsc",
@@ -1,5 +1,5 @@
1
1
  import { Effect, Layer, Option } from "effect";
2
- import type { Redis } from "ioredis";
2
+ import { Redis } from "ioredis";
3
3
  import { DistributedMutexBacking } from "./DistributedMutex.js";
4
4
  import { BackingError } from "./Errors.js";
5
5
 
@@ -162,16 +162,6 @@ export const layerFromUrl = (
162
162
  Layer.scoped(
163
163
  DistributedMutexBacking,
164
164
  Effect.gen(function* () {
165
- // Dynamic import to avoid requiring ioredis at module load time
166
- const { default: Redis } = yield* Effect.tryPromise({
167
- try: () => import("ioredis"),
168
- catch: (cause) =>
169
- new BackingError({
170
- operation: "import",
171
- cause: `Failed to import ioredis: ${cause}`,
172
- }),
173
- });
174
-
175
165
  const redis = new Redis(url);
176
166
 
177
167
  // Ensure cleanup on scope close
package/src/index.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * @example
7
7
  * ```ts
8
- * import { DistributedMutex, makeRedisBackingLayer } from "effect-distributed-lock";
8
+ * import { DistributedMutex, RedisBacking } from "effect-distributed-lock";
9
9
  * import { Effect } from "effect";
10
10
  * import Redis from "ioredis";
11
11
  *
@@ -28,7 +28,7 @@
28
28
  * });
29
29
  *
30
30
  * program.pipe(
31
- * Effect.provide(makeRedisBackingLayer(redis)),
31
+ * Effect.provide(RedisBacking.layer(redis)),
32
32
  * Effect.runPromise
33
33
  * );
34
34
  * ```
package/tsconfig.json CHANGED
@@ -3,13 +3,13 @@
3
3
  // Environment setup & latest features
4
4
  "lib": ["ESNext"],
5
5
  "target": "ESNext",
6
- "module": "ESNext",
6
+ "module": "NodeNext",
7
7
  "moduleDetection": "force",
8
8
  "jsx": "react-jsx",
9
9
  "allowJs": true,
10
10
 
11
11
  // Bundler mode
12
- "moduleResolution": "bundler",
12
+ "moduleResolution": "nodenext",
13
13
  "verbatimModuleSyntax": true,
14
14
 
15
15
  // Build output