dflockd-client 1.8.3 → 1.9.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/README.md +4 -0
- package/dist/client.cjs +348 -264
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +97 -86
- package/dist/client.d.ts +97 -86
- package/dist/client.js +348 -264
- package/dist/client.js.map +1 -1
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -21,6 +21,33 @@ var AuthError = class extends LockError {
|
|
|
21
21
|
this.name = "AuthError";
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
|
+
function validateKey(key) {
|
|
25
|
+
if (key === "") {
|
|
26
|
+
throw new LockError("key must not be empty");
|
|
27
|
+
}
|
|
28
|
+
if (/[\0\n\r]/.test(key)) {
|
|
29
|
+
throw new LockError(
|
|
30
|
+
"key must not contain NUL, newline, or carriage return characters"
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function validateAuth(auth) {
|
|
35
|
+
if (/[\0\n\r]/.test(auth)) {
|
|
36
|
+
throw new LockError(
|
|
37
|
+
"auth token must not contain NUL, newline, or carriage return characters"
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function validateToken(token) {
|
|
42
|
+
if (token === "") {
|
|
43
|
+
throw new LockError("token must not be empty");
|
|
44
|
+
}
|
|
45
|
+
if (/[\0\n\r]/.test(token)) {
|
|
46
|
+
throw new LockError(
|
|
47
|
+
"token must not contain NUL, newline, or carriage return characters"
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
24
51
|
function encodeLines(...lines) {
|
|
25
52
|
return Buffer.from(lines.map((l) => l + "\n").join(""), "utf-8");
|
|
26
53
|
}
|
|
@@ -32,7 +59,13 @@ function writeAll(sock, data) {
|
|
|
32
59
|
});
|
|
33
60
|
});
|
|
34
61
|
}
|
|
62
|
+
function parseLease(value, fallback = 30) {
|
|
63
|
+
if (value == null || value === "") return fallback;
|
|
64
|
+
const n = Number(value);
|
|
65
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
66
|
+
}
|
|
35
67
|
var _readlineBuf = /* @__PURE__ */ new WeakMap();
|
|
68
|
+
var MAX_LINE_LENGTH = 1024 * 1024;
|
|
36
69
|
function readline(sock) {
|
|
37
70
|
return new Promise((resolve, reject) => {
|
|
38
71
|
let buf = _readlineBuf.get(sock) ?? "";
|
|
@@ -51,6 +84,10 @@ function readline(sock) {
|
|
|
51
84
|
const line = buf.slice(0, idx).replace(/\r$/, "");
|
|
52
85
|
_readlineBuf.set(sock, buf.slice(idx + 1));
|
|
53
86
|
resolve(line);
|
|
87
|
+
} else if (buf.length > MAX_LINE_LENGTH) {
|
|
88
|
+
cleanup();
|
|
89
|
+
_readlineBuf.delete(sock);
|
|
90
|
+
reject(new LockError("server response exceeded maximum line length"));
|
|
54
91
|
}
|
|
55
92
|
};
|
|
56
93
|
const onError = (err) => {
|
|
@@ -63,38 +100,78 @@ function readline(sock) {
|
|
|
63
100
|
_readlineBuf.delete(sock);
|
|
64
101
|
reject(new LockError("server closed connection"));
|
|
65
102
|
};
|
|
103
|
+
const onEnd = () => {
|
|
104
|
+
cleanup();
|
|
105
|
+
_readlineBuf.delete(sock);
|
|
106
|
+
reject(new LockError("server closed connection"));
|
|
107
|
+
};
|
|
66
108
|
const cleanup = () => {
|
|
67
109
|
sock.removeListener("data", onData);
|
|
68
110
|
sock.removeListener("error", onError);
|
|
69
111
|
sock.removeListener("close", onClose);
|
|
112
|
+
sock.removeListener("end", onEnd);
|
|
70
113
|
};
|
|
71
114
|
sock.on("data", onData);
|
|
72
115
|
sock.on("error", onError);
|
|
73
116
|
sock.on("close", onClose);
|
|
117
|
+
sock.on("end", onEnd);
|
|
74
118
|
});
|
|
75
119
|
}
|
|
76
|
-
async function connect2(host, port, tlsOptions, auth) {
|
|
120
|
+
async function connect2(host, port, tlsOptions, auth, connectTimeoutMs) {
|
|
77
121
|
const sock = await new Promise((resolve, reject) => {
|
|
122
|
+
let timer = null;
|
|
123
|
+
let settled = false;
|
|
124
|
+
const connectEvent = tlsOptions ? "secureConnect" : "connect";
|
|
125
|
+
const onConnect = () => {
|
|
126
|
+
if (settled) return;
|
|
127
|
+
settled = true;
|
|
128
|
+
if (timer) clearTimeout(timer);
|
|
129
|
+
s.removeListener("error", onError);
|
|
130
|
+
resolve(s);
|
|
131
|
+
};
|
|
132
|
+
const onError = (err) => {
|
|
133
|
+
if (settled) return;
|
|
134
|
+
settled = true;
|
|
135
|
+
if (timer) clearTimeout(timer);
|
|
136
|
+
s.removeListener(connectEvent, onConnect);
|
|
137
|
+
s.destroy();
|
|
138
|
+
reject(err);
|
|
139
|
+
};
|
|
140
|
+
let s;
|
|
78
141
|
if (tlsOptions) {
|
|
79
|
-
|
|
80
|
-
s.removeListener("error", reject);
|
|
81
|
-
resolve(s);
|
|
82
|
-
});
|
|
83
|
-
s.on("error", reject);
|
|
142
|
+
s = tls.connect({ ...tlsOptions, host, port }, onConnect);
|
|
84
143
|
} else {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
144
|
+
s = net.createConnection({ host, port }, onConnect);
|
|
145
|
+
}
|
|
146
|
+
s.on("error", onError);
|
|
147
|
+
if (connectTimeoutMs != null && connectTimeoutMs > 0) {
|
|
148
|
+
timer = setTimeout(() => {
|
|
149
|
+
if (settled) return;
|
|
150
|
+
settled = true;
|
|
151
|
+
s.removeListener("error", onError);
|
|
152
|
+
s.removeListener(connectEvent, onConnect);
|
|
153
|
+
s.destroy();
|
|
154
|
+
reject(
|
|
155
|
+
new LockError(
|
|
156
|
+
`connect timed out after ${connectTimeoutMs}ms to ${host}:${port}`
|
|
157
|
+
)
|
|
158
|
+
);
|
|
159
|
+
}, connectTimeoutMs);
|
|
90
160
|
}
|
|
91
161
|
});
|
|
92
162
|
sock.setNoDelay(true);
|
|
93
163
|
sock.on("error", () => {
|
|
94
164
|
});
|
|
95
165
|
if (auth != null && auth !== "") {
|
|
96
|
-
|
|
97
|
-
|
|
166
|
+
validateAuth(auth);
|
|
167
|
+
let resp;
|
|
168
|
+
try {
|
|
169
|
+
await writeAll(sock, encodeLines("auth", "_", auth));
|
|
170
|
+
resp = await readline(sock);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
sock.destroy();
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
98
175
|
if (resp === "ok") {
|
|
99
176
|
return sock;
|
|
100
177
|
}
|
|
@@ -122,9 +199,19 @@ function crc32(buf) {
|
|
|
122
199
|
return (crc ^ 4294967295) >>> 0;
|
|
123
200
|
}
|
|
124
201
|
function stableHashShard(key, numServers) {
|
|
125
|
-
|
|
202
|
+
if (numServers <= 0) {
|
|
203
|
+
throw new LockError("numServers must be greater than 0");
|
|
204
|
+
}
|
|
205
|
+
return crc32(Buffer.from(key, "utf-8")) % numServers;
|
|
126
206
|
}
|
|
127
207
|
async function acquire(sock, key, acquireTimeoutS, leaseTtlS) {
|
|
208
|
+
validateKey(key);
|
|
209
|
+
if (!Number.isFinite(acquireTimeoutS) || acquireTimeoutS < 0) {
|
|
210
|
+
throw new LockError("acquireTimeoutS must be a finite number >= 0");
|
|
211
|
+
}
|
|
212
|
+
if (leaseTtlS != null && (!Number.isFinite(leaseTtlS) || leaseTtlS <= 0)) {
|
|
213
|
+
throw new LockError("leaseTtlS must be a finite number > 0");
|
|
214
|
+
}
|
|
128
215
|
const arg = leaseTtlS == null ? String(acquireTimeoutS) : `${acquireTimeoutS} ${leaseTtlS}`;
|
|
129
216
|
await writeAll(sock, encodeLines("l", key, arg));
|
|
130
217
|
const resp = await readline(sock);
|
|
@@ -136,30 +223,44 @@ async function acquire(sock, key, acquireTimeoutS, leaseTtlS) {
|
|
|
136
223
|
}
|
|
137
224
|
const parts = resp.split(" ");
|
|
138
225
|
const token = parts[1];
|
|
139
|
-
|
|
226
|
+
if (!token) {
|
|
227
|
+
throw new LockError(`acquire: server returned no token: '${resp}'`);
|
|
228
|
+
}
|
|
229
|
+
const lease = parseLease(parts[2]);
|
|
140
230
|
return { token, lease };
|
|
141
231
|
}
|
|
142
232
|
async function renew(sock, key, token, leaseTtlS) {
|
|
233
|
+
validateKey(key);
|
|
234
|
+
validateToken(token);
|
|
235
|
+
if (leaseTtlS != null && (!Number.isFinite(leaseTtlS) || leaseTtlS <= 0)) {
|
|
236
|
+
throw new LockError("leaseTtlS must be a finite number > 0");
|
|
237
|
+
}
|
|
143
238
|
const arg = leaseTtlS == null ? token : `${token} ${leaseTtlS}`;
|
|
144
239
|
await writeAll(sock, encodeLines("n", key, arg));
|
|
145
240
|
const resp = await readline(sock);
|
|
146
241
|
if (resp !== "ok" && !resp.startsWith("ok ")) {
|
|
147
242
|
throw new LockError(`renew failed: '${resp}'`);
|
|
148
243
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return parseInt(parts[1], 10);
|
|
244
|
+
if (resp === "ok") {
|
|
245
|
+
return leaseTtlS ?? 30;
|
|
152
246
|
}
|
|
153
|
-
|
|
247
|
+
return parseLease(resp.split(" ")[1]);
|
|
154
248
|
}
|
|
155
249
|
async function enqueue(sock, key, leaseTtlS) {
|
|
250
|
+
validateKey(key);
|
|
251
|
+
if (leaseTtlS != null && (!Number.isFinite(leaseTtlS) || leaseTtlS <= 0)) {
|
|
252
|
+
throw new LockError("leaseTtlS must be a finite number > 0");
|
|
253
|
+
}
|
|
156
254
|
const arg = leaseTtlS == null ? "" : String(leaseTtlS);
|
|
157
255
|
await writeAll(sock, encodeLines("e", key, arg));
|
|
158
256
|
const resp = await readline(sock);
|
|
159
257
|
if (resp.startsWith("acquired ")) {
|
|
160
258
|
const parts = resp.split(" ");
|
|
161
259
|
const token = parts[1];
|
|
162
|
-
|
|
260
|
+
if (!token) {
|
|
261
|
+
throw new LockError(`enqueue: server returned no token: '${resp}'`);
|
|
262
|
+
}
|
|
263
|
+
const lease = parseLease(parts[2]);
|
|
163
264
|
return { status: "acquired", token, lease };
|
|
164
265
|
}
|
|
165
266
|
if (resp === "queued") {
|
|
@@ -168,6 +269,10 @@ async function enqueue(sock, key, leaseTtlS) {
|
|
|
168
269
|
throw new LockError(`enqueue failed: '${resp}'`);
|
|
169
270
|
}
|
|
170
271
|
async function waitForLock(sock, key, waitTimeoutS) {
|
|
272
|
+
validateKey(key);
|
|
273
|
+
if (!Number.isFinite(waitTimeoutS) || waitTimeoutS < 0) {
|
|
274
|
+
throw new LockError("waitTimeoutS must be a finite number >= 0");
|
|
275
|
+
}
|
|
171
276
|
await writeAll(sock, encodeLines("w", key, String(waitTimeoutS)));
|
|
172
277
|
const resp = await readline(sock);
|
|
173
278
|
if (resp === "timeout") {
|
|
@@ -178,10 +283,15 @@ async function waitForLock(sock, key, waitTimeoutS) {
|
|
|
178
283
|
}
|
|
179
284
|
const parts = resp.split(" ");
|
|
180
285
|
const token = parts[1];
|
|
181
|
-
|
|
286
|
+
if (!token) {
|
|
287
|
+
throw new LockError(`wait: server returned no token: '${resp}'`);
|
|
288
|
+
}
|
|
289
|
+
const lease = parseLease(parts[2]);
|
|
182
290
|
return { token, lease };
|
|
183
291
|
}
|
|
184
292
|
async function release(sock, key, token) {
|
|
293
|
+
validateKey(key);
|
|
294
|
+
validateToken(token);
|
|
185
295
|
await writeAll(sock, encodeLines("r", key, token));
|
|
186
296
|
const resp = await readline(sock);
|
|
187
297
|
if (resp !== "ok") {
|
|
@@ -189,6 +299,16 @@ async function release(sock, key, token) {
|
|
|
189
299
|
}
|
|
190
300
|
}
|
|
191
301
|
async function semAcquire(sock, key, acquireTimeoutS, limit, leaseTtlS) {
|
|
302
|
+
validateKey(key);
|
|
303
|
+
if (!Number.isFinite(acquireTimeoutS) || acquireTimeoutS < 0) {
|
|
304
|
+
throw new LockError("acquireTimeoutS must be a finite number >= 0");
|
|
305
|
+
}
|
|
306
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
307
|
+
throw new LockError("limit must be an integer >= 1");
|
|
308
|
+
}
|
|
309
|
+
if (leaseTtlS != null && (!Number.isFinite(leaseTtlS) || leaseTtlS <= 0)) {
|
|
310
|
+
throw new LockError("leaseTtlS must be a finite number > 0");
|
|
311
|
+
}
|
|
192
312
|
const arg = leaseTtlS == null ? `${acquireTimeoutS} ${limit}` : `${acquireTimeoutS} ${limit} ${leaseTtlS}`;
|
|
193
313
|
await writeAll(sock, encodeLines("sl", key, arg));
|
|
194
314
|
const resp = await readline(sock);
|
|
@@ -200,30 +320,47 @@ async function semAcquire(sock, key, acquireTimeoutS, limit, leaseTtlS) {
|
|
|
200
320
|
}
|
|
201
321
|
const parts = resp.split(" ");
|
|
202
322
|
const token = parts[1];
|
|
203
|
-
|
|
323
|
+
if (!token) {
|
|
324
|
+
throw new LockError(`sem_acquire: server returned no token: '${resp}'`);
|
|
325
|
+
}
|
|
326
|
+
const lease = parseLease(parts[2]);
|
|
204
327
|
return { token, lease };
|
|
205
328
|
}
|
|
206
329
|
async function semRenew(sock, key, token, leaseTtlS) {
|
|
330
|
+
validateKey(key);
|
|
331
|
+
validateToken(token);
|
|
332
|
+
if (leaseTtlS != null && (!Number.isFinite(leaseTtlS) || leaseTtlS <= 0)) {
|
|
333
|
+
throw new LockError("leaseTtlS must be a finite number > 0");
|
|
334
|
+
}
|
|
207
335
|
const arg = leaseTtlS == null ? token : `${token} ${leaseTtlS}`;
|
|
208
336
|
await writeAll(sock, encodeLines("sn", key, arg));
|
|
209
337
|
const resp = await readline(sock);
|
|
210
338
|
if (resp !== "ok" && !resp.startsWith("ok ")) {
|
|
211
339
|
throw new LockError(`sem_renew failed: '${resp}'`);
|
|
212
340
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return parseInt(parts[1], 10);
|
|
341
|
+
if (resp === "ok") {
|
|
342
|
+
return leaseTtlS ?? 30;
|
|
216
343
|
}
|
|
217
|
-
|
|
344
|
+
return parseLease(resp.split(" ")[1]);
|
|
218
345
|
}
|
|
219
346
|
async function semEnqueue(sock, key, limit, leaseTtlS) {
|
|
347
|
+
validateKey(key);
|
|
348
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
349
|
+
throw new LockError("limit must be an integer >= 1");
|
|
350
|
+
}
|
|
351
|
+
if (leaseTtlS != null && (!Number.isFinite(leaseTtlS) || leaseTtlS <= 0)) {
|
|
352
|
+
throw new LockError("leaseTtlS must be a finite number > 0");
|
|
353
|
+
}
|
|
220
354
|
const arg = leaseTtlS == null ? String(limit) : `${limit} ${leaseTtlS}`;
|
|
221
355
|
await writeAll(sock, encodeLines("se", key, arg));
|
|
222
356
|
const resp = await readline(sock);
|
|
223
357
|
if (resp.startsWith("acquired ")) {
|
|
224
358
|
const parts = resp.split(" ");
|
|
225
359
|
const token = parts[1];
|
|
226
|
-
|
|
360
|
+
if (!token) {
|
|
361
|
+
throw new LockError(`sem_enqueue: server returned no token: '${resp}'`);
|
|
362
|
+
}
|
|
363
|
+
const lease = parseLease(parts[2]);
|
|
227
364
|
return { status: "acquired", token, lease };
|
|
228
365
|
}
|
|
229
366
|
if (resp === "queued") {
|
|
@@ -232,6 +369,10 @@ async function semEnqueue(sock, key, limit, leaseTtlS) {
|
|
|
232
369
|
throw new LockError(`sem_enqueue failed: '${resp}'`);
|
|
233
370
|
}
|
|
234
371
|
async function semWaitForLock(sock, key, waitTimeoutS) {
|
|
372
|
+
validateKey(key);
|
|
373
|
+
if (!Number.isFinite(waitTimeoutS) || waitTimeoutS < 0) {
|
|
374
|
+
throw new LockError("waitTimeoutS must be a finite number >= 0");
|
|
375
|
+
}
|
|
235
376
|
await writeAll(sock, encodeLines("sw", key, String(waitTimeoutS)));
|
|
236
377
|
const resp = await readline(sock);
|
|
237
378
|
if (resp === "timeout") {
|
|
@@ -242,10 +383,15 @@ async function semWaitForLock(sock, key, waitTimeoutS) {
|
|
|
242
383
|
}
|
|
243
384
|
const parts = resp.split(" ");
|
|
244
385
|
const token = parts[1];
|
|
245
|
-
|
|
386
|
+
if (!token) {
|
|
387
|
+
throw new LockError(`sem_wait: server returned no token: '${resp}'`);
|
|
388
|
+
}
|
|
389
|
+
const lease = parseLease(parts[2]);
|
|
246
390
|
return { token, lease };
|
|
247
391
|
}
|
|
248
392
|
async function semRelease(sock, key, token) {
|
|
393
|
+
validateKey(key);
|
|
394
|
+
validateToken(token);
|
|
249
395
|
await writeAll(sock, encodeLines("sr", key, token));
|
|
250
396
|
const resp = await readline(sock);
|
|
251
397
|
if (resp !== "ok") {
|
|
@@ -268,14 +414,14 @@ async function statsProto(sock) {
|
|
|
268
414
|
async function stats(options) {
|
|
269
415
|
const host = options?.host ?? DEFAULT_HOST;
|
|
270
416
|
const port = options?.port ?? DEFAULT_PORT;
|
|
271
|
-
const sock = await connect2(host, port, options?.tls, options?.auth);
|
|
417
|
+
const sock = await connect2(host, port, options?.tls, options?.auth, options?.connectTimeoutMs);
|
|
272
418
|
try {
|
|
273
419
|
return await statsProto(sock);
|
|
274
420
|
} finally {
|
|
275
421
|
sock.destroy();
|
|
276
422
|
}
|
|
277
423
|
}
|
|
278
|
-
var
|
|
424
|
+
var DistributedPrimitive = class {
|
|
279
425
|
key;
|
|
280
426
|
acquireTimeoutS;
|
|
281
427
|
leaseTtlS;
|
|
@@ -285,18 +431,30 @@ var DistributedLock = class {
|
|
|
285
431
|
tls;
|
|
286
432
|
auth;
|
|
287
433
|
onLockLost;
|
|
434
|
+
connectTimeoutMs;
|
|
435
|
+
socketTimeoutMs;
|
|
288
436
|
token = null;
|
|
289
437
|
lease = 0;
|
|
290
438
|
sock = null;
|
|
291
439
|
renewTimer = null;
|
|
440
|
+
renewInFlight = null;
|
|
292
441
|
closed = false;
|
|
293
442
|
constructor(opts) {
|
|
443
|
+
validateKey(opts.key);
|
|
294
444
|
this.key = opts.key;
|
|
295
445
|
this.acquireTimeoutS = opts.acquireTimeoutS ?? 10;
|
|
296
446
|
this.leaseTtlS = opts.leaseTtlS;
|
|
297
447
|
this.tls = opts.tls;
|
|
298
448
|
this.auth = opts.auth;
|
|
299
449
|
this.onLockLost = opts.onLockLost;
|
|
450
|
+
this.connectTimeoutMs = opts.connectTimeoutMs;
|
|
451
|
+
this.socketTimeoutMs = opts.socketTimeoutMs;
|
|
452
|
+
if (!Number.isFinite(this.acquireTimeoutS) || this.acquireTimeoutS < 0) {
|
|
453
|
+
throw new LockError("acquireTimeoutS must be a finite number >= 0");
|
|
454
|
+
}
|
|
455
|
+
if (this.leaseTtlS != null && (!Number.isFinite(this.leaseTtlS) || this.leaseTtlS <= 0)) {
|
|
456
|
+
throw new LockError("leaseTtlS must be a finite number > 0");
|
|
457
|
+
}
|
|
300
458
|
if (opts.servers) {
|
|
301
459
|
if (opts.servers.length === 0) {
|
|
302
460
|
throw new LockError("servers list must not be empty");
|
|
@@ -306,7 +464,36 @@ var DistributedLock = class {
|
|
|
306
464
|
this.servers = [[opts.host ?? DEFAULT_HOST, opts.port ?? DEFAULT_PORT]];
|
|
307
465
|
}
|
|
308
466
|
this.shardingStrategy = opts.shardingStrategy ?? stableHashShard;
|
|
309
|
-
|
|
467
|
+
const renewRatio = opts.renewRatio ?? 0.5;
|
|
468
|
+
if (!Number.isFinite(renewRatio) || renewRatio <= 0 || renewRatio >= 1) {
|
|
469
|
+
throw new LockError("renewRatio must be a finite number between 0 and 1 (exclusive)");
|
|
470
|
+
}
|
|
471
|
+
this.renewRatio = renewRatio;
|
|
472
|
+
}
|
|
473
|
+
// -- public API --
|
|
474
|
+
async openConnection() {
|
|
475
|
+
const [host, port] = this.pickServer();
|
|
476
|
+
const sock = await connect2(host, port, this.tls, this.auth, this.connectTimeoutMs);
|
|
477
|
+
if (this.socketTimeoutMs != null && this.socketTimeoutMs > 0) {
|
|
478
|
+
sock.on("timeout", () => {
|
|
479
|
+
sock.destroy(new LockError("socket idle timeout"));
|
|
480
|
+
});
|
|
481
|
+
sock.setTimeout(this.socketTimeoutMs);
|
|
482
|
+
}
|
|
483
|
+
return sock;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Suspend or restore the socket idle timeout. Only has effect when
|
|
487
|
+
* `socketTimeoutMs` was set at construction time and a listener was
|
|
488
|
+
* registered in `openConnection`.
|
|
489
|
+
*/
|
|
490
|
+
suspendSocketTimeout(sock) {
|
|
491
|
+
sock.setTimeout(0);
|
|
492
|
+
}
|
|
493
|
+
restoreSocketTimeout(sock) {
|
|
494
|
+
if (this.socketTimeoutMs != null && this.socketTimeoutMs > 0) {
|
|
495
|
+
sock.setTimeout(this.socketTimeoutMs);
|
|
496
|
+
}
|
|
310
497
|
}
|
|
311
498
|
pickServer() {
|
|
312
499
|
const idx = this.shardingStrategy(this.key, this.servers.length);
|
|
@@ -318,26 +505,26 @@ var DistributedLock = class {
|
|
|
318
505
|
return this.servers[idx];
|
|
319
506
|
}
|
|
320
507
|
/**
|
|
321
|
-
* Acquire the lock
|
|
322
|
-
*
|
|
508
|
+
* Acquire the lock / semaphore slot.
|
|
509
|
+
* Returns `true` on success, `false` on timeout.
|
|
510
|
+
* @param opts.force - If `true`, silently close any existing connection
|
|
511
|
+
* before acquiring. Defaults to `false`, which throws if already connected.
|
|
323
512
|
*/
|
|
324
513
|
async acquire(opts) {
|
|
325
514
|
if (this.sock && !this.closed) {
|
|
326
515
|
if (!opts?.force) {
|
|
327
|
-
throw new LockError(
|
|
516
|
+
throw new LockError(
|
|
517
|
+
"already connected; call release() or close() first, or pass { force: true }"
|
|
518
|
+
);
|
|
328
519
|
}
|
|
329
520
|
this.close();
|
|
330
521
|
}
|
|
331
522
|
this.closed = false;
|
|
332
|
-
|
|
333
|
-
this.sock = await connect2(host, port, this.tls, this.auth);
|
|
523
|
+
this.sock = await this.openConnection();
|
|
334
524
|
try {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
this.acquireTimeoutS,
|
|
339
|
-
this.leaseTtlS
|
|
340
|
-
);
|
|
525
|
+
this.suspendSocketTimeout(this.sock);
|
|
526
|
+
const result = await this.doAcquire(this.sock);
|
|
527
|
+
this.restoreSocketTimeout(this.sock);
|
|
341
528
|
this.token = result.token;
|
|
342
529
|
this.lease = result.lease;
|
|
343
530
|
} catch (err) {
|
|
@@ -348,12 +535,33 @@ var DistributedLock = class {
|
|
|
348
535
|
this.startRenew();
|
|
349
536
|
return true;
|
|
350
537
|
}
|
|
351
|
-
/**
|
|
538
|
+
/**
|
|
539
|
+
* Release the lock / semaphore slot and close the connection.
|
|
540
|
+
*
|
|
541
|
+
* Throws `LockError` if the instance is already closed (e.g. after a
|
|
542
|
+
* previous `release()` or `close()` call).
|
|
543
|
+
*
|
|
544
|
+
* The server-side release itself is best-effort: if the underlying
|
|
545
|
+
* connection is already dead the protocol-level release error is silently
|
|
546
|
+
* ignored so that the method doesn't throw on transient network failures.
|
|
547
|
+
*/
|
|
352
548
|
async release() {
|
|
549
|
+
if (this.closed) {
|
|
550
|
+
throw new LockError("not connected; nothing to release");
|
|
551
|
+
}
|
|
552
|
+
const tokenToRelease = this.token;
|
|
553
|
+
const sockToRelease = this.sock;
|
|
353
554
|
try {
|
|
354
555
|
this.stopRenew();
|
|
355
|
-
if (this.
|
|
356
|
-
await
|
|
556
|
+
if (this.renewInFlight) {
|
|
557
|
+
await this.renewInFlight;
|
|
558
|
+
this.stopRenew();
|
|
559
|
+
}
|
|
560
|
+
if (sockToRelease != null && tokenToRelease != null) {
|
|
561
|
+
try {
|
|
562
|
+
await this.doRelease(sockToRelease, tokenToRelease);
|
|
563
|
+
} catch {
|
|
564
|
+
}
|
|
357
565
|
}
|
|
358
566
|
} finally {
|
|
359
567
|
this.close();
|
|
@@ -361,23 +569,27 @@ var DistributedLock = class {
|
|
|
361
569
|
}
|
|
362
570
|
/**
|
|
363
571
|
* Two-phase step 1: connect and join the FIFO queue.
|
|
364
|
-
* Returns `"acquired"` (fast-path
|
|
572
|
+
* Returns `"acquired"` (fast-path) or `"queued"`.
|
|
365
573
|
* If acquired immediately, the renew loop starts automatically.
|
|
366
|
-
* @param opts.force - If `true`, silently close any existing connection
|
|
574
|
+
* @param opts.force - If `true`, silently close any existing connection
|
|
575
|
+
* before enqueuing. Defaults to `false`, which throws if already connected.
|
|
367
576
|
*/
|
|
368
577
|
async enqueue(opts) {
|
|
369
578
|
if (this.sock && !this.closed) {
|
|
370
579
|
if (!opts?.force) {
|
|
371
|
-
throw new LockError(
|
|
580
|
+
throw new LockError(
|
|
581
|
+
"already connected; call release() or close() first, or pass { force: true }"
|
|
582
|
+
);
|
|
372
583
|
}
|
|
373
584
|
this.close();
|
|
374
585
|
}
|
|
375
586
|
this.closed = false;
|
|
376
|
-
|
|
377
|
-
this.sock = await connect2(host, port, this.tls, this.auth);
|
|
587
|
+
this.sock = await this.openConnection();
|
|
378
588
|
try {
|
|
379
|
-
|
|
589
|
+
this.suspendSocketTimeout(this.sock);
|
|
590
|
+
const result = await this.doEnqueue(this.sock);
|
|
380
591
|
if (result.status === "acquired") {
|
|
592
|
+
this.restoreSocketTimeout(this.sock);
|
|
381
593
|
this.token = result.token;
|
|
382
594
|
this.lease = result.lease ?? 0;
|
|
383
595
|
this.startRenew();
|
|
@@ -389,7 +601,7 @@ var DistributedLock = class {
|
|
|
389
601
|
}
|
|
390
602
|
}
|
|
391
603
|
/**
|
|
392
|
-
* Two-phase step 2: block until the lock is granted.
|
|
604
|
+
* Two-phase step 2: block until the lock / slot is granted.
|
|
393
605
|
* Returns `true` if granted, `false` on timeout.
|
|
394
606
|
* If already acquired during `enqueue()`, returns `true` immediately.
|
|
395
607
|
*/
|
|
@@ -397,12 +609,17 @@ var DistributedLock = class {
|
|
|
397
609
|
if (this.token !== null) {
|
|
398
610
|
return true;
|
|
399
611
|
}
|
|
612
|
+
if (this.closed) {
|
|
613
|
+
throw new LockError("connection closed; call enqueue() again");
|
|
614
|
+
}
|
|
400
615
|
if (!this.sock) {
|
|
401
616
|
throw new LockError("not connected; call enqueue() first");
|
|
402
617
|
}
|
|
403
618
|
const timeout = timeoutS ?? this.acquireTimeoutS;
|
|
404
619
|
try {
|
|
405
|
-
|
|
620
|
+
this.suspendSocketTimeout(this.sock);
|
|
621
|
+
const result = await this.doWait(this.sock, timeout);
|
|
622
|
+
this.restoreSocketTimeout(this.sock);
|
|
406
623
|
this.token = result.token;
|
|
407
624
|
this.lease = result.lease;
|
|
408
625
|
} catch (err) {
|
|
@@ -414,24 +631,28 @@ var DistributedLock = class {
|
|
|
414
631
|
return true;
|
|
415
632
|
}
|
|
416
633
|
/**
|
|
417
|
-
* Run `fn` while holding the lock, then release automatically.
|
|
634
|
+
* Run `fn` while holding the lock / slot, then release automatically.
|
|
418
635
|
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
* await lock.withLock(async () => {
|
|
422
|
-
* // critical section
|
|
423
|
-
* });
|
|
424
|
-
* ```
|
|
636
|
+
* If `fn()` throws, its error is always preserved — a concurrent
|
|
637
|
+
* release failure will not mask it.
|
|
425
638
|
*/
|
|
426
639
|
async withLock(fn) {
|
|
427
640
|
const ok = await this.acquire();
|
|
428
641
|
if (!ok) {
|
|
429
642
|
throw new AcquireTimeoutError(this.key);
|
|
430
643
|
}
|
|
644
|
+
let threw = false;
|
|
431
645
|
try {
|
|
432
646
|
return await fn();
|
|
647
|
+
} catch (err) {
|
|
648
|
+
threw = true;
|
|
649
|
+
throw err;
|
|
433
650
|
} finally {
|
|
434
|
-
|
|
651
|
+
try {
|
|
652
|
+
await this.release();
|
|
653
|
+
} catch (releaseErr) {
|
|
654
|
+
if (!threw) throw releaseErr;
|
|
655
|
+
}
|
|
435
656
|
}
|
|
436
657
|
}
|
|
437
658
|
/** Close the underlying socket (idempotent). */
|
|
@@ -439,29 +660,47 @@ var DistributedLock = class {
|
|
|
439
660
|
if (this.closed) return;
|
|
440
661
|
this.closed = true;
|
|
441
662
|
this.stopRenew();
|
|
663
|
+
this.renewInFlight = null;
|
|
442
664
|
if (this.sock) {
|
|
443
665
|
this.sock.destroy();
|
|
444
666
|
this.sock = null;
|
|
445
667
|
}
|
|
446
668
|
this.token = null;
|
|
669
|
+
this.lease = 0;
|
|
447
670
|
}
|
|
448
671
|
// -- internals --
|
|
449
672
|
startRenew() {
|
|
673
|
+
this.stopRenew();
|
|
450
674
|
const loop = async () => {
|
|
451
675
|
const savedToken = this.token;
|
|
452
|
-
|
|
676
|
+
const sock = this.sock;
|
|
677
|
+
if (!sock || !savedToken) return;
|
|
453
678
|
const start = Date.now();
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
this.token
|
|
459
|
-
|
|
460
|
-
this.onLockLost
|
|
679
|
+
const p = (async () => {
|
|
680
|
+
try {
|
|
681
|
+
this.lease = await this.doRenew(sock, savedToken);
|
|
682
|
+
} catch {
|
|
683
|
+
if (this.token === savedToken) {
|
|
684
|
+
this.token = null;
|
|
685
|
+
if (this.onLockLost) {
|
|
686
|
+
try {
|
|
687
|
+
const result = this.onLockLost(this.key, savedToken);
|
|
688
|
+
if (result instanceof Promise) {
|
|
689
|
+
result.catch(() => {
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
} catch {
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
this.close();
|
|
461
696
|
}
|
|
697
|
+
return;
|
|
462
698
|
}
|
|
463
|
-
|
|
464
|
-
|
|
699
|
+
})();
|
|
700
|
+
this.renewInFlight = p;
|
|
701
|
+
await p;
|
|
702
|
+
this.renewInFlight = null;
|
|
703
|
+
if (this.closed || this.token !== savedToken) return;
|
|
465
704
|
const elapsed = Date.now() - start;
|
|
466
705
|
const interval2 = Math.max(1, this.lease * this.renewRatio) * 1e3;
|
|
467
706
|
this.renewTimer = setTimeout(loop, Math.max(0, interval2 - elapsed));
|
|
@@ -478,210 +717,55 @@ var DistributedLock = class {
|
|
|
478
717
|
}
|
|
479
718
|
}
|
|
480
719
|
};
|
|
481
|
-
var
|
|
482
|
-
key;
|
|
483
|
-
limit;
|
|
484
|
-
acquireTimeoutS;
|
|
485
|
-
leaseTtlS;
|
|
486
|
-
servers;
|
|
487
|
-
shardingStrategy;
|
|
488
|
-
renewRatio;
|
|
489
|
-
tls;
|
|
490
|
-
auth;
|
|
491
|
-
onLockLost;
|
|
492
|
-
token = null;
|
|
493
|
-
lease = 0;
|
|
494
|
-
sock = null;
|
|
495
|
-
renewTimer = null;
|
|
496
|
-
closed = false;
|
|
720
|
+
var DistributedLock = class extends DistributedPrimitive {
|
|
497
721
|
constructor(opts) {
|
|
498
|
-
|
|
499
|
-
this.limit = opts.limit;
|
|
500
|
-
this.acquireTimeoutS = opts.acquireTimeoutS ?? 10;
|
|
501
|
-
this.leaseTtlS = opts.leaseTtlS;
|
|
502
|
-
this.tls = opts.tls;
|
|
503
|
-
this.auth = opts.auth;
|
|
504
|
-
this.onLockLost = opts.onLockLost;
|
|
505
|
-
if (opts.servers) {
|
|
506
|
-
if (opts.servers.length === 0) {
|
|
507
|
-
throw new LockError("servers list must not be empty");
|
|
508
|
-
}
|
|
509
|
-
this.servers = [...opts.servers];
|
|
510
|
-
} else {
|
|
511
|
-
this.servers = [[opts.host ?? DEFAULT_HOST, opts.port ?? DEFAULT_PORT]];
|
|
512
|
-
}
|
|
513
|
-
this.shardingStrategy = opts.shardingStrategy ?? stableHashShard;
|
|
514
|
-
this.renewRatio = opts.renewRatio ?? 0.5;
|
|
722
|
+
super(opts);
|
|
515
723
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
if (!Number.isInteger(idx) || idx < 0 || idx >= this.servers.length) {
|
|
519
|
-
throw new LockError(
|
|
520
|
-
`shardingStrategy returned invalid index ${idx} for ${this.servers.length} server(s)`
|
|
521
|
-
);
|
|
522
|
-
}
|
|
523
|
-
return this.servers[idx];
|
|
724
|
+
doAcquire(sock) {
|
|
725
|
+
return acquire(sock, this.key, this.acquireTimeoutS, this.leaseTtlS);
|
|
524
726
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
* @param opts.force - If `true`, silently close any existing connection before acquiring. Defaults to `false`, which throws if already connected.
|
|
528
|
-
*/
|
|
529
|
-
async acquire(opts) {
|
|
530
|
-
if (this.sock && !this.closed) {
|
|
531
|
-
if (!opts?.force) {
|
|
532
|
-
throw new LockError("already connected; call release() or close() first, or pass { force: true }");
|
|
533
|
-
}
|
|
534
|
-
this.close();
|
|
535
|
-
}
|
|
536
|
-
this.closed = false;
|
|
537
|
-
const [host, port] = this.pickServer();
|
|
538
|
-
this.sock = await connect2(host, port, this.tls, this.auth);
|
|
539
|
-
try {
|
|
540
|
-
const result = await semAcquire(
|
|
541
|
-
this.sock,
|
|
542
|
-
this.key,
|
|
543
|
-
this.acquireTimeoutS,
|
|
544
|
-
this.limit,
|
|
545
|
-
this.leaseTtlS
|
|
546
|
-
);
|
|
547
|
-
this.token = result.token;
|
|
548
|
-
this.lease = result.lease;
|
|
549
|
-
} catch (err) {
|
|
550
|
-
this.close();
|
|
551
|
-
if (err instanceof AcquireTimeoutError) return false;
|
|
552
|
-
throw err;
|
|
553
|
-
}
|
|
554
|
-
this.startRenew();
|
|
555
|
-
return true;
|
|
727
|
+
doEnqueue(sock) {
|
|
728
|
+
return enqueue(sock, this.key, this.leaseTtlS);
|
|
556
729
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
try {
|
|
560
|
-
this.stopRenew();
|
|
561
|
-
if (this.sock && this.token) {
|
|
562
|
-
await semRelease(this.sock, this.key, this.token);
|
|
563
|
-
}
|
|
564
|
-
} finally {
|
|
565
|
-
this.close();
|
|
566
|
-
}
|
|
730
|
+
doWait(sock, timeoutS) {
|
|
731
|
+
return waitForLock(sock, this.key, timeoutS);
|
|
567
732
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
* Returns `"acquired"` (fast-path, slot granted immediately) or `"queued"`.
|
|
571
|
-
* If acquired immediately, the renew loop starts automatically.
|
|
572
|
-
* @param opts.force - If `true`, silently close any existing connection before enqueuing. Defaults to `false`, which throws if already connected.
|
|
573
|
-
*/
|
|
574
|
-
async enqueue(opts) {
|
|
575
|
-
if (this.sock && !this.closed) {
|
|
576
|
-
if (!opts?.force) {
|
|
577
|
-
throw new LockError("already connected; call release() or close() first, or pass { force: true }");
|
|
578
|
-
}
|
|
579
|
-
this.close();
|
|
580
|
-
}
|
|
581
|
-
this.closed = false;
|
|
582
|
-
const [host, port] = this.pickServer();
|
|
583
|
-
this.sock = await connect2(host, port, this.tls, this.auth);
|
|
584
|
-
try {
|
|
585
|
-
const result = await semEnqueue(this.sock, this.key, this.limit, this.leaseTtlS);
|
|
586
|
-
if (result.status === "acquired") {
|
|
587
|
-
this.token = result.token;
|
|
588
|
-
this.lease = result.lease ?? 0;
|
|
589
|
-
this.startRenew();
|
|
590
|
-
}
|
|
591
|
-
return result.status;
|
|
592
|
-
} catch (err) {
|
|
593
|
-
this.close();
|
|
594
|
-
throw err;
|
|
595
|
-
}
|
|
733
|
+
doRelease(sock, token) {
|
|
734
|
+
return release(sock, this.key, token);
|
|
596
735
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
* Returns `true` if granted, `false` on timeout.
|
|
600
|
-
* If already acquired during `enqueue()`, returns `true` immediately.
|
|
601
|
-
*/
|
|
602
|
-
async wait(timeoutS) {
|
|
603
|
-
if (this.token !== null) {
|
|
604
|
-
return true;
|
|
605
|
-
}
|
|
606
|
-
if (!this.sock) {
|
|
607
|
-
throw new LockError("not connected; call enqueue() first");
|
|
608
|
-
}
|
|
609
|
-
const timeout = timeoutS ?? this.acquireTimeoutS;
|
|
610
|
-
try {
|
|
611
|
-
const result = await semWaitForLock(this.sock, this.key, timeout);
|
|
612
|
-
this.token = result.token;
|
|
613
|
-
this.lease = result.lease;
|
|
614
|
-
} catch (err) {
|
|
615
|
-
this.close();
|
|
616
|
-
if (err instanceof AcquireTimeoutError) return false;
|
|
617
|
-
throw err;
|
|
618
|
-
}
|
|
619
|
-
this.startRenew();
|
|
620
|
-
return true;
|
|
736
|
+
doRenew(sock, token) {
|
|
737
|
+
return renew(sock, this.key, token, this.leaseTtlS);
|
|
621
738
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
* });
|
|
630
|
-
* ```
|
|
631
|
-
*/
|
|
632
|
-
async withLock(fn) {
|
|
633
|
-
const ok = await this.acquire();
|
|
634
|
-
if (!ok) {
|
|
635
|
-
throw new AcquireTimeoutError(this.key);
|
|
636
|
-
}
|
|
637
|
-
try {
|
|
638
|
-
return await fn();
|
|
639
|
-
} finally {
|
|
640
|
-
await this.release();
|
|
739
|
+
};
|
|
740
|
+
var DistributedSemaphore = class extends DistributedPrimitive {
|
|
741
|
+
limit;
|
|
742
|
+
constructor(opts) {
|
|
743
|
+
super(opts);
|
|
744
|
+
if (!Number.isInteger(opts.limit) || opts.limit < 1) {
|
|
745
|
+
throw new LockError("limit must be an integer >= 1");
|
|
641
746
|
}
|
|
747
|
+
this.limit = opts.limit;
|
|
642
748
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
this.
|
|
650
|
-
|
|
651
|
-
}
|
|
652
|
-
this.token = null;
|
|
749
|
+
doAcquire(sock) {
|
|
750
|
+
return semAcquire(
|
|
751
|
+
sock,
|
|
752
|
+
this.key,
|
|
753
|
+
this.acquireTimeoutS,
|
|
754
|
+
this.limit,
|
|
755
|
+
this.leaseTtlS
|
|
756
|
+
);
|
|
653
757
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
const loop = async () => {
|
|
657
|
-
const savedToken = this.token;
|
|
658
|
-
if (!this.sock || !savedToken) return;
|
|
659
|
-
const start = Date.now();
|
|
660
|
-
try {
|
|
661
|
-
this.lease = await semRenew(this.sock, this.key, savedToken, this.leaseTtlS);
|
|
662
|
-
} catch {
|
|
663
|
-
if (this.token === savedToken) {
|
|
664
|
-
this.token = null;
|
|
665
|
-
if (this.onLockLost) {
|
|
666
|
-
this.onLockLost(this.key, savedToken);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
const elapsed = Date.now() - start;
|
|
672
|
-
const interval2 = Math.max(1, this.lease * this.renewRatio) * 1e3;
|
|
673
|
-
this.renewTimer = setTimeout(loop, Math.max(0, interval2 - elapsed));
|
|
674
|
-
this.renewTimer.unref();
|
|
675
|
-
};
|
|
676
|
-
const interval = Math.max(1, this.lease * this.renewRatio) * 1e3;
|
|
677
|
-
this.renewTimer = setTimeout(loop, interval);
|
|
678
|
-
this.renewTimer.unref();
|
|
758
|
+
doEnqueue(sock) {
|
|
759
|
+
return semEnqueue(sock, this.key, this.limit, this.leaseTtlS);
|
|
679
760
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
761
|
+
doWait(sock, timeoutS) {
|
|
762
|
+
return semWaitForLock(sock, this.key, timeoutS);
|
|
763
|
+
}
|
|
764
|
+
doRelease(sock, token) {
|
|
765
|
+
return semRelease(sock, this.key, token);
|
|
766
|
+
}
|
|
767
|
+
doRenew(sock, token) {
|
|
768
|
+
return semRenew(sock, this.key, token, this.leaseTtlS);
|
|
685
769
|
}
|
|
686
770
|
};
|
|
687
771
|
export {
|