@lickle/lock 0.0.1-alpha.1 → 0.0.1-alpha.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/README.md +182 -76
- package/cjs.js +57 -57
- package/dist/cjs/core/guard.d.ts +50 -0
- package/dist/cjs/core/guard.d.ts.map +1 -0
- package/dist/cjs/{backend → core}/guard.js +43 -11
- package/dist/cjs/core/guard.js.map +1 -0
- package/dist/cjs/core/index.d.ts +8 -0
- package/dist/cjs/core/index.d.ts.map +1 -0
- package/dist/cjs/core/index.js +41 -0
- package/dist/cjs/core/index.js.map +1 -0
- package/dist/cjs/core/locking.d.ts +23 -0
- package/dist/cjs/core/locking.d.ts.map +1 -0
- package/dist/cjs/core/locking.js +93 -0
- package/dist/cjs/core/locking.js.map +1 -0
- package/dist/cjs/core/node.d.ts +19 -0
- package/dist/cjs/core/node.d.ts.map +1 -0
- package/dist/cjs/core/node.js +69 -0
- package/dist/cjs/core/node.js.map +1 -0
- package/dist/cjs/core/types.d.ts +49 -0
- package/dist/cjs/core/types.d.ts.map +1 -0
- package/dist/cjs/core/types.js +13 -0
- package/dist/cjs/core/types.js.map +1 -0
- package/dist/cjs/index.d.ts +43 -34
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +59 -39
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/core/guard.d.ts +50 -0
- package/dist/esm/core/guard.d.ts.map +1 -0
- package/dist/esm/{backend → core}/guard.js +40 -9
- package/dist/esm/core/guard.js.map +1 -0
- package/dist/esm/core/index.d.ts +8 -0
- package/dist/esm/core/index.d.ts.map +1 -0
- package/dist/esm/core/index.js +21 -0
- package/dist/esm/core/index.js.map +1 -0
- package/dist/esm/core/locking.d.ts +23 -0
- package/dist/esm/core/locking.d.ts.map +1 -0
- package/dist/esm/core/locking.js +56 -0
- package/dist/esm/core/locking.js.map +1 -0
- package/dist/esm/core/node.d.ts +19 -0
- package/dist/esm/core/node.d.ts.map +1 -0
- package/dist/esm/core/node.js +61 -0
- package/dist/esm/core/node.js.map +1 -0
- package/dist/esm/core/types.d.ts +49 -0
- package/dist/esm/core/types.d.ts.map +1 -0
- package/dist/esm/core/types.js +9 -0
- package/dist/esm/core/types.js.map +1 -0
- package/dist/esm/index.d.ts +43 -34
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +53 -34
- package/dist/esm/index.js.map +1 -1
- package/esm.js +58 -58
- package/index.d.ts +32 -8
- package/package.json +6 -6
- package/dist/cjs/backend/guard.d.ts +0 -31
- package/dist/cjs/backend/guard.d.ts.map +0 -1
- package/dist/cjs/backend/guard.js.map +0 -1
- package/dist/cjs/backend/index.d.ts +0 -8
- package/dist/cjs/backend/index.d.ts.map +0 -1
- package/dist/cjs/backend/index.js +0 -31
- package/dist/cjs/backend/index.js.map +0 -1
- package/dist/cjs/backend/lockable.d.ts +0 -15
- package/dist/cjs/backend/lockable.d.ts.map +0 -1
- package/dist/cjs/backend/lockable.js +0 -76
- package/dist/cjs/backend/lockable.js.map +0 -1
- package/dist/cjs/backend/node.d.ts +0 -36
- package/dist/cjs/backend/node.d.ts.map +0 -1
- package/dist/cjs/backend/node.js +0 -118
- package/dist/cjs/backend/node.js.map +0 -1
- package/dist/cjs/backend/types.d.ts +0 -36
- package/dist/cjs/backend/types.d.ts.map +0 -1
- package/dist/cjs/backend/types.js +0 -3
- package/dist/cjs/backend/types.js.map +0 -1
- package/dist/esm/backend/guard.d.ts +0 -31
- package/dist/esm/backend/guard.d.ts.map +0 -1
- package/dist/esm/backend/guard.js.map +0 -1
- package/dist/esm/backend/index.d.ts +0 -8
- package/dist/esm/backend/index.d.ts.map +0 -1
- package/dist/esm/backend/index.js +0 -13
- package/dist/esm/backend/index.js.map +0 -1
- package/dist/esm/backend/lockable.d.ts +0 -15
- package/dist/esm/backend/lockable.d.ts.map +0 -1
- package/dist/esm/backend/lockable.js +0 -39
- package/dist/esm/backend/lockable.js.map +0 -1
- package/dist/esm/backend/node.d.ts +0 -36
- package/dist/esm/backend/node.d.ts.map +0 -1
- package/dist/esm/backend/node.js +0 -109
- package/dist/esm/backend/node.js.map +0 -1
- package/dist/esm/backend/types.d.ts +0 -36
- package/dist/esm/backend/types.d.ts.map +0 -1
- package/dist/esm/backend/types.js +0 -2
- package/dist/esm/backend/types.js.map +0 -1
package/README.md
CHANGED
|
@@ -5,14 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@lickle/lock)
|
|
6
6
|
[](https://www.npmjs.com/package/@lickle/lock)
|
|
7
7
|
|
|
8
|
-
File-based locking for Node.js using
|
|
9
|
-
|
|
10
|
-
Supports:
|
|
11
|
-
|
|
12
|
-
- **Exclusive (write) locks**
|
|
13
|
-
- **Shared (read) locks**
|
|
14
|
-
- **Cross-process / cross-thread coordination**
|
|
15
|
-
- **Automatic cleanup** on exit, signals, and garbage collection
|
|
8
|
+
File-based locking for Node.js using native OS locks (`flock` on Unix, `LockFileEx` on Windows).
|
|
16
9
|
|
|
17
10
|
---
|
|
18
11
|
|
|
@@ -24,14 +17,14 @@ npm install @lickle/lock
|
|
|
24
17
|
|
|
25
18
|
---
|
|
26
19
|
|
|
27
|
-
|
|
20
|
+
## Quick Start
|
|
28
21
|
|
|
29
22
|
```ts
|
|
30
|
-
import {
|
|
23
|
+
import { openLock, Lock } from '@lickle/lock'
|
|
31
24
|
|
|
32
|
-
await using guard = await
|
|
25
|
+
await using guard = await openLock('/tmp/my.lock', Lock.Exclusive)
|
|
33
26
|
|
|
34
|
-
// access the
|
|
27
|
+
// access the file handle through the guard
|
|
35
28
|
await guard.handle.readFile({ encoding: 'utf-8' })
|
|
36
29
|
|
|
37
30
|
// lock automatically released when guard goes out of scope
|
|
@@ -41,18 +34,16 @@ Locks work **across processes and worker threads**, making them suitable for coo
|
|
|
41
34
|
|
|
42
35
|
---
|
|
43
36
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
## `exclusive(file, options?)`
|
|
37
|
+
## API
|
|
47
38
|
|
|
48
|
-
|
|
39
|
+
### `openLock(file, type, options?)`
|
|
49
40
|
|
|
50
|
-
|
|
41
|
+
Open a file and acquire a lock, polling until available. Returns a `FileLockGuard`.
|
|
51
42
|
|
|
52
43
|
```ts
|
|
53
|
-
import {
|
|
44
|
+
import { openLock, Lock } from '@lickle/lock'
|
|
54
45
|
|
|
55
|
-
const guard = await
|
|
46
|
+
const guard = await openLock('/tmp/my.lock', Lock.Exclusive)
|
|
56
47
|
|
|
57
48
|
try {
|
|
58
49
|
// critical section
|
|
@@ -61,77 +52,101 @@ try {
|
|
|
61
52
|
}
|
|
62
53
|
```
|
|
63
54
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
## `shared(file, options?)`
|
|
67
|
-
|
|
68
|
-
Acquire a **shared (read) lock**.
|
|
55
|
+
### `tryOpenLock(file, type, options?)`
|
|
69
56
|
|
|
70
|
-
|
|
57
|
+
Open a file and try to acquire a lock **without waiting**. Returns `undefined` if the lock is not available.
|
|
71
58
|
|
|
72
59
|
```ts
|
|
73
|
-
import {
|
|
60
|
+
import { tryOpenLock, Lock } from '@lickle/lock'
|
|
74
61
|
|
|
75
|
-
const
|
|
76
|
-
const reader2 = await shared('/tmp/data.lock')
|
|
62
|
+
const guard = await tryOpenLock('/tmp/my.lock', Lock.Exclusive)
|
|
77
63
|
|
|
78
|
-
|
|
79
|
-
|
|
64
|
+
if (guard) {
|
|
65
|
+
// acquired lock
|
|
66
|
+
await guard.drop()
|
|
67
|
+
}
|
|
80
68
|
```
|
|
81
69
|
|
|
82
|
-
|
|
70
|
+
### `lock(handle, type, options?)`
|
|
83
71
|
|
|
84
|
-
|
|
72
|
+
Acquire a lock on an already-open file handle. Returns a `LockGuard` (does not close the file on drop).
|
|
85
73
|
|
|
86
|
-
|
|
74
|
+
```ts
|
|
75
|
+
import fs from 'node:fs/promises'
|
|
76
|
+
import { lock, Lock } from '@lickle/lock'
|
|
77
|
+
|
|
78
|
+
const handle = await fs.open('/tmp/my.lock', 'r+')
|
|
79
|
+
await using guard = await lock(handle, Lock.Exclusive)
|
|
80
|
+
```
|
|
87
81
|
|
|
88
|
-
|
|
82
|
+
### `tryLock(handle, type, options?)`
|
|
83
|
+
|
|
84
|
+
Try to acquire a lock on an already-open file handle **without waiting**. Returns `undefined` if the lock is not available.
|
|
89
85
|
|
|
90
86
|
```ts
|
|
91
|
-
import
|
|
87
|
+
import fs from 'node:fs/promises'
|
|
88
|
+
import { tryLock, Lock } from '@lickle/lock'
|
|
92
89
|
|
|
93
|
-
const
|
|
90
|
+
const handle = await fs.open('/tmp/my.lock', 'r+')
|
|
91
|
+
const guard = await tryLock(handle, Lock.Exclusive)
|
|
94
92
|
|
|
95
93
|
if (guard) {
|
|
96
94
|
// acquired lock
|
|
97
|
-
await guard.drop()
|
|
98
95
|
}
|
|
99
96
|
```
|
|
100
97
|
|
|
101
|
-
|
|
98
|
+
### `unlock(handle, options?)`
|
|
102
99
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
Attempt to acquire a shared lock **without waiting**.
|
|
106
|
-
|
|
107
|
-
Returns `undefined` if an exclusive lock is currently held.
|
|
100
|
+
Release a lock on a file descriptor or file handle.
|
|
108
101
|
|
|
109
102
|
```ts
|
|
110
|
-
import
|
|
103
|
+
import fs from 'node:fs/promises'
|
|
104
|
+
import { lock, unlock, Lock } from '@lickle/lock'
|
|
111
105
|
|
|
112
|
-
const
|
|
106
|
+
const handle = await fs.open('/tmp/my.lock', 'r+')
|
|
107
|
+
await lock(handle, Lock.Exclusive)
|
|
108
|
+
// ... critical section ...
|
|
109
|
+
await unlock(handle)
|
|
113
110
|
```
|
|
114
111
|
|
|
115
112
|
---
|
|
116
113
|
|
|
117
|
-
|
|
114
|
+
## Options
|
|
118
115
|
|
|
119
|
-
|
|
116
|
+
`openLock` and `lock` accept `PollOptions`:
|
|
120
117
|
|
|
121
118
|
```ts
|
|
122
119
|
{
|
|
123
120
|
pollMs?: number // polling interval (default: 10ms)
|
|
124
121
|
timeout?: number // max wait time before throwing
|
|
125
|
-
|
|
122
|
+
backoff?: number // multiplier applied to pollMs after each attempt (e.g. 2 = exponential)
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
All functions accept `LockingOptions`:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
{
|
|
130
|
+
locking?: Locker // custom locker implementation
|
|
131
|
+
range?: LockRange // byte range to lock (see Range Locks)
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`openLock` and `tryOpenLock` additionally accept:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
{
|
|
139
|
+
fs?: Fs // custom filesystem for opening files
|
|
126
140
|
}
|
|
127
141
|
```
|
|
128
142
|
|
|
129
143
|
Example:
|
|
130
144
|
|
|
131
145
|
```ts
|
|
132
|
-
await
|
|
133
|
-
pollMs:
|
|
134
|
-
|
|
146
|
+
await openLock('/tmp/my.lock', Lock.Exclusive, {
|
|
147
|
+
pollMs: 10,
|
|
148
|
+
backoff: 2,
|
|
149
|
+
timeout: 5000,
|
|
135
150
|
})
|
|
136
151
|
```
|
|
137
152
|
|
|
@@ -143,66 +158,157 @@ Error: Timed out acquiring lock
|
|
|
143
158
|
|
|
144
159
|
---
|
|
145
160
|
|
|
146
|
-
|
|
161
|
+
## Range Locks
|
|
162
|
+
|
|
163
|
+
Lock a specific byte range within a file instead of the entire file. This allows multiple processes to lock different regions concurrently.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { openLock, Lock } from '@lickle/lock'
|
|
167
|
+
|
|
168
|
+
// lock bytes 0–99
|
|
169
|
+
await using header = await openLock('/tmp/data.bin', Lock.Exclusive, {
|
|
170
|
+
range: { offset: 0, length: 100 },
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// lock bytes 100–199 concurrently — no conflict
|
|
174
|
+
await using body = await openLock('/tmp/data.bin', Lock.Exclusive, {
|
|
175
|
+
range: { offset: 100, length: 100 },
|
|
176
|
+
})
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Range locks also work with `tryOpenLock`, `lock`, and `tryLock`.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
const guard = await tryOpenLock('/tmp/data.bin', Lock.Exclusive, {
|
|
183
|
+
range: { offset: 0, length: 512 },
|
|
184
|
+
})
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
When no range is specified, the entire file is locked (the default).
|
|
188
|
+
|
|
189
|
+
See [Platform Notes](#platform-notes) for important platform-specific behavior of range locks.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Guards
|
|
147
194
|
|
|
148
|
-
Lock functions return
|
|
195
|
+
Lock functions return guards that manage the lifetime of the lock.
|
|
149
196
|
|
|
150
|
-
|
|
197
|
+
### `FileLockGuard`
|
|
198
|
+
|
|
199
|
+
Returned by `openLock` and `tryOpenLock`. Owns both the lock and the file handle — dropping it unlocks and closes the file.
|
|
151
200
|
|
|
152
201
|
```ts
|
|
153
|
-
const guard = await
|
|
202
|
+
const guard = await openLock('/tmp/my.lock', Lock.Exclusive)
|
|
154
203
|
|
|
155
204
|
guard.handle // fs.promises.FileHandle
|
|
156
205
|
guard.fd // file descriptor
|
|
157
|
-
guard.path // lock file path
|
|
158
206
|
guard.dropped // boolean
|
|
159
207
|
|
|
160
208
|
await guard.drop()
|
|
161
209
|
```
|
|
162
210
|
|
|
163
|
-
|
|
211
|
+
### `LockGuard`
|
|
212
|
+
|
|
213
|
+
Returned by `lock` and `tryLock`. Owns only the lock — dropping it unlocks but does not close the file.
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
const guard = await lock(handle, Lock.Exclusive)
|
|
164
217
|
|
|
165
|
-
|
|
218
|
+
guard.fd // file descriptor
|
|
219
|
+
guard.dropped // boolean
|
|
220
|
+
|
|
221
|
+
await guard.drop()
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Automatic cleanup with `await using`
|
|
166
225
|
|
|
167
|
-
|
|
226
|
+
Both guards implement the [Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management) proposal.
|
|
168
227
|
|
|
169
228
|
```ts
|
|
170
|
-
await using guard = await
|
|
229
|
+
await using guard = await openLock('/tmp/my.lock', Lock.Exclusive)
|
|
171
230
|
|
|
172
231
|
const text = await guard.handle.readFile('utf8')
|
|
173
232
|
```
|
|
174
233
|
|
|
175
234
|
The lock is automatically released when the scope exits.
|
|
176
235
|
|
|
177
|
-
Spec:
|
|
178
|
-
[https://github.com/tc39/proposal-explicit-resource-management](https://github.com/tc39/proposal-explicit-resource-management)
|
|
179
|
-
|
|
180
236
|
---
|
|
181
237
|
|
|
182
|
-
|
|
238
|
+
## Customization
|
|
239
|
+
|
|
240
|
+
The default implementation uses native OS locks and Node.js `fs` for file operations. You can customize either piece independently.
|
|
183
241
|
|
|
184
|
-
|
|
242
|
+
### `Fs` — file opening
|
|
185
243
|
|
|
186
|
-
|
|
187
|
-
- `SIGINT` or `SIGTERM` is received
|
|
188
|
-
- a worker thread shuts down
|
|
189
|
-
- the guard is garbage collected
|
|
244
|
+
Controls how lock files are opened. Implement the `Fs` interface to use alternative file systems.
|
|
190
245
|
|
|
191
|
-
|
|
246
|
+
```ts
|
|
247
|
+
import { openLock, Lock, type Fs } from '@lickle/lock'
|
|
248
|
+
|
|
249
|
+
const myFs: Fs<MyHandle> = {
|
|
250
|
+
open: (file, type) => /* your open logic */,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
await openLock('/tmp/my.lock', Lock.Exclusive, { fs: myFs })
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### `Locker` — lock operations
|
|
257
|
+
|
|
258
|
+
Controls how locks are acquired and released. Use `createLocker()` with custom hooks or implement `Locker` directly.
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import { lock, Lock, createLocker } from '@lickle/lock'
|
|
262
|
+
|
|
263
|
+
const locker = createLocker(myHooks)
|
|
264
|
+
await lock(handle, Lock.Exclusive, { locking: locker })
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
The default locker uses:
|
|
268
|
+
|
|
269
|
+
- **Unix:** `flock(2)` (whole-file), `fcntl(2)` (byte-range)
|
|
270
|
+
- **Windows:** `LockFileEx` / `UnlockFileEx` (both whole-file and byte-range)
|
|
192
271
|
|
|
193
272
|
---
|
|
194
273
|
|
|
195
|
-
|
|
274
|
+
## Platform Notes
|
|
275
|
+
|
|
276
|
+
The locking primitives used by this library differ across operating systems. The table below summarizes the syscalls and their behavior.
|
|
277
|
+
|
|
278
|
+
| | Whole-file lock | Range lock | Range lock scope |
|
|
279
|
+
| --------------- | --------------- | ----------------------------------------- | -------------------- |
|
|
280
|
+
| **Linux** | `flock(2)` | `fcntl(2)` OFD locks (`F_OFD_SETLK`) | per file-description |
|
|
281
|
+
| **macOS / BSD** | `flock(2)` | `fcntl(2)` POSIX record locks (`F_SETLK`) | per process |
|
|
282
|
+
| **Windows** | `LockFileEx` | `LockFileEx` (with offset/length) | per handle |
|
|
283
|
+
|
|
284
|
+
### Linux
|
|
285
|
+
|
|
286
|
+
Range locks use Open File Description (OFD) locks, available since Linux 3.15. OFD locks are scoped to the **file description** (the kernel object behind an `open()` call), not the process. This means different threads holding different file descriptors can hold independent range locks safely.
|
|
287
|
+
|
|
288
|
+
### Windows
|
|
289
|
+
|
|
290
|
+
Both whole-file and range locks use `LockFileEx`/`UnlockFileEx`. Locks are scoped to the file handle and do not interfere across handles within the same process.
|
|
291
|
+
|
|
292
|
+
### macOS / BSD — POSIX record lock caveat
|
|
293
|
+
|
|
294
|
+
On macOS and BSD, range locks use classic POSIX record locks (`fcntl` with `F_SETLK`/`F_SETLKW`). These locks have a well-documented flaw in the POSIX.1 specification:
|
|
295
|
+
|
|
296
|
+
> **Closing _any_ file descriptor for a given file releases _all_ locks the process holds on that file.**
|
|
297
|
+
|
|
298
|
+
If thread A holds a range lock on `data.db` via fd 5, and thread B independently opens `data.db`, reads a byte, and closes its fd, the kernel silently releases thread A's lock. This is specified behavior — not a bug.
|
|
299
|
+
|
|
300
|
+
This means **range locks on macOS are not safe for intra-process concurrency** when multiple threads or code paths may open the same file. The lock can vanish without warning.
|
|
196
301
|
|
|
197
|
-
|
|
302
|
+
Whole-file locks (`flock`) are **not** affected by this issue — `flock` and `fcntl` are independent locking systems.
|
|
198
303
|
|
|
199
|
-
|
|
200
|
-
- **Windows:** `LockFileEx`
|
|
304
|
+
**Recommendations:**
|
|
201
305
|
|
|
202
|
-
|
|
306
|
+
- For **inter-process** locking (one lock holder per process), range locks work correctly on all platforms.
|
|
307
|
+
- For **intra-process** locking (multiple threads in the same process), range locks are safe on **Linux** (OFD locks) and **Windows**, but **not on macOS/BSD**.
|
|
308
|
+
- On macOS, if you need concurrent range locks within a single process, ensure only one file descriptor for the target file is open at a time across all threads, or use whole-file locks instead.
|
|
203
309
|
|
|
204
310
|
---
|
|
205
311
|
|
|
206
|
-
|
|
312
|
+
## License
|
|
207
313
|
|
|
208
314
|
MIT © Dan Beaven
|