dflockd-client 1.9.2 → 1.10.1
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 +89 -1
- package/dist/client.cjs +308 -2
- 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 +306 -2
- package/dist/client.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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,
|
|
@@ -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);
|
|
@@ -577,8 +579,8 @@ 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") {
|
|
@@ -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,
|