dflockd-client 1.9.1 → 1.10.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 +94 -6
- package/dist/client.cjs +324 -18
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +105 -1
- package/dist/client.d.ts +105 -1
- package/dist/client.js +322 -18
- package/dist/client.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,8 +103,8 @@ without blocking. If the lock is contended, `enqueue()` returns `"queued"` and
|
|
|
103
103
|
| Option | Type | Default | Description |
|
|
104
104
|
|--------------------|-------------------------------|--------------------------|--------------------------------------------------|
|
|
105
105
|
| `key` | `string` | *(required)* | Lock name |
|
|
106
|
-
| `acquireTimeoutS` | `number` | `10` | Seconds to wait for the lock before giving up
|
|
107
|
-
| `leaseTtlS` | `number` | server default | Server-side lease duration in seconds
|
|
106
|
+
| `acquireTimeoutS` | `number` | `10` | Seconds to wait for the lock before giving up (integer ≥ 0) |
|
|
107
|
+
| `leaseTtlS` | `number` | server default | Server-side lease duration in seconds (integer ≥ 1) |
|
|
108
108
|
| `servers` | `Array<[string, number]>` | `[["127.0.0.1", 6388]]` | List of `[host, port]` pairs |
|
|
109
109
|
| `shardingStrategy` | `ShardingStrategy` | `stableHashShard` | Function mapping `(key, numServers)` to a server index |
|
|
110
110
|
| `host` | `string` | `127.0.0.1` | Server host *(deprecated — use `servers`)* |
|
|
@@ -233,9 +233,9 @@ try {
|
|
|
233
233
|
| Option | Type | Default | Description |
|
|
234
234
|
|--------------------|-------------------------------|--------------------------|--------------------------------------------------|
|
|
235
235
|
| `key` | `string` | *(required)* | Semaphore name |
|
|
236
|
-
| `limit` | `number` | *(required)* | Max concurrent holders
|
|
237
|
-
| `acquireTimeoutS` | `number` | `10` | Seconds to wait before giving up
|
|
238
|
-
| `leaseTtlS` | `number` | server default | Server-side lease duration in seconds
|
|
236
|
+
| `limit` | `number` | *(required)* | Max concurrent holders (integer ≥ 1) |
|
|
237
|
+
| `acquireTimeoutS` | `number` | `10` | Seconds to wait before giving up (integer ≥ 0) |
|
|
238
|
+
| `leaseTtlS` | `number` | server default | Server-side lease duration in seconds (integer ≥ 1) |
|
|
239
239
|
| `servers` | `Array<[string, number]>` | `[["127.0.0.1", 6388]]` | List of `[host, port]` pairs |
|
|
240
240
|
| `shardingStrategy` | `ShardingStrategy` | `stableHashShard` | Function mapping `(key, numServers)` to a server index |
|
|
241
241
|
| `host` | `string` | `127.0.0.1` | Server host *(deprecated — use `servers`)* |
|
|
@@ -356,7 +356,7 @@ import * as net from "net";
|
|
|
356
356
|
import {
|
|
357
357
|
acquire, enqueue, waitForLock, renew, release,
|
|
358
358
|
semAcquire, semEnqueue, semWaitForLock, semRenew, semRelease,
|
|
359
|
-
stats,
|
|
359
|
+
publish, stats,
|
|
360
360
|
} from "dflockd-client";
|
|
361
361
|
|
|
362
362
|
const sock = net.createConnection({ host: "127.0.0.1", port: 6388 });
|
|
@@ -383,9 +383,97 @@ if (semResult.status === "queued") {
|
|
|
383
383
|
const semGranted = await semWaitForLock(sock, "sem-key", 10); // { token, lease }
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
+
// Signal — publish only (no subscription on raw sockets)
|
|
387
|
+
const delivered = await publish(sock, "events.user.login", '{"user":"alice"}');
|
|
388
|
+
|
|
386
389
|
sock.destroy();
|
|
387
390
|
```
|
|
388
391
|
|
|
392
|
+
## Signals
|
|
393
|
+
|
|
394
|
+
Publish/subscribe messaging with NATS-style pattern matching and optional
|
|
395
|
+
queue groups for load-balanced consumption.
|
|
396
|
+
|
|
397
|
+
### `SignalConnection` (recommended)
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
import { SignalConnection } from "dflockd-client";
|
|
401
|
+
|
|
402
|
+
const conn = await SignalConnection.connect();
|
|
403
|
+
|
|
404
|
+
// Subscribe to signals
|
|
405
|
+
conn.onSignal((sig) => {
|
|
406
|
+
console.log(`${sig.channel}: ${sig.payload}`);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
await conn.listen("events.>");
|
|
410
|
+
|
|
411
|
+
// ... later
|
|
412
|
+
await conn.unlisten("events.>");
|
|
413
|
+
conn.close();
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Publish a signal
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
import { SignalConnection } from "dflockd-client";
|
|
420
|
+
|
|
421
|
+
const conn = await SignalConnection.connect();
|
|
422
|
+
const delivered = await conn.emit("events.user.login", '{"user":"alice"}');
|
|
423
|
+
console.log(`delivered to ${delivered} listener(s)`);
|
|
424
|
+
conn.close();
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Pattern matching
|
|
428
|
+
|
|
429
|
+
Subscription patterns support NATS-style wildcards:
|
|
430
|
+
|
|
431
|
+
- `*` matches exactly one dot-separated token (`events.*.login` matches
|
|
432
|
+
`events.user.login` but not `events.user.admin.login`)
|
|
433
|
+
- `>` matches one or more trailing tokens (`events.>` matches
|
|
434
|
+
`events.user.login` and `events.order.created`)
|
|
435
|
+
|
|
436
|
+
Publishing always uses literal channel names (no wildcards).
|
|
437
|
+
|
|
438
|
+
### Queue groups
|
|
439
|
+
|
|
440
|
+
Listeners can join a named queue group. Within a group, each signal is
|
|
441
|
+
delivered to exactly one member (round-robin), enabling load-balanced
|
|
442
|
+
processing. Non-grouped listeners always receive every matching signal.
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
// Worker 1
|
|
446
|
+
await conn.listen("tasks.>", "workers");
|
|
447
|
+
|
|
448
|
+
// Worker 2 (same group — only one receives each signal)
|
|
449
|
+
await conn2.listen("tasks.>", "workers");
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Async iterator
|
|
453
|
+
|
|
454
|
+
`SignalConnection` implements `Symbol.asyncIterator`, so you can consume
|
|
455
|
+
signals with a `for-await-of` loop:
|
|
456
|
+
|
|
457
|
+
```ts
|
|
458
|
+
const conn = await SignalConnection.connect();
|
|
459
|
+
await conn.listen("events.>");
|
|
460
|
+
|
|
461
|
+
for await (const sig of conn) {
|
|
462
|
+
console.log(sig.channel, sig.payload);
|
|
463
|
+
}
|
|
464
|
+
// Loop ends when conn.close() is called
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Signal connection options
|
|
468
|
+
|
|
469
|
+
| Option | Type | Default | Description |
|
|
470
|
+
|--------------------|-------------------------|----------------|-------------------------------------|
|
|
471
|
+
| `host` | `string` | `127.0.0.1` | Server host |
|
|
472
|
+
| `port` | `number` | `6388` | Server port |
|
|
473
|
+
| `tls` | `tls.ConnectionOptions` | `undefined` | TLS options; pass `{}` for system CA|
|
|
474
|
+
| `auth` | `string` | `undefined` | Auth token |
|
|
475
|
+
| `connectTimeoutMs` | `number` | `undefined` | TCP connect timeout in milliseconds |
|
|
476
|
+
|
|
389
477
|
## License
|
|
390
478
|
|
|
391
479
|
MIT
|
package/dist/client.cjs
CHANGED
|
@@ -35,8 +35,10 @@ __export(client_exports, {
|
|
|
35
35
|
DistributedLock: () => DistributedLock,
|
|
36
36
|
DistributedSemaphore: () => DistributedSemaphore,
|
|
37
37
|
LockError: () => LockError,
|
|
38
|
+
SignalConnection: () => SignalConnection,
|
|
38
39
|
acquire: () => acquire,
|
|
39
40
|
enqueue: () => enqueue,
|
|
41
|
+
publish: () => publish,
|
|
40
42
|
release: () => release,
|
|
41
43
|
renew: () => renew,
|
|
42
44
|
semAcquire: () => semAcquire,
|
|
@@ -261,14 +263,14 @@ function stableHashShard(key, numServers) {
|
|
|
261
263
|
}
|
|
262
264
|
async function protoAcquire(sock, cmd, label, key, acquireTimeoutS, leaseTtlS, limit) {
|
|
263
265
|
validateKey(key);
|
|
264
|
-
if (!Number.
|
|
265
|
-
throw new LockError("acquireTimeoutS must be
|
|
266
|
+
if (!Number.isInteger(acquireTimeoutS) || acquireTimeoutS < 0) {
|
|
267
|
+
throw new LockError("acquireTimeoutS must be an integer >= 0");
|
|
266
268
|
}
|
|
267
269
|
if (limit != null && (!Number.isInteger(limit) || limit < 1)) {
|
|
268
270
|
throw new LockError("limit must be an integer >= 1");
|
|
269
271
|
}
|
|
270
|
-
if (leaseTtlS != null && (!Number.
|
|
271
|
-
throw new LockError("leaseTtlS must be
|
|
272
|
+
if (leaseTtlS != null && (!Number.isInteger(leaseTtlS) || leaseTtlS < 1)) {
|
|
273
|
+
throw new LockError("leaseTtlS must be an integer >= 1");
|
|
272
274
|
}
|
|
273
275
|
const parts = [acquireTimeoutS];
|
|
274
276
|
if (limit != null) parts.push(limit);
|
|
@@ -291,8 +293,8 @@ async function protoAcquire(sock, cmd, label, key, acquireTimeoutS, leaseTtlS, l
|
|
|
291
293
|
async function protoRenew(sock, cmd, label, key, token, leaseTtlS) {
|
|
292
294
|
validateKey(key);
|
|
293
295
|
validateToken(token);
|
|
294
|
-
if (leaseTtlS != null && (!Number.
|
|
295
|
-
throw new LockError("leaseTtlS must be
|
|
296
|
+
if (leaseTtlS != null && (!Number.isInteger(leaseTtlS) || leaseTtlS < 1)) {
|
|
297
|
+
throw new LockError("leaseTtlS must be an integer >= 1");
|
|
296
298
|
}
|
|
297
299
|
const arg = leaseTtlS == null ? token : `${token} ${leaseTtlS}`;
|
|
298
300
|
await writeAll(sock, encodeLines(cmd, key, arg));
|
|
@@ -308,8 +310,8 @@ async function protoEnqueue(sock, cmd, label, key, leaseTtlS, limit) {
|
|
|
308
310
|
if (limit != null && (!Number.isInteger(limit) || limit < 1)) {
|
|
309
311
|
throw new LockError("limit must be an integer >= 1");
|
|
310
312
|
}
|
|
311
|
-
if (leaseTtlS != null && (!Number.
|
|
312
|
-
throw new LockError("leaseTtlS must be
|
|
313
|
+
if (leaseTtlS != null && (!Number.isInteger(leaseTtlS) || leaseTtlS < 1)) {
|
|
314
|
+
throw new LockError("leaseTtlS must be an integer >= 1");
|
|
313
315
|
}
|
|
314
316
|
const parts = [];
|
|
315
317
|
if (limit != null) parts.push(limit);
|
|
@@ -331,8 +333,8 @@ async function protoEnqueue(sock, cmd, label, key, leaseTtlS, limit) {
|
|
|
331
333
|
}
|
|
332
334
|
async function protoWait(sock, cmd, label, key, waitTimeoutS) {
|
|
333
335
|
validateKey(key);
|
|
334
|
-
if (!Number.
|
|
335
|
-
throw new LockError("waitTimeoutS must be
|
|
336
|
+
if (!Number.isInteger(waitTimeoutS) || waitTimeoutS < 0) {
|
|
337
|
+
throw new LockError("waitTimeoutS must be an integer >= 0");
|
|
336
338
|
}
|
|
337
339
|
await writeAll(sock, encodeLines(cmd, key, String(waitTimeoutS)));
|
|
338
340
|
const resp = await readline(sock);
|
|
@@ -439,11 +441,11 @@ var DistributedPrimitive = class {
|
|
|
439
441
|
this.onLockLost = opts.onLockLost;
|
|
440
442
|
this.connectTimeoutMs = opts.connectTimeoutMs;
|
|
441
443
|
this.socketTimeoutMs = opts.socketTimeoutMs;
|
|
442
|
-
if (!Number.
|
|
443
|
-
throw new LockError("acquireTimeoutS must be
|
|
444
|
+
if (!Number.isInteger(this.acquireTimeoutS) || this.acquireTimeoutS < 0) {
|
|
445
|
+
throw new LockError("acquireTimeoutS must be an integer >= 0");
|
|
444
446
|
}
|
|
445
|
-
if (this.leaseTtlS != null && (!Number.
|
|
446
|
-
throw new LockError("leaseTtlS must be
|
|
447
|
+
if (this.leaseTtlS != null && (!Number.isInteger(this.leaseTtlS) || this.leaseTtlS < 1)) {
|
|
448
|
+
throw new LockError("leaseTtlS must be an integer >= 1");
|
|
447
449
|
}
|
|
448
450
|
if (opts.servers) {
|
|
449
451
|
if (opts.servers.length === 0) {
|
|
@@ -510,8 +512,8 @@ var DistributedPrimitive = class {
|
|
|
510
512
|
this.close();
|
|
511
513
|
}
|
|
512
514
|
this.closed = false;
|
|
513
|
-
this.sock = await this.openConnection();
|
|
514
515
|
try {
|
|
516
|
+
this.sock = await this.openConnection();
|
|
515
517
|
this.suspendSocketTimeout(this.sock);
|
|
516
518
|
const result = await this.doAcquire(this.sock);
|
|
517
519
|
this.restoreSocketTimeout(this.sock);
|
|
@@ -546,7 +548,7 @@ var DistributedPrimitive = class {
|
|
|
546
548
|
if (this.renewInFlight) {
|
|
547
549
|
await Promise.race([
|
|
548
550
|
this.renewInFlight,
|
|
549
|
-
new Promise((r) => setTimeout(r, 5e3))
|
|
551
|
+
new Promise((r) => setTimeout(r, 5e3).unref())
|
|
550
552
|
]);
|
|
551
553
|
this.stopRenew();
|
|
552
554
|
}
|
|
@@ -577,16 +579,16 @@ var DistributedPrimitive = class {
|
|
|
577
579
|
this.close();
|
|
578
580
|
}
|
|
579
581
|
this.closed = false;
|
|
580
|
-
this.sock = await this.openConnection();
|
|
581
582
|
try {
|
|
583
|
+
this.sock = await this.openConnection();
|
|
582
584
|
this.suspendSocketTimeout(this.sock);
|
|
583
585
|
const result = await this.doEnqueue(this.sock);
|
|
584
586
|
if (result.status === "acquired") {
|
|
585
587
|
this.token = result.token;
|
|
586
588
|
this.lease = result.lease ?? 0;
|
|
589
|
+
this.restoreSocketTimeout(this.sock);
|
|
587
590
|
this.startRenew();
|
|
588
591
|
}
|
|
589
|
-
this.restoreSocketTimeout(this.sock);
|
|
590
592
|
return result.status;
|
|
591
593
|
} catch (err) {
|
|
592
594
|
this.close();
|
|
@@ -764,6 +766,308 @@ var DistributedSemaphore = class extends DistributedPrimitive {
|
|
|
764
766
|
return semRenew(sock, this.key, token, this.leaseTtlS);
|
|
765
767
|
}
|
|
766
768
|
};
|
|
769
|
+
function validatePayload(payload) {
|
|
770
|
+
if (payload === "") {
|
|
771
|
+
throw new LockError("payload must not be empty");
|
|
772
|
+
}
|
|
773
|
+
if (/[\0\n\r]/.test(payload)) {
|
|
774
|
+
throw new LockError(
|
|
775
|
+
"payload must not contain NUL, newline, or carriage return characters"
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
async function publish(sock, channel, payload) {
|
|
780
|
+
validateKey(channel);
|
|
781
|
+
validatePayload(payload);
|
|
782
|
+
await writeAll(sock, encodeLines("signal", channel, payload));
|
|
783
|
+
const resp = await readline(sock);
|
|
784
|
+
if (!resp.startsWith("ok ")) {
|
|
785
|
+
throw new LockError(`signal failed: '${resp}'`);
|
|
786
|
+
}
|
|
787
|
+
const n = parseInt(resp.split(" ")[1], 10);
|
|
788
|
+
if (!Number.isFinite(n)) {
|
|
789
|
+
throw new LockError(`signal: bad delivery count: '${resp}'`);
|
|
790
|
+
}
|
|
791
|
+
return n;
|
|
792
|
+
}
|
|
793
|
+
var SignalConnection = class _SignalConnection {
|
|
794
|
+
sock;
|
|
795
|
+
buf = "";
|
|
796
|
+
responseResolve = null;
|
|
797
|
+
responseReject = null;
|
|
798
|
+
cmdQueue = [];
|
|
799
|
+
cmdBusy = false;
|
|
800
|
+
signalListeners = [];
|
|
801
|
+
_closed = false;
|
|
802
|
+
/**
|
|
803
|
+
* Wrap an existing socket as a SignalConnection.
|
|
804
|
+
*
|
|
805
|
+
* The socket should already be connected (and authenticated, if needed).
|
|
806
|
+
* Prefer `SignalConnection.connect()` for convenience.
|
|
807
|
+
*/
|
|
808
|
+
constructor(sock) {
|
|
809
|
+
this.sock = sock;
|
|
810
|
+
const leftover = _readlineBuf.get(sock);
|
|
811
|
+
if (leftover) {
|
|
812
|
+
this.buf = leftover;
|
|
813
|
+
_readlineBuf.delete(sock);
|
|
814
|
+
}
|
|
815
|
+
sock.on("data", (chunk) => this.onData(chunk));
|
|
816
|
+
sock.on("error", (err) => this.onSocketError(err));
|
|
817
|
+
sock.on("close", () => this.onSocketEnd());
|
|
818
|
+
sock.on("end", () => this.onSocketEnd());
|
|
819
|
+
this.drainLines();
|
|
820
|
+
}
|
|
821
|
+
/** Connect to a dflockd server and return a SignalConnection. */
|
|
822
|
+
static async connect(opts) {
|
|
823
|
+
const host = opts?.host ?? DEFAULT_HOST;
|
|
824
|
+
const port = opts?.port ?? DEFAULT_PORT;
|
|
825
|
+
const sock = await connect2(
|
|
826
|
+
host,
|
|
827
|
+
port,
|
|
828
|
+
opts?.tls,
|
|
829
|
+
opts?.auth,
|
|
830
|
+
opts?.connectTimeoutMs
|
|
831
|
+
);
|
|
832
|
+
return new _SignalConnection(sock);
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Subscribe to signals matching `pattern`.
|
|
836
|
+
*
|
|
837
|
+
* Patterns support NATS-style wildcards:
|
|
838
|
+
* - `*` matches exactly one dot-separated token
|
|
839
|
+
* - `>` matches one or more trailing tokens
|
|
840
|
+
*
|
|
841
|
+
* If `group` is provided, the subscription joins a queue group where
|
|
842
|
+
* signals are load-balanced (round-robin) among group members.
|
|
843
|
+
*/
|
|
844
|
+
async listen(pattern, group) {
|
|
845
|
+
validateKey(pattern);
|
|
846
|
+
if (group != null && /[\0\n\r]/.test(group)) {
|
|
847
|
+
throw new LockError(
|
|
848
|
+
"group must not contain NUL, newline, or carriage return characters"
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
const resp = await this.sendCmd("listen", pattern, group ?? "");
|
|
852
|
+
if (resp !== "ok") {
|
|
853
|
+
throw new LockError(`listen failed: '${resp}'`);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Unsubscribe from signals matching `pattern`.
|
|
858
|
+
* The `pattern` and `group` must match a previous `listen()` call.
|
|
859
|
+
*/
|
|
860
|
+
async unlisten(pattern, group) {
|
|
861
|
+
validateKey(pattern);
|
|
862
|
+
if (group != null && /[\0\n\r]/.test(group)) {
|
|
863
|
+
throw new LockError(
|
|
864
|
+
"group must not contain NUL, newline, or carriage return characters"
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
const resp = await this.sendCmd("unlisten", pattern, group ?? "");
|
|
868
|
+
if (resp !== "ok") {
|
|
869
|
+
throw new LockError(`unlisten failed: '${resp}'`);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Publish a signal on a literal channel (no wildcards).
|
|
874
|
+
* Returns the number of listeners that received the signal.
|
|
875
|
+
*/
|
|
876
|
+
async emit(channel, payload) {
|
|
877
|
+
validateKey(channel);
|
|
878
|
+
validatePayload(payload);
|
|
879
|
+
const resp = await this.sendCmd("signal", channel, payload);
|
|
880
|
+
if (!resp.startsWith("ok ")) {
|
|
881
|
+
throw new LockError(`signal failed: '${resp}'`);
|
|
882
|
+
}
|
|
883
|
+
const n = parseInt(resp.split(" ")[1], 10);
|
|
884
|
+
if (!Number.isFinite(n)) {
|
|
885
|
+
throw new LockError(`signal: bad delivery count: '${resp}'`);
|
|
886
|
+
}
|
|
887
|
+
return n;
|
|
888
|
+
}
|
|
889
|
+
/** Register a listener for incoming signals. */
|
|
890
|
+
onSignal(listener) {
|
|
891
|
+
this.signalListeners.push(listener);
|
|
892
|
+
}
|
|
893
|
+
/** Remove a previously registered signal listener. */
|
|
894
|
+
offSignal(listener) {
|
|
895
|
+
const idx = this.signalListeners.indexOf(listener);
|
|
896
|
+
if (idx >= 0) this.signalListeners.splice(idx, 1);
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Async iterator that yields signals as they arrive.
|
|
900
|
+
* Terminates when the connection closes.
|
|
901
|
+
*
|
|
902
|
+
* ```ts
|
|
903
|
+
* for await (const sig of conn) {
|
|
904
|
+
* console.log(sig.channel, sig.payload);
|
|
905
|
+
* }
|
|
906
|
+
* ```
|
|
907
|
+
*/
|
|
908
|
+
async *[Symbol.asyncIterator]() {
|
|
909
|
+
const buffer = [];
|
|
910
|
+
let waiter = null;
|
|
911
|
+
const listener = (sig) => {
|
|
912
|
+
buffer.push(sig);
|
|
913
|
+
if (waiter) {
|
|
914
|
+
const w = waiter;
|
|
915
|
+
waiter = null;
|
|
916
|
+
w();
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
const onEnd = () => {
|
|
920
|
+
if (waiter) {
|
|
921
|
+
const w = waiter;
|
|
922
|
+
waiter = null;
|
|
923
|
+
w();
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
this.onSignal(listener);
|
|
927
|
+
this.sock.once("close", onEnd);
|
|
928
|
+
try {
|
|
929
|
+
while (true) {
|
|
930
|
+
if (buffer.length > 0) {
|
|
931
|
+
yield buffer.shift();
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
if (this._closed) break;
|
|
935
|
+
await new Promise((resolve) => {
|
|
936
|
+
waiter = resolve;
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
while (buffer.length > 0) {
|
|
940
|
+
yield buffer.shift();
|
|
941
|
+
}
|
|
942
|
+
} finally {
|
|
943
|
+
this.offSignal(listener);
|
|
944
|
+
this.sock.removeListener("close", onEnd);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
/** Close the connection (idempotent). */
|
|
948
|
+
close() {
|
|
949
|
+
if (this._closed) return;
|
|
950
|
+
this._closed = true;
|
|
951
|
+
this.sock.destroy();
|
|
952
|
+
this.rejectPending(new LockError("connection closed"));
|
|
953
|
+
}
|
|
954
|
+
/** Whether the connection is closed. */
|
|
955
|
+
get isClosed() {
|
|
956
|
+
return this._closed;
|
|
957
|
+
}
|
|
958
|
+
// ---- internals ----
|
|
959
|
+
onData(chunk) {
|
|
960
|
+
this.buf += chunk.toString("utf-8");
|
|
961
|
+
if (this.buf.length > MAX_LINE_LENGTH * 2) {
|
|
962
|
+
this.buf = "";
|
|
963
|
+
this._closed = true;
|
|
964
|
+
this.sock.destroy();
|
|
965
|
+
this.rejectPending(
|
|
966
|
+
new LockError("server response exceeded maximum buffer size")
|
|
967
|
+
);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
this.drainLines();
|
|
971
|
+
}
|
|
972
|
+
drainLines() {
|
|
973
|
+
let idx;
|
|
974
|
+
while ((idx = this.buf.indexOf("\n")) !== -1) {
|
|
975
|
+
const line = this.buf.slice(0, idx).replace(/\r$/, "");
|
|
976
|
+
this.buf = this.buf.slice(idx + 1);
|
|
977
|
+
this.handleLine(line);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
handleLine(line) {
|
|
981
|
+
if (line.startsWith("sig ")) {
|
|
982
|
+
const rest = line.slice(4);
|
|
983
|
+
const spaceIdx = rest.indexOf(" ");
|
|
984
|
+
if (spaceIdx < 0) return;
|
|
985
|
+
const sig = {
|
|
986
|
+
channel: rest.slice(0, spaceIdx),
|
|
987
|
+
payload: rest.slice(spaceIdx + 1)
|
|
988
|
+
};
|
|
989
|
+
for (const listener of [...this.signalListeners]) {
|
|
990
|
+
try {
|
|
991
|
+
listener(sig);
|
|
992
|
+
} catch {
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
if (this.responseResolve) {
|
|
998
|
+
const resolve = this.responseResolve;
|
|
999
|
+
this.responseResolve = null;
|
|
1000
|
+
this.responseReject = null;
|
|
1001
|
+
resolve(line);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
onSocketError(err) {
|
|
1005
|
+
this._closed = true;
|
|
1006
|
+
this.rejectPending(err);
|
|
1007
|
+
}
|
|
1008
|
+
onSocketEnd() {
|
|
1009
|
+
this._closed = true;
|
|
1010
|
+
this.rejectPending(new LockError("server closed connection"));
|
|
1011
|
+
}
|
|
1012
|
+
rejectPending(err) {
|
|
1013
|
+
if (this.responseReject) {
|
|
1014
|
+
const reject = this.responseReject;
|
|
1015
|
+
this.responseResolve = null;
|
|
1016
|
+
this.responseReject = null;
|
|
1017
|
+
reject(err);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
sendCmd(cmd, key, arg) {
|
|
1021
|
+
return new Promise((outerResolve, outerReject) => {
|
|
1022
|
+
const execute = () => {
|
|
1023
|
+
if (this._closed) {
|
|
1024
|
+
this.cmdBusy = false;
|
|
1025
|
+
this.dequeueNext();
|
|
1026
|
+
outerReject(new LockError("connection closed"));
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
let settled = false;
|
|
1030
|
+
this.responseResolve = (line) => {
|
|
1031
|
+
if (settled) return;
|
|
1032
|
+
settled = true;
|
|
1033
|
+
this.cmdBusy = false;
|
|
1034
|
+
this.dequeueNext();
|
|
1035
|
+
outerResolve(line);
|
|
1036
|
+
};
|
|
1037
|
+
this.responseReject = (err) => {
|
|
1038
|
+
if (settled) return;
|
|
1039
|
+
settled = true;
|
|
1040
|
+
this.cmdBusy = false;
|
|
1041
|
+
this.dequeueNext();
|
|
1042
|
+
outerReject(err);
|
|
1043
|
+
};
|
|
1044
|
+
this.sock.write(encodeLines(cmd, key, arg), (err) => {
|
|
1045
|
+
if (err && !settled) {
|
|
1046
|
+
settled = true;
|
|
1047
|
+
this.responseResolve = null;
|
|
1048
|
+
this.responseReject = null;
|
|
1049
|
+
this.cmdBusy = false;
|
|
1050
|
+
this.dequeueNext();
|
|
1051
|
+
outerReject(err);
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
};
|
|
1055
|
+
if (this.cmdBusy) {
|
|
1056
|
+
this.cmdQueue.push(execute);
|
|
1057
|
+
} else {
|
|
1058
|
+
this.cmdBusy = true;
|
|
1059
|
+
execute();
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
dequeueNext() {
|
|
1064
|
+
const next = this.cmdQueue.shift();
|
|
1065
|
+
if (next) {
|
|
1066
|
+
this.cmdBusy = true;
|
|
1067
|
+
next();
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
767
1071
|
// Annotate the CommonJS export names for ESM import in node:
|
|
768
1072
|
0 && (module.exports = {
|
|
769
1073
|
AcquireTimeoutError,
|
|
@@ -771,8 +1075,10 @@ var DistributedSemaphore = class extends DistributedPrimitive {
|
|
|
771
1075
|
DistributedLock,
|
|
772
1076
|
DistributedSemaphore,
|
|
773
1077
|
LockError,
|
|
1078
|
+
SignalConnection,
|
|
774
1079
|
acquire,
|
|
775
1080
|
enqueue,
|
|
1081
|
+
publish,
|
|
776
1082
|
release,
|
|
777
1083
|
renew,
|
|
778
1084
|
semAcquire,
|