dflockd-client 1.0.0
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 +21 -0
- package/README.md +152 -0
- package/dist/client.cjs +329 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +78 -0
- package/dist/client.d.ts +78 -0
- package/dist/client.js +287 -0
- package/dist/client.js.map +1 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Matthew Francis-Landau
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# dflockd-client
|
|
2
|
+
|
|
3
|
+
TypeScript client for the [dflockd](https://github.com/mtingers/dflockd) distributed lock daemon.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install dflockd-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Start the dflockd server first:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv run dflockd
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### `withLock` (recommended)
|
|
20
|
+
|
|
21
|
+
The simplest way to use a lock. Acquires, runs your callback, and releases
|
|
22
|
+
automatically — even if the callback throws.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { DistributedLock } from "dflockd-client";
|
|
26
|
+
|
|
27
|
+
const lock = new DistributedLock({ key: "my-resource" });
|
|
28
|
+
|
|
29
|
+
await lock.withLock(async () => {
|
|
30
|
+
// critical section — lock is held here
|
|
31
|
+
});
|
|
32
|
+
// lock is released
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Manual `acquire` / `release`
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { DistributedLock } from "dflockd-client";
|
|
39
|
+
|
|
40
|
+
const lock = new DistributedLock({
|
|
41
|
+
key: "my-resource",
|
|
42
|
+
acquireTimeoutS: 10, // wait up to 10 s (default)
|
|
43
|
+
leaseTtlS: 20, // server-side lease duration
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const ok = await lock.acquire(); // true on success, false on timeout
|
|
47
|
+
if (!ok) {
|
|
48
|
+
console.error("could not acquire lock");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// critical section — lock is held and auto-renewed
|
|
54
|
+
} finally {
|
|
55
|
+
await lock.release();
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Two-phase lock (enqueue / wait)
|
|
60
|
+
|
|
61
|
+
Split acquisition into two steps so you can notify an external system
|
|
62
|
+
(webhook, database, queue) between joining the FIFO queue and blocking.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { DistributedLock } from "dflockd-client";
|
|
66
|
+
|
|
67
|
+
const lock = new DistributedLock({
|
|
68
|
+
key: "my-resource",
|
|
69
|
+
acquireTimeoutS: 10,
|
|
70
|
+
leaseTtlS: 20,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Step 1: join the queue (non-blocking, returns immediately)
|
|
74
|
+
const status = await lock.enqueue(); // "acquired" or "queued"
|
|
75
|
+
|
|
76
|
+
// Step 2: do something between enqueue and blocking
|
|
77
|
+
console.log(`enqueue status: ${status}`);
|
|
78
|
+
notifyExternalSystem(status);
|
|
79
|
+
|
|
80
|
+
// Step 3: block until the lock is granted (or timeout)
|
|
81
|
+
const granted = await lock.wait(10); // true on success, false on timeout
|
|
82
|
+
if (!granted) {
|
|
83
|
+
console.error("timed out waiting for lock");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// critical section — lock is held and auto-renewed
|
|
89
|
+
} finally {
|
|
90
|
+
await lock.release();
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
If the lock is free when `enqueue()` is called, it returns `"acquired"` and
|
|
95
|
+
the lock is already held (fast path). `wait()` then returns `true` immediately
|
|
96
|
+
without blocking. If the lock is contended, `enqueue()` returns `"queued"` and
|
|
97
|
+
`wait()` blocks until the lock is granted or the timeout expires.
|
|
98
|
+
|
|
99
|
+
### Options
|
|
100
|
+
|
|
101
|
+
| Option | Type | Default | Description |
|
|
102
|
+
|------------------|----------|---------------|--------------------------------------------------|
|
|
103
|
+
| `key` | `string` | *(required)* | Lock name |
|
|
104
|
+
| `acquireTimeoutS`| `number` | `10` | Seconds to wait for the lock before giving up |
|
|
105
|
+
| `leaseTtlS` | `number` | server default| Server-side lease duration in seconds |
|
|
106
|
+
| `host` | `string` | `127.0.0.1` | Server host |
|
|
107
|
+
| `port` | `number` | `6388` | Server port |
|
|
108
|
+
| `renewRatio` | `number` | `0.5` | Renew at `lease * ratio` seconds (e.g. 50% of TTL)|
|
|
109
|
+
|
|
110
|
+
### Error handling
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { DistributedLock, AcquireTimeoutError, LockError } from "dflockd-client";
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await lock.withLock(async () => { /* ... */ });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (err instanceof AcquireTimeoutError) {
|
|
119
|
+
// lock could not be acquired within acquireTimeoutS
|
|
120
|
+
} else if (err instanceof LockError) {
|
|
121
|
+
// protocol-level error (bad token, server disconnect, etc.)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Low-level functions
|
|
127
|
+
|
|
128
|
+
For cases where you manage the socket yourself:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import * as net from "net";
|
|
132
|
+
import { acquire, enqueue, waitForLock, renew, release } from "dflockd-client";
|
|
133
|
+
|
|
134
|
+
const sock = net.createConnection({ host: "127.0.0.1", port: 6388 });
|
|
135
|
+
|
|
136
|
+
// Single-phase
|
|
137
|
+
const { token, lease } = await acquire(sock, "my-key", 10);
|
|
138
|
+
const remaining = await renew(sock, "my-key", token, 60);
|
|
139
|
+
await release(sock, "my-key", token);
|
|
140
|
+
|
|
141
|
+
// Two-phase
|
|
142
|
+
const result = await enqueue(sock, "another-key"); // { status, token, lease }
|
|
143
|
+
if (result.status === "queued") {
|
|
144
|
+
const granted = await waitForLock(sock, "another-key", 10); // { token, lease }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
sock.destroy();
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT
|
package/dist/client.cjs
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/client.ts
|
|
31
|
+
var client_exports = {};
|
|
32
|
+
__export(client_exports, {
|
|
33
|
+
AcquireTimeoutError: () => AcquireTimeoutError,
|
|
34
|
+
DistributedLock: () => DistributedLock,
|
|
35
|
+
LockError: () => LockError,
|
|
36
|
+
acquire: () => acquire,
|
|
37
|
+
enqueue: () => enqueue,
|
|
38
|
+
release: () => release,
|
|
39
|
+
renew: () => renew,
|
|
40
|
+
waitForLock: () => waitForLock
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(client_exports);
|
|
43
|
+
var net = __toESM(require("net"), 1);
|
|
44
|
+
var DEFAULT_HOST = "127.0.0.1";
|
|
45
|
+
var DEFAULT_PORT = 6388;
|
|
46
|
+
var AcquireTimeoutError = class extends Error {
|
|
47
|
+
constructor(key) {
|
|
48
|
+
super(`timeout acquiring '${key}'`);
|
|
49
|
+
this.name = "AcquireTimeoutError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var LockError = class extends Error {
|
|
53
|
+
constructor(message) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = "LockError";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
function encodeLines(...lines) {
|
|
59
|
+
return Buffer.from(lines.map((l) => l + "\n").join(""), "utf-8");
|
|
60
|
+
}
|
|
61
|
+
function readline(sock) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
let buf = "";
|
|
64
|
+
const onData = (chunk) => {
|
|
65
|
+
buf += chunk.toString("utf-8");
|
|
66
|
+
const idx = buf.indexOf("\n");
|
|
67
|
+
if (idx !== -1) {
|
|
68
|
+
cleanup();
|
|
69
|
+
resolve(buf.slice(0, idx).replace(/\r$/, ""));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const onError = (err) => {
|
|
73
|
+
cleanup();
|
|
74
|
+
reject(err);
|
|
75
|
+
};
|
|
76
|
+
const onClose = () => {
|
|
77
|
+
cleanup();
|
|
78
|
+
reject(new LockError("server closed connection"));
|
|
79
|
+
};
|
|
80
|
+
const cleanup = () => {
|
|
81
|
+
sock.removeListener("data", onData);
|
|
82
|
+
sock.removeListener("error", onError);
|
|
83
|
+
sock.removeListener("close", onClose);
|
|
84
|
+
};
|
|
85
|
+
sock.on("data", onData);
|
|
86
|
+
sock.on("error", onError);
|
|
87
|
+
sock.on("close", onClose);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function connect(host, port) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const sock = net.createConnection({ host, port }, () => resolve(sock));
|
|
93
|
+
sock.on("error", reject);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async function acquire(sock, key, acquireTimeoutS, leaseTtlS) {
|
|
97
|
+
const arg = leaseTtlS == null ? String(acquireTimeoutS) : `${acquireTimeoutS} ${leaseTtlS}`;
|
|
98
|
+
sock.write(encodeLines("l", key, arg));
|
|
99
|
+
const resp = await readline(sock);
|
|
100
|
+
if (resp === "timeout") {
|
|
101
|
+
throw new AcquireTimeoutError(key);
|
|
102
|
+
}
|
|
103
|
+
if (!resp.startsWith("ok ")) {
|
|
104
|
+
throw new LockError(`acquire failed: '${resp}'`);
|
|
105
|
+
}
|
|
106
|
+
const parts = resp.split(" ");
|
|
107
|
+
if (parts.length < 2) {
|
|
108
|
+
throw new LockError(`bad ok response: '${resp}'`);
|
|
109
|
+
}
|
|
110
|
+
const token = parts[1];
|
|
111
|
+
const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 30;
|
|
112
|
+
return { token, lease };
|
|
113
|
+
}
|
|
114
|
+
async function renew(sock, key, token, leaseTtlS) {
|
|
115
|
+
const arg = leaseTtlS == null ? token : `${token} ${leaseTtlS}`;
|
|
116
|
+
sock.write(encodeLines("n", key, arg));
|
|
117
|
+
const resp = await readline(sock);
|
|
118
|
+
if (!resp.startsWith("ok")) {
|
|
119
|
+
throw new LockError(`renew failed: '${resp}'`);
|
|
120
|
+
}
|
|
121
|
+
const parts = resp.split(" ");
|
|
122
|
+
if (parts.length >= 2 && /^\d+$/.test(parts[1])) {
|
|
123
|
+
return parseInt(parts[1], 10);
|
|
124
|
+
}
|
|
125
|
+
return -1;
|
|
126
|
+
}
|
|
127
|
+
async function enqueue(sock, key, leaseTtlS) {
|
|
128
|
+
const arg = leaseTtlS == null ? "" : String(leaseTtlS);
|
|
129
|
+
sock.write(encodeLines("e", key, arg));
|
|
130
|
+
const resp = await readline(sock);
|
|
131
|
+
if (resp.startsWith("acquired ")) {
|
|
132
|
+
const parts = resp.split(" ");
|
|
133
|
+
const token = parts[1];
|
|
134
|
+
const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 33;
|
|
135
|
+
return { status: "acquired", token, lease };
|
|
136
|
+
}
|
|
137
|
+
if (resp === "queued") {
|
|
138
|
+
return { status: "queued", token: null, lease: null };
|
|
139
|
+
}
|
|
140
|
+
throw new LockError(`enqueue failed: '${resp}'`);
|
|
141
|
+
}
|
|
142
|
+
async function waitForLock(sock, key, waitTimeoutS) {
|
|
143
|
+
sock.write(encodeLines("w", key, String(waitTimeoutS)));
|
|
144
|
+
const resp = await readline(sock);
|
|
145
|
+
if (resp === "timeout") {
|
|
146
|
+
throw new AcquireTimeoutError(key);
|
|
147
|
+
}
|
|
148
|
+
if (!resp.startsWith("ok ")) {
|
|
149
|
+
throw new LockError(`wait failed: '${resp}'`);
|
|
150
|
+
}
|
|
151
|
+
const parts = resp.split(" ");
|
|
152
|
+
const token = parts[1];
|
|
153
|
+
const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 33;
|
|
154
|
+
return { token, lease };
|
|
155
|
+
}
|
|
156
|
+
async function release(sock, key, token) {
|
|
157
|
+
sock.write(encodeLines("r", key, token));
|
|
158
|
+
const resp = await readline(sock);
|
|
159
|
+
if (resp !== "ok") {
|
|
160
|
+
throw new LockError(`release failed: '${resp}'`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
var DistributedLock = class {
|
|
164
|
+
key;
|
|
165
|
+
acquireTimeoutS;
|
|
166
|
+
leaseTtlS;
|
|
167
|
+
host;
|
|
168
|
+
port;
|
|
169
|
+
renewRatio;
|
|
170
|
+
token = null;
|
|
171
|
+
lease = 0;
|
|
172
|
+
sock = null;
|
|
173
|
+
renewTimer = null;
|
|
174
|
+
closed = false;
|
|
175
|
+
constructor(opts) {
|
|
176
|
+
this.key = opts.key;
|
|
177
|
+
this.acquireTimeoutS = opts.acquireTimeoutS ?? 10;
|
|
178
|
+
this.leaseTtlS = opts.leaseTtlS;
|
|
179
|
+
this.host = opts.host ?? DEFAULT_HOST;
|
|
180
|
+
this.port = opts.port ?? DEFAULT_PORT;
|
|
181
|
+
this.renewRatio = opts.renewRatio ?? 0.5;
|
|
182
|
+
}
|
|
183
|
+
/** Acquire the lock. Returns `true` on success, `false` on timeout. */
|
|
184
|
+
async acquire() {
|
|
185
|
+
this.closed = false;
|
|
186
|
+
this.sock = await connect(this.host, this.port);
|
|
187
|
+
try {
|
|
188
|
+
const result = await acquire(
|
|
189
|
+
this.sock,
|
|
190
|
+
this.key,
|
|
191
|
+
this.acquireTimeoutS,
|
|
192
|
+
this.leaseTtlS
|
|
193
|
+
);
|
|
194
|
+
this.token = result.token;
|
|
195
|
+
this.lease = result.lease;
|
|
196
|
+
} catch (err) {
|
|
197
|
+
await this.close();
|
|
198
|
+
if (err instanceof AcquireTimeoutError) return false;
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
201
|
+
this.startRenew();
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
/** Release the lock and close the connection. */
|
|
205
|
+
async release() {
|
|
206
|
+
try {
|
|
207
|
+
this.stopRenew();
|
|
208
|
+
if (this.sock && this.token) {
|
|
209
|
+
await release(this.sock, this.key, this.token);
|
|
210
|
+
}
|
|
211
|
+
} finally {
|
|
212
|
+
await this.close();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Two-phase step 1: connect and join the FIFO queue.
|
|
217
|
+
* Returns `"acquired"` (fast-path, lock is already held) or `"queued"`.
|
|
218
|
+
* If acquired immediately, the renew loop starts automatically.
|
|
219
|
+
*/
|
|
220
|
+
async enqueue() {
|
|
221
|
+
this.closed = false;
|
|
222
|
+
this.sock = await connect(this.host, this.port);
|
|
223
|
+
try {
|
|
224
|
+
const result = await enqueue(this.sock, this.key, this.leaseTtlS);
|
|
225
|
+
if (result.status === "acquired") {
|
|
226
|
+
this.token = result.token;
|
|
227
|
+
this.lease = result.lease ?? 0;
|
|
228
|
+
this.startRenew();
|
|
229
|
+
}
|
|
230
|
+
return result.status;
|
|
231
|
+
} catch (err) {
|
|
232
|
+
await this.close();
|
|
233
|
+
throw err;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Two-phase step 2: block until the lock is granted.
|
|
238
|
+
* Returns `true` if granted, `false` on timeout.
|
|
239
|
+
* If already acquired during `enqueue()`, returns `true` immediately.
|
|
240
|
+
*/
|
|
241
|
+
async wait(timeoutS) {
|
|
242
|
+
if (this.token !== null) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
if (!this.sock) {
|
|
246
|
+
throw new LockError("not connected; call enqueue() first");
|
|
247
|
+
}
|
|
248
|
+
const timeout = timeoutS ?? this.acquireTimeoutS;
|
|
249
|
+
try {
|
|
250
|
+
const result = await waitForLock(this.sock, this.key, timeout);
|
|
251
|
+
this.token = result.token;
|
|
252
|
+
this.lease = result.lease;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
await this.close();
|
|
255
|
+
if (err instanceof AcquireTimeoutError) return false;
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
258
|
+
this.startRenew();
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Run `fn` while holding the lock, then release automatically.
|
|
263
|
+
*
|
|
264
|
+
* ```ts
|
|
265
|
+
* const lock = new DistributedLock({ key: "my-resource" });
|
|
266
|
+
* await lock.withLock(async () => {
|
|
267
|
+
* // critical section
|
|
268
|
+
* });
|
|
269
|
+
* ```
|
|
270
|
+
*/
|
|
271
|
+
async withLock(fn) {
|
|
272
|
+
const ok = await this.acquire();
|
|
273
|
+
if (!ok) {
|
|
274
|
+
throw new AcquireTimeoutError(this.key);
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
return await fn();
|
|
278
|
+
} finally {
|
|
279
|
+
await this.release();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/** Close the underlying socket (idempotent). */
|
|
283
|
+
async close() {
|
|
284
|
+
if (this.closed) return;
|
|
285
|
+
this.closed = true;
|
|
286
|
+
this.stopRenew();
|
|
287
|
+
if (this.sock) {
|
|
288
|
+
this.sock.destroy();
|
|
289
|
+
this.sock = null;
|
|
290
|
+
}
|
|
291
|
+
this.token = null;
|
|
292
|
+
}
|
|
293
|
+
// -- internals --
|
|
294
|
+
startRenew() {
|
|
295
|
+
const interval = Math.max(1, this.lease * this.renewRatio) * 1e3;
|
|
296
|
+
const loop = async () => {
|
|
297
|
+
if (!this.sock || !this.token) return;
|
|
298
|
+
try {
|
|
299
|
+
await renew(this.sock, this.key, this.token, this.leaseTtlS);
|
|
300
|
+
} catch {
|
|
301
|
+
console.error(
|
|
302
|
+
`lock lost (renew failed): key=${this.key} token=${this.token}`
|
|
303
|
+
);
|
|
304
|
+
this.token = null;
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
this.renewTimer = setTimeout(loop, interval);
|
|
308
|
+
};
|
|
309
|
+
this.renewTimer = setTimeout(loop, interval);
|
|
310
|
+
}
|
|
311
|
+
stopRenew() {
|
|
312
|
+
if (this.renewTimer != null) {
|
|
313
|
+
clearTimeout(this.renewTimer);
|
|
314
|
+
this.renewTimer = null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
319
|
+
0 && (module.exports = {
|
|
320
|
+
AcquireTimeoutError,
|
|
321
|
+
DistributedLock,
|
|
322
|
+
LockError,
|
|
323
|
+
acquire,
|
|
324
|
+
enqueue,
|
|
325
|
+
release,
|
|
326
|
+
renew,
|
|
327
|
+
waitForLock
|
|
328
|
+
});
|
|
329
|
+
//# sourceMappingURL=client.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["import * as net from \"net\";\n\nconst DEFAULT_HOST = \"127.0.0.1\";\nconst DEFAULT_PORT = 6388;\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\nexport class AcquireTimeoutError extends Error {\n constructor(key: string) {\n super(`timeout acquiring '${key}'`);\n this.name = \"AcquireTimeoutError\";\n }\n}\n\nexport class LockError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"LockError\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Low-level helpers\n// ---------------------------------------------------------------------------\n\nfunction encodeLines(...lines: string[]): Buffer {\n return Buffer.from(lines.map((l) => l + \"\\n\").join(\"\"), \"utf-8\");\n}\n\n/**\n * Read one newline-terminated line from the socket.\n * Resolves with the line (without trailing \\r\\n).\n * Rejects if the connection closes before a full line arrives.\n */\nfunction readline(sock: net.Socket): Promise<string> {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n\n const onData = (chunk: Buffer) => {\n buf += chunk.toString(\"utf-8\");\n const idx = buf.indexOf(\"\\n\");\n if (idx !== -1) {\n cleanup();\n resolve(buf.slice(0, idx).replace(/\\r$/, \"\"));\n }\n };\n\n const onError = (err: Error) => {\n cleanup();\n reject(err);\n };\n\n const onClose = () => {\n cleanup();\n reject(new LockError(\"server closed connection\"));\n };\n\n const cleanup = () => {\n sock.removeListener(\"data\", onData);\n sock.removeListener(\"error\", onError);\n sock.removeListener(\"close\", onClose);\n };\n\n sock.on(\"data\", onData);\n sock.on(\"error\", onError);\n sock.on(\"close\", onClose);\n });\n}\n\nfunction connect(host: string, port: number): Promise<net.Socket> {\n return new Promise((resolve, reject) => {\n const sock = net.createConnection({ host, port }, () => resolve(sock));\n sock.on(\"error\", reject);\n });\n}\n\n// ---------------------------------------------------------------------------\n// Protocol functions\n// ---------------------------------------------------------------------------\n\nexport async function acquire(\n sock: net.Socket,\n key: string,\n acquireTimeoutS: number,\n leaseTtlS?: number,\n): Promise<{ token: string; lease: number }> {\n const arg =\n leaseTtlS == null\n ? String(acquireTimeoutS)\n : `${acquireTimeoutS} ${leaseTtlS}`;\n\n sock.write(encodeLines(\"l\", key, arg));\n\n const resp = await readline(sock);\n if (resp === \"timeout\") {\n throw new AcquireTimeoutError(key);\n }\n if (!resp.startsWith(\"ok \")) {\n throw new LockError(`acquire failed: '${resp}'`);\n }\n\n const parts = resp.split(\" \");\n if (parts.length < 2) {\n throw new LockError(`bad ok response: '${resp}'`);\n }\n const token = parts[1];\n const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 30;\n return { token, lease };\n}\n\nexport async function renew(\n sock: net.Socket,\n key: string,\n token: string,\n leaseTtlS?: number,\n): Promise<number> {\n const arg = leaseTtlS == null ? token : `${token} ${leaseTtlS}`;\n sock.write(encodeLines(\"n\", key, arg));\n\n const resp = await readline(sock);\n if (!resp.startsWith(\"ok\")) {\n throw new LockError(`renew failed: '${resp}'`);\n }\n\n const parts = resp.split(\" \");\n if (parts.length >= 2 && /^\\d+$/.test(parts[1])) {\n return parseInt(parts[1], 10);\n }\n return -1;\n}\n\nexport async function enqueue(\n sock: net.Socket,\n key: string,\n leaseTtlS?: number,\n): Promise<{ status: \"acquired\" | \"queued\"; token: string | null; lease: number | null }> {\n const arg = leaseTtlS == null ? \"\" : String(leaseTtlS);\n sock.write(encodeLines(\"e\", key, arg));\n\n const resp = await readline(sock);\n if (resp.startsWith(\"acquired \")) {\n const parts = resp.split(\" \");\n const token = parts[1];\n const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 33;\n return { status: \"acquired\", token, lease };\n }\n if (resp === \"queued\") {\n return { status: \"queued\", token: null, lease: null };\n }\n throw new LockError(`enqueue failed: '${resp}'`);\n}\n\nexport async function waitForLock(\n sock: net.Socket,\n key: string,\n waitTimeoutS: number,\n): Promise<{ token: string; lease: number }> {\n sock.write(encodeLines(\"w\", key, String(waitTimeoutS)));\n\n const resp = await readline(sock);\n if (resp === \"timeout\") {\n throw new AcquireTimeoutError(key);\n }\n if (!resp.startsWith(\"ok \")) {\n throw new LockError(`wait failed: '${resp}'`);\n }\n\n const parts = resp.split(\" \");\n const token = parts[1];\n const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 33;\n return { token, lease };\n}\n\nexport async function release(\n sock: net.Socket,\n key: string,\n token: string,\n): Promise<void> {\n sock.write(encodeLines(\"r\", key, token));\n\n const resp = await readline(sock);\n if (resp !== \"ok\") {\n throw new LockError(`release failed: '${resp}'`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// DistributedLock\n// ---------------------------------------------------------------------------\n\nexport interface DistributedLockOptions {\n key: string;\n acquireTimeoutS?: number;\n leaseTtlS?: number;\n host?: string;\n port?: number;\n renewRatio?: number;\n}\n\nexport class DistributedLock {\n readonly key: string;\n readonly acquireTimeoutS: number;\n readonly leaseTtlS: number | undefined;\n readonly host: string;\n readonly port: number;\n readonly renewRatio: number;\n\n token: string | null = null;\n lease: number = 0;\n\n private sock: net.Socket | null = null;\n private renewTimer: ReturnType<typeof setTimeout> | null = null;\n private closed = false;\n\n constructor(opts: DistributedLockOptions) {\n this.key = opts.key;\n this.acquireTimeoutS = opts.acquireTimeoutS ?? 10;\n this.leaseTtlS = opts.leaseTtlS;\n this.host = opts.host ?? DEFAULT_HOST;\n this.port = opts.port ?? DEFAULT_PORT;\n this.renewRatio = opts.renewRatio ?? 0.5;\n }\n\n /** Acquire the lock. Returns `true` on success, `false` on timeout. */\n async acquire(): Promise<boolean> {\n this.closed = false;\n this.sock = await connect(this.host, this.port);\n try {\n const result = await acquire(\n this.sock,\n this.key,\n this.acquireTimeoutS,\n this.leaseTtlS,\n );\n this.token = result.token;\n this.lease = result.lease;\n } catch (err) {\n await this.close();\n if (err instanceof AcquireTimeoutError) return false;\n throw err;\n }\n this.startRenew();\n return true;\n }\n\n /** Release the lock and close the connection. */\n async release(): Promise<void> {\n try {\n this.stopRenew();\n if (this.sock && this.token) {\n await release(this.sock, this.key, this.token);\n }\n } finally {\n await this.close();\n }\n }\n\n /**\n * Two-phase step 1: connect and join the FIFO queue.\n * Returns `\"acquired\"` (fast-path, lock is already held) or `\"queued\"`.\n * If acquired immediately, the renew loop starts automatically.\n */\n async enqueue(): Promise<\"acquired\" | \"queued\"> {\n this.closed = false;\n this.sock = await connect(this.host, this.port);\n try {\n const result = await enqueue(this.sock, this.key, this.leaseTtlS);\n if (result.status === \"acquired\") {\n this.token = result.token;\n this.lease = result.lease ?? 0;\n this.startRenew();\n }\n return result.status;\n } catch (err) {\n await this.close();\n throw err;\n }\n }\n\n /**\n * Two-phase step 2: block until the lock is granted.\n * Returns `true` if granted, `false` on timeout.\n * If already acquired during `enqueue()`, returns `true` immediately.\n */\n async wait(timeoutS?: number): Promise<boolean> {\n if (this.token !== null) {\n // Already acquired during enqueue (fast path)\n return true;\n }\n if (!this.sock) {\n throw new LockError(\"not connected; call enqueue() first\");\n }\n const timeout = timeoutS ?? this.acquireTimeoutS;\n try {\n const result = await waitForLock(this.sock, this.key, timeout);\n this.token = result.token;\n this.lease = result.lease;\n } catch (err) {\n await this.close();\n if (err instanceof AcquireTimeoutError) return false;\n throw err;\n }\n this.startRenew();\n return true;\n }\n\n /**\n * Run `fn` while holding the lock, then release automatically.\n *\n * ```ts\n * const lock = new DistributedLock({ key: \"my-resource\" });\n * await lock.withLock(async () => {\n * // critical section\n * });\n * ```\n */\n async withLock<T>(fn: () => T | Promise<T>): Promise<T> {\n const ok = await this.acquire();\n if (!ok) {\n throw new AcquireTimeoutError(this.key);\n }\n try {\n return await fn();\n } finally {\n await this.release();\n }\n }\n\n /** Close the underlying socket (idempotent). */\n async close(): Promise<void> {\n if (this.closed) return;\n this.closed = true;\n this.stopRenew();\n if (this.sock) {\n this.sock.destroy();\n this.sock = null;\n }\n this.token = null;\n }\n\n // -- internals --\n\n private startRenew(): void {\n const interval = Math.max(1, this.lease * this.renewRatio) * 1000;\n const loop = async () => {\n if (!this.sock || !this.token) return;\n try {\n await renew(this.sock, this.key, this.token, this.leaseTtlS);\n } catch {\n console.error(\n `lock lost (renew failed): key=${this.key} token=${this.token}`,\n );\n this.token = null;\n return;\n }\n this.renewTimer = setTimeout(loop, interval);\n };\n this.renewTimer = setTimeout(loop, interval);\n }\n\n private stopRenew(): void {\n if (this.renewTimer != null) {\n clearTimeout(this.renewTimer);\n this.renewTimer = null;\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAAqB;AAErB,IAAM,eAAe;AACrB,IAAM,eAAe;AAMd,IAAM,sBAAN,cAAkC,MAAM;AAAA,EAC7C,YAAY,KAAa;AACvB,UAAM,sBAAsB,GAAG,GAAG;AAClC,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,YAAN,cAAwB,MAAM;AAAA,EACnC,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAMA,SAAS,eAAe,OAAyB;AAC/C,SAAO,OAAO,KAAK,MAAM,IAAI,CAAC,MAAM,IAAI,IAAI,EAAE,KAAK,EAAE,GAAG,OAAO;AACjE;AAOA,SAAS,SAAS,MAAmC;AACnD,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI,MAAM;AAEV,UAAM,SAAS,CAAC,UAAkB;AAChC,aAAO,MAAM,SAAS,OAAO;AAC7B,YAAM,MAAM,IAAI,QAAQ,IAAI;AAC5B,UAAI,QAAQ,IAAI;AACd,gBAAQ;AACR,gBAAQ,IAAI,MAAM,GAAG,GAAG,EAAE,QAAQ,OAAO,EAAE,CAAC;AAAA,MAC9C;AAAA,IACF;AAEA,UAAM,UAAU,CAAC,QAAe;AAC9B,cAAQ;AACR,aAAO,GAAG;AAAA,IACZ;AAEA,UAAM,UAAU,MAAM;AACpB,cAAQ;AACR,aAAO,IAAI,UAAU,0BAA0B,CAAC;AAAA,IAClD;AAEA,UAAM,UAAU,MAAM;AACpB,WAAK,eAAe,QAAQ,MAAM;AAClC,WAAK,eAAe,SAAS,OAAO;AACpC,WAAK,eAAe,SAAS,OAAO;AAAA,IACtC;AAEA,SAAK,GAAG,QAAQ,MAAM;AACtB,SAAK,GAAG,SAAS,OAAO;AACxB,SAAK,GAAG,SAAS,OAAO;AAAA,EAC1B,CAAC;AACH;AAEA,SAAS,QAAQ,MAAc,MAAmC;AAChE,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,OAAW,qBAAiB,EAAE,MAAM,KAAK,GAAG,MAAM,QAAQ,IAAI,CAAC;AACrE,SAAK,GAAG,SAAS,MAAM;AAAA,EACzB,CAAC;AACH;AAMA,eAAsB,QACpB,MACA,KACA,iBACA,WAC2C;AAC3C,QAAM,MACJ,aAAa,OACT,OAAO,eAAe,IACtB,GAAG,eAAe,IAAI,SAAS;AAErC,OAAK,MAAM,YAAY,KAAK,KAAK,GAAG,CAAC;AAErC,QAAM,OAAO,MAAM,SAAS,IAAI;AAChC,MAAI,SAAS,WAAW;AACtB,UAAM,IAAI,oBAAoB,GAAG;AAAA,EACnC;AACA,MAAI,CAAC,KAAK,WAAW,KAAK,GAAG;AAC3B,UAAM,IAAI,UAAU,oBAAoB,IAAI,GAAG;AAAA,EACjD;AAEA,QAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,MAAI,MAAM,SAAS,GAAG;AACpB,UAAM,IAAI,UAAU,qBAAqB,IAAI,GAAG;AAAA,EAClD;AACA,QAAM,QAAQ,MAAM,CAAC;AACrB,QAAM,QAAQ,MAAM,UAAU,IAAI,SAAS,MAAM,CAAC,GAAG,EAAE,IAAI;AAC3D,SAAO,EAAE,OAAO,MAAM;AACxB;AAEA,eAAsB,MACpB,MACA,KACA,OACA,WACiB;AACjB,QAAM,MAAM,aAAa,OAAO,QAAQ,GAAG,KAAK,IAAI,SAAS;AAC7D,OAAK,MAAM,YAAY,KAAK,KAAK,GAAG,CAAC;AAErC,QAAM,OAAO,MAAM,SAAS,IAAI;AAChC,MAAI,CAAC,KAAK,WAAW,IAAI,GAAG;AAC1B,UAAM,IAAI,UAAU,kBAAkB,IAAI,GAAG;AAAA,EAC/C;AAEA,QAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,MAAI,MAAM,UAAU,KAAK,QAAQ,KAAK,MAAM,CAAC,CAAC,GAAG;AAC/C,WAAO,SAAS,MAAM,CAAC,GAAG,EAAE;AAAA,EAC9B;AACA,SAAO;AACT;AAEA,eAAsB,QACpB,MACA,KACA,WACwF;AACxF,QAAM,MAAM,aAAa,OAAO,KAAK,OAAO,SAAS;AACrD,OAAK,MAAM,YAAY,KAAK,KAAK,GAAG,CAAC;AAErC,QAAM,OAAO,MAAM,SAAS,IAAI;AAChC,MAAI,KAAK,WAAW,WAAW,GAAG;AAChC,UAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,UAAM,QAAQ,MAAM,CAAC;AACrB,UAAM,QAAQ,MAAM,UAAU,IAAI,SAAS,MAAM,CAAC,GAAG,EAAE,IAAI;AAC3D,WAAO,EAAE,QAAQ,YAAY,OAAO,MAAM;AAAA,EAC5C;AACA,MAAI,SAAS,UAAU;AACrB,WAAO,EAAE,QAAQ,UAAU,OAAO,MAAM,OAAO,KAAK;AAAA,EACtD;AACA,QAAM,IAAI,UAAU,oBAAoB,IAAI,GAAG;AACjD;AAEA,eAAsB,YACpB,MACA,KACA,cAC2C;AAC3C,OAAK,MAAM,YAAY,KAAK,KAAK,OAAO,YAAY,CAAC,CAAC;AAEtD,QAAM,OAAO,MAAM,SAAS,IAAI;AAChC,MAAI,SAAS,WAAW;AACtB,UAAM,IAAI,oBAAoB,GAAG;AAAA,EACnC;AACA,MAAI,CAAC,KAAK,WAAW,KAAK,GAAG;AAC3B,UAAM,IAAI,UAAU,iBAAiB,IAAI,GAAG;AAAA,EAC9C;AAEA,QAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,QAAM,QAAQ,MAAM,CAAC;AACrB,QAAM,QAAQ,MAAM,UAAU,IAAI,SAAS,MAAM,CAAC,GAAG,EAAE,IAAI;AAC3D,SAAO,EAAE,OAAO,MAAM;AACxB;AAEA,eAAsB,QACpB,MACA,KACA,OACe;AACf,OAAK,MAAM,YAAY,KAAK,KAAK,KAAK,CAAC;AAEvC,QAAM,OAAO,MAAM,SAAS,IAAI;AAChC,MAAI,SAAS,MAAM;AACjB,UAAM,IAAI,UAAU,oBAAoB,IAAI,GAAG;AAAA,EACjD;AACF;AAeO,IAAM,kBAAN,MAAsB;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET,QAAuB;AAAA,EACvB,QAAgB;AAAA,EAER,OAA0B;AAAA,EAC1B,aAAmD;AAAA,EACnD,SAAS;AAAA,EAEjB,YAAY,MAA8B;AACxC,SAAK,MAAM,KAAK;AAChB,SAAK,kBAAkB,KAAK,mBAAmB;AAC/C,SAAK,YAAY,KAAK;AACtB,SAAK,OAAO,KAAK,QAAQ;AACzB,SAAK,OAAO,KAAK,QAAQ;AACzB,SAAK,aAAa,KAAK,cAAc;AAAA,EACvC;AAAA;AAAA,EAGA,MAAM,UAA4B;AAChC,SAAK,SAAS;AACd,SAAK,OAAO,MAAM,QAAQ,KAAK,MAAM,KAAK,IAAI;AAC9C,QAAI;AACF,YAAM,SAAS,MAAM;AAAA,QACnB,KAAK;AAAA,QACL,KAAK;AAAA,QACL,KAAK;AAAA,QACL,KAAK;AAAA,MACP;AACA,WAAK,QAAQ,OAAO;AACpB,WAAK,QAAQ,OAAO;AAAA,IACtB,SAAS,KAAK;AACZ,YAAM,KAAK,MAAM;AACjB,UAAI,eAAe,oBAAqB,QAAO;AAC/C,YAAM;AAAA,IACR;AACA,SAAK,WAAW;AAChB,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,MAAM,UAAyB;AAC7B,QAAI;AACF,WAAK,UAAU;AACf,UAAI,KAAK,QAAQ,KAAK,OAAO;AAC3B,cAAM,QAAQ,KAAK,MAAM,KAAK,KAAK,KAAK,KAAK;AAAA,MAC/C;AAAA,IACF,UAAE;AACA,YAAM,KAAK,MAAM;AAAA,IACnB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,UAA0C;AAC9C,SAAK,SAAS;AACd,SAAK,OAAO,MAAM,QAAQ,KAAK,MAAM,KAAK,IAAI;AAC9C,QAAI;AACF,YAAM,SAAS,MAAM,QAAQ,KAAK,MAAM,KAAK,KAAK,KAAK,SAAS;AAChE,UAAI,OAAO,WAAW,YAAY;AAChC,aAAK,QAAQ,OAAO;AACpB,aAAK,QAAQ,OAAO,SAAS;AAC7B,aAAK,WAAW;AAAA,MAClB;AACA,aAAO,OAAO;AAAA,IAChB,SAAS,KAAK;AACZ,YAAM,KAAK,MAAM;AACjB,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,KAAK,UAAqC;AAC9C,QAAI,KAAK,UAAU,MAAM;AAEvB,aAAO;AAAA,IACT;AACA,QAAI,CAAC,KAAK,MAAM;AACd,YAAM,IAAI,UAAU,qCAAqC;AAAA,IAC3D;AACA,UAAM,UAAU,YAAY,KAAK;AACjC,QAAI;AACF,YAAM,SAAS,MAAM,YAAY,KAAK,MAAM,KAAK,KAAK,OAAO;AAC7D,WAAK,QAAQ,OAAO;AACpB,WAAK,QAAQ,OAAO;AAAA,IACtB,SAAS,KAAK;AACZ,YAAM,KAAK,MAAM;AACjB,UAAI,eAAe,oBAAqB,QAAO;AAC/C,YAAM;AAAA,IACR;AACA,SAAK,WAAW;AAChB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,SAAY,IAAsC;AACtD,UAAM,KAAK,MAAM,KAAK,QAAQ;AAC9B,QAAI,CAAC,IAAI;AACP,YAAM,IAAI,oBAAoB,KAAK,GAAG;AAAA,IACxC;AACA,QAAI;AACF,aAAO,MAAM,GAAG;AAAA,IAClB,UAAE;AACA,YAAM,KAAK,QAAQ;AAAA,IACrB;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,QAAuB;AAC3B,QAAI,KAAK,OAAQ;AACjB,SAAK,SAAS;AACd,SAAK,UAAU;AACf,QAAI,KAAK,MAAM;AACb,WAAK,KAAK,QAAQ;AAClB,WAAK,OAAO;AAAA,IACd;AACA,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA,EAIQ,aAAmB;AACzB,UAAM,WAAW,KAAK,IAAI,GAAG,KAAK,QAAQ,KAAK,UAAU,IAAI;AAC7D,UAAM,OAAO,YAAY;AACvB,UAAI,CAAC,KAAK,QAAQ,CAAC,KAAK,MAAO;AAC/B,UAAI;AACF,cAAM,MAAM,KAAK,MAAM,KAAK,KAAK,KAAK,OAAO,KAAK,SAAS;AAAA,MAC7D,QAAQ;AACN,gBAAQ;AAAA,UACN,iCAAiC,KAAK,GAAG,UAAU,KAAK,KAAK;AAAA,QAC/D;AACA,aAAK,QAAQ;AACb;AAAA,MACF;AACA,WAAK,aAAa,WAAW,MAAM,QAAQ;AAAA,IAC7C;AACA,SAAK,aAAa,WAAW,MAAM,QAAQ;AAAA,EAC7C;AAAA,EAEQ,YAAkB;AACxB,QAAI,KAAK,cAAc,MAAM;AAC3B,mBAAa,KAAK,UAAU;AAC5B,WAAK,aAAa;AAAA,IACpB;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as net from 'net';
|
|
2
|
+
|
|
3
|
+
declare class AcquireTimeoutError extends Error {
|
|
4
|
+
constructor(key: string);
|
|
5
|
+
}
|
|
6
|
+
declare class LockError extends Error {
|
|
7
|
+
constructor(message: string);
|
|
8
|
+
}
|
|
9
|
+
declare function acquire(sock: net.Socket, key: string, acquireTimeoutS: number, leaseTtlS?: number): Promise<{
|
|
10
|
+
token: string;
|
|
11
|
+
lease: number;
|
|
12
|
+
}>;
|
|
13
|
+
declare function renew(sock: net.Socket, key: string, token: string, leaseTtlS?: number): Promise<number>;
|
|
14
|
+
declare function enqueue(sock: net.Socket, key: string, leaseTtlS?: number): Promise<{
|
|
15
|
+
status: "acquired" | "queued";
|
|
16
|
+
token: string | null;
|
|
17
|
+
lease: number | null;
|
|
18
|
+
}>;
|
|
19
|
+
declare function waitForLock(sock: net.Socket, key: string, waitTimeoutS: number): Promise<{
|
|
20
|
+
token: string;
|
|
21
|
+
lease: number;
|
|
22
|
+
}>;
|
|
23
|
+
declare function release(sock: net.Socket, key: string, token: string): Promise<void>;
|
|
24
|
+
interface DistributedLockOptions {
|
|
25
|
+
key: string;
|
|
26
|
+
acquireTimeoutS?: number;
|
|
27
|
+
leaseTtlS?: number;
|
|
28
|
+
host?: string;
|
|
29
|
+
port?: number;
|
|
30
|
+
renewRatio?: number;
|
|
31
|
+
}
|
|
32
|
+
declare class DistributedLock {
|
|
33
|
+
readonly key: string;
|
|
34
|
+
readonly acquireTimeoutS: number;
|
|
35
|
+
readonly leaseTtlS: number | undefined;
|
|
36
|
+
readonly host: string;
|
|
37
|
+
readonly port: number;
|
|
38
|
+
readonly renewRatio: number;
|
|
39
|
+
token: string | null;
|
|
40
|
+
lease: number;
|
|
41
|
+
private sock;
|
|
42
|
+
private renewTimer;
|
|
43
|
+
private closed;
|
|
44
|
+
constructor(opts: DistributedLockOptions);
|
|
45
|
+
/** Acquire the lock. Returns `true` on success, `false` on timeout. */
|
|
46
|
+
acquire(): Promise<boolean>;
|
|
47
|
+
/** Release the lock and close the connection. */
|
|
48
|
+
release(): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Two-phase step 1: connect and join the FIFO queue.
|
|
51
|
+
* Returns `"acquired"` (fast-path, lock is already held) or `"queued"`.
|
|
52
|
+
* If acquired immediately, the renew loop starts automatically.
|
|
53
|
+
*/
|
|
54
|
+
enqueue(): Promise<"acquired" | "queued">;
|
|
55
|
+
/**
|
|
56
|
+
* Two-phase step 2: block until the lock is granted.
|
|
57
|
+
* Returns `true` if granted, `false` on timeout.
|
|
58
|
+
* If already acquired during `enqueue()`, returns `true` immediately.
|
|
59
|
+
*/
|
|
60
|
+
wait(timeoutS?: number): Promise<boolean>;
|
|
61
|
+
/**
|
|
62
|
+
* Run `fn` while holding the lock, then release automatically.
|
|
63
|
+
*
|
|
64
|
+
* ```ts
|
|
65
|
+
* const lock = new DistributedLock({ key: "my-resource" });
|
|
66
|
+
* await lock.withLock(async () => {
|
|
67
|
+
* // critical section
|
|
68
|
+
* });
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
withLock<T>(fn: () => T | Promise<T>): Promise<T>;
|
|
72
|
+
/** Close the underlying socket (idempotent). */
|
|
73
|
+
close(): Promise<void>;
|
|
74
|
+
private startRenew;
|
|
75
|
+
private stopRenew;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export { AcquireTimeoutError, DistributedLock, type DistributedLockOptions, LockError, acquire, enqueue, release, renew, waitForLock };
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as net from 'net';
|
|
2
|
+
|
|
3
|
+
declare class AcquireTimeoutError extends Error {
|
|
4
|
+
constructor(key: string);
|
|
5
|
+
}
|
|
6
|
+
declare class LockError extends Error {
|
|
7
|
+
constructor(message: string);
|
|
8
|
+
}
|
|
9
|
+
declare function acquire(sock: net.Socket, key: string, acquireTimeoutS: number, leaseTtlS?: number): Promise<{
|
|
10
|
+
token: string;
|
|
11
|
+
lease: number;
|
|
12
|
+
}>;
|
|
13
|
+
declare function renew(sock: net.Socket, key: string, token: string, leaseTtlS?: number): Promise<number>;
|
|
14
|
+
declare function enqueue(sock: net.Socket, key: string, leaseTtlS?: number): Promise<{
|
|
15
|
+
status: "acquired" | "queued";
|
|
16
|
+
token: string | null;
|
|
17
|
+
lease: number | null;
|
|
18
|
+
}>;
|
|
19
|
+
declare function waitForLock(sock: net.Socket, key: string, waitTimeoutS: number): Promise<{
|
|
20
|
+
token: string;
|
|
21
|
+
lease: number;
|
|
22
|
+
}>;
|
|
23
|
+
declare function release(sock: net.Socket, key: string, token: string): Promise<void>;
|
|
24
|
+
interface DistributedLockOptions {
|
|
25
|
+
key: string;
|
|
26
|
+
acquireTimeoutS?: number;
|
|
27
|
+
leaseTtlS?: number;
|
|
28
|
+
host?: string;
|
|
29
|
+
port?: number;
|
|
30
|
+
renewRatio?: number;
|
|
31
|
+
}
|
|
32
|
+
declare class DistributedLock {
|
|
33
|
+
readonly key: string;
|
|
34
|
+
readonly acquireTimeoutS: number;
|
|
35
|
+
readonly leaseTtlS: number | undefined;
|
|
36
|
+
readonly host: string;
|
|
37
|
+
readonly port: number;
|
|
38
|
+
readonly renewRatio: number;
|
|
39
|
+
token: string | null;
|
|
40
|
+
lease: number;
|
|
41
|
+
private sock;
|
|
42
|
+
private renewTimer;
|
|
43
|
+
private closed;
|
|
44
|
+
constructor(opts: DistributedLockOptions);
|
|
45
|
+
/** Acquire the lock. Returns `true` on success, `false` on timeout. */
|
|
46
|
+
acquire(): Promise<boolean>;
|
|
47
|
+
/** Release the lock and close the connection. */
|
|
48
|
+
release(): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Two-phase step 1: connect and join the FIFO queue.
|
|
51
|
+
* Returns `"acquired"` (fast-path, lock is already held) or `"queued"`.
|
|
52
|
+
* If acquired immediately, the renew loop starts automatically.
|
|
53
|
+
*/
|
|
54
|
+
enqueue(): Promise<"acquired" | "queued">;
|
|
55
|
+
/**
|
|
56
|
+
* Two-phase step 2: block until the lock is granted.
|
|
57
|
+
* Returns `true` if granted, `false` on timeout.
|
|
58
|
+
* If already acquired during `enqueue()`, returns `true` immediately.
|
|
59
|
+
*/
|
|
60
|
+
wait(timeoutS?: number): Promise<boolean>;
|
|
61
|
+
/**
|
|
62
|
+
* Run `fn` while holding the lock, then release automatically.
|
|
63
|
+
*
|
|
64
|
+
* ```ts
|
|
65
|
+
* const lock = new DistributedLock({ key: "my-resource" });
|
|
66
|
+
* await lock.withLock(async () => {
|
|
67
|
+
* // critical section
|
|
68
|
+
* });
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
withLock<T>(fn: () => T | Promise<T>): Promise<T>;
|
|
72
|
+
/** Close the underlying socket (idempotent). */
|
|
73
|
+
close(): Promise<void>;
|
|
74
|
+
private startRenew;
|
|
75
|
+
private stopRenew;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export { AcquireTimeoutError, DistributedLock, type DistributedLockOptions, LockError, acquire, enqueue, release, renew, waitForLock };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import * as net from "net";
|
|
3
|
+
var DEFAULT_HOST = "127.0.0.1";
|
|
4
|
+
var DEFAULT_PORT = 6388;
|
|
5
|
+
var AcquireTimeoutError = class extends Error {
|
|
6
|
+
constructor(key) {
|
|
7
|
+
super(`timeout acquiring '${key}'`);
|
|
8
|
+
this.name = "AcquireTimeoutError";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var LockError = class extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "LockError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
function encodeLines(...lines) {
|
|
18
|
+
return Buffer.from(lines.map((l) => l + "\n").join(""), "utf-8");
|
|
19
|
+
}
|
|
20
|
+
function readline(sock) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
let buf = "";
|
|
23
|
+
const onData = (chunk) => {
|
|
24
|
+
buf += chunk.toString("utf-8");
|
|
25
|
+
const idx = buf.indexOf("\n");
|
|
26
|
+
if (idx !== -1) {
|
|
27
|
+
cleanup();
|
|
28
|
+
resolve(buf.slice(0, idx).replace(/\r$/, ""));
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const onError = (err) => {
|
|
32
|
+
cleanup();
|
|
33
|
+
reject(err);
|
|
34
|
+
};
|
|
35
|
+
const onClose = () => {
|
|
36
|
+
cleanup();
|
|
37
|
+
reject(new LockError("server closed connection"));
|
|
38
|
+
};
|
|
39
|
+
const cleanup = () => {
|
|
40
|
+
sock.removeListener("data", onData);
|
|
41
|
+
sock.removeListener("error", onError);
|
|
42
|
+
sock.removeListener("close", onClose);
|
|
43
|
+
};
|
|
44
|
+
sock.on("data", onData);
|
|
45
|
+
sock.on("error", onError);
|
|
46
|
+
sock.on("close", onClose);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function connect(host, port) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const sock = net.createConnection({ host, port }, () => resolve(sock));
|
|
52
|
+
sock.on("error", reject);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async function acquire(sock, key, acquireTimeoutS, leaseTtlS) {
|
|
56
|
+
const arg = leaseTtlS == null ? String(acquireTimeoutS) : `${acquireTimeoutS} ${leaseTtlS}`;
|
|
57
|
+
sock.write(encodeLines("l", key, arg));
|
|
58
|
+
const resp = await readline(sock);
|
|
59
|
+
if (resp === "timeout") {
|
|
60
|
+
throw new AcquireTimeoutError(key);
|
|
61
|
+
}
|
|
62
|
+
if (!resp.startsWith("ok ")) {
|
|
63
|
+
throw new LockError(`acquire failed: '${resp}'`);
|
|
64
|
+
}
|
|
65
|
+
const parts = resp.split(" ");
|
|
66
|
+
if (parts.length < 2) {
|
|
67
|
+
throw new LockError(`bad ok response: '${resp}'`);
|
|
68
|
+
}
|
|
69
|
+
const token = parts[1];
|
|
70
|
+
const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 30;
|
|
71
|
+
return { token, lease };
|
|
72
|
+
}
|
|
73
|
+
async function renew(sock, key, token, leaseTtlS) {
|
|
74
|
+
const arg = leaseTtlS == null ? token : `${token} ${leaseTtlS}`;
|
|
75
|
+
sock.write(encodeLines("n", key, arg));
|
|
76
|
+
const resp = await readline(sock);
|
|
77
|
+
if (!resp.startsWith("ok")) {
|
|
78
|
+
throw new LockError(`renew failed: '${resp}'`);
|
|
79
|
+
}
|
|
80
|
+
const parts = resp.split(" ");
|
|
81
|
+
if (parts.length >= 2 && /^\d+$/.test(parts[1])) {
|
|
82
|
+
return parseInt(parts[1], 10);
|
|
83
|
+
}
|
|
84
|
+
return -1;
|
|
85
|
+
}
|
|
86
|
+
async function enqueue(sock, key, leaseTtlS) {
|
|
87
|
+
const arg = leaseTtlS == null ? "" : String(leaseTtlS);
|
|
88
|
+
sock.write(encodeLines("e", key, arg));
|
|
89
|
+
const resp = await readline(sock);
|
|
90
|
+
if (resp.startsWith("acquired ")) {
|
|
91
|
+
const parts = resp.split(" ");
|
|
92
|
+
const token = parts[1];
|
|
93
|
+
const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 33;
|
|
94
|
+
return { status: "acquired", token, lease };
|
|
95
|
+
}
|
|
96
|
+
if (resp === "queued") {
|
|
97
|
+
return { status: "queued", token: null, lease: null };
|
|
98
|
+
}
|
|
99
|
+
throw new LockError(`enqueue failed: '${resp}'`);
|
|
100
|
+
}
|
|
101
|
+
async function waitForLock(sock, key, waitTimeoutS) {
|
|
102
|
+
sock.write(encodeLines("w", key, String(waitTimeoutS)));
|
|
103
|
+
const resp = await readline(sock);
|
|
104
|
+
if (resp === "timeout") {
|
|
105
|
+
throw new AcquireTimeoutError(key);
|
|
106
|
+
}
|
|
107
|
+
if (!resp.startsWith("ok ")) {
|
|
108
|
+
throw new LockError(`wait failed: '${resp}'`);
|
|
109
|
+
}
|
|
110
|
+
const parts = resp.split(" ");
|
|
111
|
+
const token = parts[1];
|
|
112
|
+
const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 33;
|
|
113
|
+
return { token, lease };
|
|
114
|
+
}
|
|
115
|
+
async function release(sock, key, token) {
|
|
116
|
+
sock.write(encodeLines("r", key, token));
|
|
117
|
+
const resp = await readline(sock);
|
|
118
|
+
if (resp !== "ok") {
|
|
119
|
+
throw new LockError(`release failed: '${resp}'`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
var DistributedLock = class {
|
|
123
|
+
key;
|
|
124
|
+
acquireTimeoutS;
|
|
125
|
+
leaseTtlS;
|
|
126
|
+
host;
|
|
127
|
+
port;
|
|
128
|
+
renewRatio;
|
|
129
|
+
token = null;
|
|
130
|
+
lease = 0;
|
|
131
|
+
sock = null;
|
|
132
|
+
renewTimer = null;
|
|
133
|
+
closed = false;
|
|
134
|
+
constructor(opts) {
|
|
135
|
+
this.key = opts.key;
|
|
136
|
+
this.acquireTimeoutS = opts.acquireTimeoutS ?? 10;
|
|
137
|
+
this.leaseTtlS = opts.leaseTtlS;
|
|
138
|
+
this.host = opts.host ?? DEFAULT_HOST;
|
|
139
|
+
this.port = opts.port ?? DEFAULT_PORT;
|
|
140
|
+
this.renewRatio = opts.renewRatio ?? 0.5;
|
|
141
|
+
}
|
|
142
|
+
/** Acquire the lock. Returns `true` on success, `false` on timeout. */
|
|
143
|
+
async acquire() {
|
|
144
|
+
this.closed = false;
|
|
145
|
+
this.sock = await connect(this.host, this.port);
|
|
146
|
+
try {
|
|
147
|
+
const result = await acquire(
|
|
148
|
+
this.sock,
|
|
149
|
+
this.key,
|
|
150
|
+
this.acquireTimeoutS,
|
|
151
|
+
this.leaseTtlS
|
|
152
|
+
);
|
|
153
|
+
this.token = result.token;
|
|
154
|
+
this.lease = result.lease;
|
|
155
|
+
} catch (err) {
|
|
156
|
+
await this.close();
|
|
157
|
+
if (err instanceof AcquireTimeoutError) return false;
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
this.startRenew();
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
/** Release the lock and close the connection. */
|
|
164
|
+
async release() {
|
|
165
|
+
try {
|
|
166
|
+
this.stopRenew();
|
|
167
|
+
if (this.sock && this.token) {
|
|
168
|
+
await release(this.sock, this.key, this.token);
|
|
169
|
+
}
|
|
170
|
+
} finally {
|
|
171
|
+
await this.close();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Two-phase step 1: connect and join the FIFO queue.
|
|
176
|
+
* Returns `"acquired"` (fast-path, lock is already held) or `"queued"`.
|
|
177
|
+
* If acquired immediately, the renew loop starts automatically.
|
|
178
|
+
*/
|
|
179
|
+
async enqueue() {
|
|
180
|
+
this.closed = false;
|
|
181
|
+
this.sock = await connect(this.host, this.port);
|
|
182
|
+
try {
|
|
183
|
+
const result = await enqueue(this.sock, this.key, this.leaseTtlS);
|
|
184
|
+
if (result.status === "acquired") {
|
|
185
|
+
this.token = result.token;
|
|
186
|
+
this.lease = result.lease ?? 0;
|
|
187
|
+
this.startRenew();
|
|
188
|
+
}
|
|
189
|
+
return result.status;
|
|
190
|
+
} catch (err) {
|
|
191
|
+
await this.close();
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Two-phase step 2: block until the lock is granted.
|
|
197
|
+
* Returns `true` if granted, `false` on timeout.
|
|
198
|
+
* If already acquired during `enqueue()`, returns `true` immediately.
|
|
199
|
+
*/
|
|
200
|
+
async wait(timeoutS) {
|
|
201
|
+
if (this.token !== null) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
if (!this.sock) {
|
|
205
|
+
throw new LockError("not connected; call enqueue() first");
|
|
206
|
+
}
|
|
207
|
+
const timeout = timeoutS ?? this.acquireTimeoutS;
|
|
208
|
+
try {
|
|
209
|
+
const result = await waitForLock(this.sock, this.key, timeout);
|
|
210
|
+
this.token = result.token;
|
|
211
|
+
this.lease = result.lease;
|
|
212
|
+
} catch (err) {
|
|
213
|
+
await this.close();
|
|
214
|
+
if (err instanceof AcquireTimeoutError) return false;
|
|
215
|
+
throw err;
|
|
216
|
+
}
|
|
217
|
+
this.startRenew();
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Run `fn` while holding the lock, then release automatically.
|
|
222
|
+
*
|
|
223
|
+
* ```ts
|
|
224
|
+
* const lock = new DistributedLock({ key: "my-resource" });
|
|
225
|
+
* await lock.withLock(async () => {
|
|
226
|
+
* // critical section
|
|
227
|
+
* });
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
async withLock(fn) {
|
|
231
|
+
const ok = await this.acquire();
|
|
232
|
+
if (!ok) {
|
|
233
|
+
throw new AcquireTimeoutError(this.key);
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
return await fn();
|
|
237
|
+
} finally {
|
|
238
|
+
await this.release();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** Close the underlying socket (idempotent). */
|
|
242
|
+
async close() {
|
|
243
|
+
if (this.closed) return;
|
|
244
|
+
this.closed = true;
|
|
245
|
+
this.stopRenew();
|
|
246
|
+
if (this.sock) {
|
|
247
|
+
this.sock.destroy();
|
|
248
|
+
this.sock = null;
|
|
249
|
+
}
|
|
250
|
+
this.token = null;
|
|
251
|
+
}
|
|
252
|
+
// -- internals --
|
|
253
|
+
startRenew() {
|
|
254
|
+
const interval = Math.max(1, this.lease * this.renewRatio) * 1e3;
|
|
255
|
+
const loop = async () => {
|
|
256
|
+
if (!this.sock || !this.token) return;
|
|
257
|
+
try {
|
|
258
|
+
await renew(this.sock, this.key, this.token, this.leaseTtlS);
|
|
259
|
+
} catch {
|
|
260
|
+
console.error(
|
|
261
|
+
`lock lost (renew failed): key=${this.key} token=${this.token}`
|
|
262
|
+
);
|
|
263
|
+
this.token = null;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
this.renewTimer = setTimeout(loop, interval);
|
|
267
|
+
};
|
|
268
|
+
this.renewTimer = setTimeout(loop, interval);
|
|
269
|
+
}
|
|
270
|
+
stopRenew() {
|
|
271
|
+
if (this.renewTimer != null) {
|
|
272
|
+
clearTimeout(this.renewTimer);
|
|
273
|
+
this.renewTimer = null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
export {
|
|
278
|
+
AcquireTimeoutError,
|
|
279
|
+
DistributedLock,
|
|
280
|
+
LockError,
|
|
281
|
+
acquire,
|
|
282
|
+
enqueue,
|
|
283
|
+
release,
|
|
284
|
+
renew,
|
|
285
|
+
waitForLock
|
|
286
|
+
};
|
|
287
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["import * as net from \"net\";\n\nconst DEFAULT_HOST = \"127.0.0.1\";\nconst DEFAULT_PORT = 6388;\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\nexport class AcquireTimeoutError extends Error {\n constructor(key: string) {\n super(`timeout acquiring '${key}'`);\n this.name = \"AcquireTimeoutError\";\n }\n}\n\nexport class LockError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"LockError\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Low-level helpers\n// ---------------------------------------------------------------------------\n\nfunction encodeLines(...lines: string[]): Buffer {\n return Buffer.from(lines.map((l) => l + \"\\n\").join(\"\"), \"utf-8\");\n}\n\n/**\n * Read one newline-terminated line from the socket.\n * Resolves with the line (without trailing \\r\\n).\n * Rejects if the connection closes before a full line arrives.\n */\nfunction readline(sock: net.Socket): Promise<string> {\n return new Promise((resolve, reject) => {\n let buf = \"\";\n\n const onData = (chunk: Buffer) => {\n buf += chunk.toString(\"utf-8\");\n const idx = buf.indexOf(\"\\n\");\n if (idx !== -1) {\n cleanup();\n resolve(buf.slice(0, idx).replace(/\\r$/, \"\"));\n }\n };\n\n const onError = (err: Error) => {\n cleanup();\n reject(err);\n };\n\n const onClose = () => {\n cleanup();\n reject(new LockError(\"server closed connection\"));\n };\n\n const cleanup = () => {\n sock.removeListener(\"data\", onData);\n sock.removeListener(\"error\", onError);\n sock.removeListener(\"close\", onClose);\n };\n\n sock.on(\"data\", onData);\n sock.on(\"error\", onError);\n sock.on(\"close\", onClose);\n });\n}\n\nfunction connect(host: string, port: number): Promise<net.Socket> {\n return new Promise((resolve, reject) => {\n const sock = net.createConnection({ host, port }, () => resolve(sock));\n sock.on(\"error\", reject);\n });\n}\n\n// ---------------------------------------------------------------------------\n// Protocol functions\n// ---------------------------------------------------------------------------\n\nexport async function acquire(\n sock: net.Socket,\n key: string,\n acquireTimeoutS: number,\n leaseTtlS?: number,\n): Promise<{ token: string; lease: number }> {\n const arg =\n leaseTtlS == null\n ? String(acquireTimeoutS)\n : `${acquireTimeoutS} ${leaseTtlS}`;\n\n sock.write(encodeLines(\"l\", key, arg));\n\n const resp = await readline(sock);\n if (resp === \"timeout\") {\n throw new AcquireTimeoutError(key);\n }\n if (!resp.startsWith(\"ok \")) {\n throw new LockError(`acquire failed: '${resp}'`);\n }\n\n const parts = resp.split(\" \");\n if (parts.length < 2) {\n throw new LockError(`bad ok response: '${resp}'`);\n }\n const token = parts[1];\n const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 30;\n return { token, lease };\n}\n\nexport async function renew(\n sock: net.Socket,\n key: string,\n token: string,\n leaseTtlS?: number,\n): Promise<number> {\n const arg = leaseTtlS == null ? token : `${token} ${leaseTtlS}`;\n sock.write(encodeLines(\"n\", key, arg));\n\n const resp = await readline(sock);\n if (!resp.startsWith(\"ok\")) {\n throw new LockError(`renew failed: '${resp}'`);\n }\n\n const parts = resp.split(\" \");\n if (parts.length >= 2 && /^\\d+$/.test(parts[1])) {\n return parseInt(parts[1], 10);\n }\n return -1;\n}\n\nexport async function enqueue(\n sock: net.Socket,\n key: string,\n leaseTtlS?: number,\n): Promise<{ status: \"acquired\" | \"queued\"; token: string | null; lease: number | null }> {\n const arg = leaseTtlS == null ? \"\" : String(leaseTtlS);\n sock.write(encodeLines(\"e\", key, arg));\n\n const resp = await readline(sock);\n if (resp.startsWith(\"acquired \")) {\n const parts = resp.split(\" \");\n const token = parts[1];\n const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 33;\n return { status: \"acquired\", token, lease };\n }\n if (resp === \"queued\") {\n return { status: \"queued\", token: null, lease: null };\n }\n throw new LockError(`enqueue failed: '${resp}'`);\n}\n\nexport async function waitForLock(\n sock: net.Socket,\n key: string,\n waitTimeoutS: number,\n): Promise<{ token: string; lease: number }> {\n sock.write(encodeLines(\"w\", key, String(waitTimeoutS)));\n\n const resp = await readline(sock);\n if (resp === \"timeout\") {\n throw new AcquireTimeoutError(key);\n }\n if (!resp.startsWith(\"ok \")) {\n throw new LockError(`wait failed: '${resp}'`);\n }\n\n const parts = resp.split(\" \");\n const token = parts[1];\n const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 33;\n return { token, lease };\n}\n\nexport async function release(\n sock: net.Socket,\n key: string,\n token: string,\n): Promise<void> {\n sock.write(encodeLines(\"r\", key, token));\n\n const resp = await readline(sock);\n if (resp !== \"ok\") {\n throw new LockError(`release failed: '${resp}'`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// DistributedLock\n// ---------------------------------------------------------------------------\n\nexport interface DistributedLockOptions {\n key: string;\n acquireTimeoutS?: number;\n leaseTtlS?: number;\n host?: string;\n port?: number;\n renewRatio?: number;\n}\n\nexport class DistributedLock {\n readonly key: string;\n readonly acquireTimeoutS: number;\n readonly leaseTtlS: number | undefined;\n readonly host: string;\n readonly port: number;\n readonly renewRatio: number;\n\n token: string | null = null;\n lease: number = 0;\n\n private sock: net.Socket | null = null;\n private renewTimer: ReturnType<typeof setTimeout> | null = null;\n private closed = false;\n\n constructor(opts: DistributedLockOptions) {\n this.key = opts.key;\n this.acquireTimeoutS = opts.acquireTimeoutS ?? 10;\n this.leaseTtlS = opts.leaseTtlS;\n this.host = opts.host ?? DEFAULT_HOST;\n this.port = opts.port ?? DEFAULT_PORT;\n this.renewRatio = opts.renewRatio ?? 0.5;\n }\n\n /** Acquire the lock. Returns `true` on success, `false` on timeout. */\n async acquire(): Promise<boolean> {\n this.closed = false;\n this.sock = await connect(this.host, this.port);\n try {\n const result = await acquire(\n this.sock,\n this.key,\n this.acquireTimeoutS,\n this.leaseTtlS,\n );\n this.token = result.token;\n this.lease = result.lease;\n } catch (err) {\n await this.close();\n if (err instanceof AcquireTimeoutError) return false;\n throw err;\n }\n this.startRenew();\n return true;\n }\n\n /** Release the lock and close the connection. */\n async release(): Promise<void> {\n try {\n this.stopRenew();\n if (this.sock && this.token) {\n await release(this.sock, this.key, this.token);\n }\n } finally {\n await this.close();\n }\n }\n\n /**\n * Two-phase step 1: connect and join the FIFO queue.\n * Returns `\"acquired\"` (fast-path, lock is already held) or `\"queued\"`.\n * If acquired immediately, the renew loop starts automatically.\n */\n async enqueue(): Promise<\"acquired\" | \"queued\"> {\n this.closed = false;\n this.sock = await connect(this.host, this.port);\n try {\n const result = await enqueue(this.sock, this.key, this.leaseTtlS);\n if (result.status === \"acquired\") {\n this.token = result.token;\n this.lease = result.lease ?? 0;\n this.startRenew();\n }\n return result.status;\n } catch (err) {\n await this.close();\n throw err;\n }\n }\n\n /**\n * Two-phase step 2: block until the lock is granted.\n * Returns `true` if granted, `false` on timeout.\n * If already acquired during `enqueue()`, returns `true` immediately.\n */\n async wait(timeoutS?: number): Promise<boolean> {\n if (this.token !== null) {\n // Already acquired during enqueue (fast path)\n return true;\n }\n if (!this.sock) {\n throw new LockError(\"not connected; call enqueue() first\");\n }\n const timeout = timeoutS ?? this.acquireTimeoutS;\n try {\n const result = await waitForLock(this.sock, this.key, timeout);\n this.token = result.token;\n this.lease = result.lease;\n } catch (err) {\n await this.close();\n if (err instanceof AcquireTimeoutError) return false;\n throw err;\n }\n this.startRenew();\n return true;\n }\n\n /**\n * Run `fn` while holding the lock, then release automatically.\n *\n * ```ts\n * const lock = new DistributedLock({ key: \"my-resource\" });\n * await lock.withLock(async () => {\n * // critical section\n * });\n * ```\n */\n async withLock<T>(fn: () => T | Promise<T>): Promise<T> {\n const ok = await this.acquire();\n if (!ok) {\n throw new AcquireTimeoutError(this.key);\n }\n try {\n return await fn();\n } finally {\n await this.release();\n }\n }\n\n /** Close the underlying socket (idempotent). */\n async close(): Promise<void> {\n if (this.closed) return;\n this.closed = true;\n this.stopRenew();\n if (this.sock) {\n this.sock.destroy();\n this.sock = null;\n }\n this.token = null;\n }\n\n // -- internals --\n\n private startRenew(): void {\n const interval = Math.max(1, this.lease * this.renewRatio) * 1000;\n const loop = async () => {\n if (!this.sock || !this.token) return;\n try {\n await renew(this.sock, this.key, this.token, this.leaseTtlS);\n } catch {\n console.error(\n `lock lost (renew failed): key=${this.key} token=${this.token}`,\n );\n this.token = null;\n return;\n }\n this.renewTimer = setTimeout(loop, interval);\n };\n this.renewTimer = setTimeout(loop, interval);\n }\n\n private stopRenew(): void {\n if (this.renewTimer != null) {\n clearTimeout(this.renewTimer);\n this.renewTimer = null;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dflockd-client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "TypeScript client for the dflockd distributed lock daemon",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": {
|
|
9
|
+
"types": "./dist/client.d.ts",
|
|
10
|
+
"default": "./dist/client.js"
|
|
11
|
+
},
|
|
12
|
+
"require": {
|
|
13
|
+
"types": "./dist/client.d.cts",
|
|
14
|
+
"default": "./dist/client.cjs"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"main": "dist/client.cjs",
|
|
19
|
+
"module": "dist/client.js",
|
|
20
|
+
"types": "dist/client.d.ts",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"distributed-lock",
|
|
31
|
+
"lock",
|
|
32
|
+
"mutex",
|
|
33
|
+
"dflockd",
|
|
34
|
+
"distributed"
|
|
35
|
+
],
|
|
36
|
+
"author": "",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=16"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/mtingers/dflockd-client-ts.git"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.0.0",
|
|
47
|
+
"tsup": "^8.0.0",
|
|
48
|
+
"typescript": "^5.7.0"
|
|
49
|
+
}
|
|
50
|
+
}
|