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