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/dist/client.js CHANGED
@@ -211,14 +211,14 @@ function stableHashShard(key, numServers) {
211
211
  }
212
212
  async function protoAcquire(sock, cmd, label, key, acquireTimeoutS, leaseTtlS, limit) {
213
213
  validateKey(key);
214
- if (!Number.isFinite(acquireTimeoutS) || acquireTimeoutS < 0) {
215
- throw new LockError("acquireTimeoutS must be a finite number >= 0");
214
+ if (!Number.isInteger(acquireTimeoutS) || acquireTimeoutS < 0) {
215
+ throw new LockError("acquireTimeoutS must be an integer >= 0");
216
216
  }
217
217
  if (limit != null && (!Number.isInteger(limit) || limit < 1)) {
218
218
  throw new LockError("limit must be an integer >= 1");
219
219
  }
220
- if (leaseTtlS != null && (!Number.isFinite(leaseTtlS) || leaseTtlS <= 0)) {
221
- throw new LockError("leaseTtlS must be a finite number > 0");
220
+ if (leaseTtlS != null && (!Number.isInteger(leaseTtlS) || leaseTtlS < 1)) {
221
+ throw new LockError("leaseTtlS must be an integer >= 1");
222
222
  }
223
223
  const parts = [acquireTimeoutS];
224
224
  if (limit != null) parts.push(limit);
@@ -241,8 +241,8 @@ async function protoAcquire(sock, cmd, label, key, acquireTimeoutS, leaseTtlS, l
241
241
  async function protoRenew(sock, cmd, label, key, token, leaseTtlS) {
242
242
  validateKey(key);
243
243
  validateToken(token);
244
- if (leaseTtlS != null && (!Number.isFinite(leaseTtlS) || leaseTtlS <= 0)) {
245
- throw new LockError("leaseTtlS must be a finite number > 0");
244
+ if (leaseTtlS != null && (!Number.isInteger(leaseTtlS) || leaseTtlS < 1)) {
245
+ throw new LockError("leaseTtlS must be an integer >= 1");
246
246
  }
247
247
  const arg = leaseTtlS == null ? token : `${token} ${leaseTtlS}`;
248
248
  await writeAll(sock, encodeLines(cmd, key, arg));
@@ -258,8 +258,8 @@ async function protoEnqueue(sock, cmd, label, key, leaseTtlS, limit) {
258
258
  if (limit != null && (!Number.isInteger(limit) || limit < 1)) {
259
259
  throw new LockError("limit must be an integer >= 1");
260
260
  }
261
- if (leaseTtlS != null && (!Number.isFinite(leaseTtlS) || leaseTtlS <= 0)) {
262
- throw new LockError("leaseTtlS must be a finite number > 0");
261
+ if (leaseTtlS != null && (!Number.isInteger(leaseTtlS) || leaseTtlS < 1)) {
262
+ throw new LockError("leaseTtlS must be an integer >= 1");
263
263
  }
264
264
  const parts = [];
265
265
  if (limit != null) parts.push(limit);
@@ -281,8 +281,8 @@ async function protoEnqueue(sock, cmd, label, key, leaseTtlS, limit) {
281
281
  }
282
282
  async function protoWait(sock, cmd, label, key, waitTimeoutS) {
283
283
  validateKey(key);
284
- if (!Number.isFinite(waitTimeoutS) || waitTimeoutS < 0) {
285
- throw new LockError("waitTimeoutS must be a finite number >= 0");
284
+ if (!Number.isInteger(waitTimeoutS) || waitTimeoutS < 0) {
285
+ throw new LockError("waitTimeoutS must be an integer >= 0");
286
286
  }
287
287
  await writeAll(sock, encodeLines(cmd, key, String(waitTimeoutS)));
288
288
  const resp = await readline(sock);
@@ -389,11 +389,11 @@ var DistributedPrimitive = class {
389
389
  this.onLockLost = opts.onLockLost;
390
390
  this.connectTimeoutMs = opts.connectTimeoutMs;
391
391
  this.socketTimeoutMs = opts.socketTimeoutMs;
392
- if (!Number.isFinite(this.acquireTimeoutS) || this.acquireTimeoutS < 0) {
393
- throw new LockError("acquireTimeoutS must be a finite number >= 0");
392
+ if (!Number.isInteger(this.acquireTimeoutS) || this.acquireTimeoutS < 0) {
393
+ throw new LockError("acquireTimeoutS must be an integer >= 0");
394
394
  }
395
- if (this.leaseTtlS != null && (!Number.isFinite(this.leaseTtlS) || this.leaseTtlS <= 0)) {
396
- throw new LockError("leaseTtlS must be a finite number > 0");
395
+ if (this.leaseTtlS != null && (!Number.isInteger(this.leaseTtlS) || this.leaseTtlS < 1)) {
396
+ throw new LockError("leaseTtlS must be an integer >= 1");
397
397
  }
398
398
  if (opts.servers) {
399
399
  if (opts.servers.length === 0) {
@@ -460,8 +460,8 @@ var DistributedPrimitive = class {
460
460
  this.close();
461
461
  }
462
462
  this.closed = false;
463
- this.sock = await this.openConnection();
464
463
  try {
464
+ this.sock = await this.openConnection();
465
465
  this.suspendSocketTimeout(this.sock);
466
466
  const result = await this.doAcquire(this.sock);
467
467
  this.restoreSocketTimeout(this.sock);
@@ -496,7 +496,7 @@ var DistributedPrimitive = class {
496
496
  if (this.renewInFlight) {
497
497
  await Promise.race([
498
498
  this.renewInFlight,
499
- new Promise((r) => setTimeout(r, 5e3))
499
+ new Promise((r) => setTimeout(r, 5e3).unref())
500
500
  ]);
501
501
  this.stopRenew();
502
502
  }
@@ -527,16 +527,16 @@ var DistributedPrimitive = class {
527
527
  this.close();
528
528
  }
529
529
  this.closed = false;
530
- this.sock = await this.openConnection();
531
530
  try {
531
+ this.sock = await this.openConnection();
532
532
  this.suspendSocketTimeout(this.sock);
533
533
  const result = await this.doEnqueue(this.sock);
534
534
  if (result.status === "acquired") {
535
535
  this.token = result.token;
536
536
  this.lease = result.lease ?? 0;
537
+ this.restoreSocketTimeout(this.sock);
537
538
  this.startRenew();
538
539
  }
539
- this.restoreSocketTimeout(this.sock);
540
540
  return result.status;
541
541
  } catch (err) {
542
542
  this.close();
@@ -714,14 +714,318 @@ var DistributedSemaphore = class extends DistributedPrimitive {
714
714
  return semRenew(sock, this.key, token, this.leaseTtlS);
715
715
  }
716
716
  };
717
+ function validatePayload(payload) {
718
+ if (payload === "") {
719
+ throw new LockError("payload must not be empty");
720
+ }
721
+ if (/[\0\n\r]/.test(payload)) {
722
+ throw new LockError(
723
+ "payload must not contain NUL, newline, or carriage return characters"
724
+ );
725
+ }
726
+ }
727
+ async function publish(sock, channel, payload) {
728
+ validateKey(channel);
729
+ validatePayload(payload);
730
+ await writeAll(sock, encodeLines("signal", channel, payload));
731
+ const resp = await readline(sock);
732
+ if (!resp.startsWith("ok ")) {
733
+ throw new LockError(`signal failed: '${resp}'`);
734
+ }
735
+ const n = parseInt(resp.split(" ")[1], 10);
736
+ if (!Number.isFinite(n)) {
737
+ throw new LockError(`signal: bad delivery count: '${resp}'`);
738
+ }
739
+ return n;
740
+ }
741
+ var SignalConnection = class _SignalConnection {
742
+ sock;
743
+ buf = "";
744
+ responseResolve = null;
745
+ responseReject = null;
746
+ cmdQueue = [];
747
+ cmdBusy = false;
748
+ signalListeners = [];
749
+ _closed = false;
750
+ /**
751
+ * Wrap an existing socket as a SignalConnection.
752
+ *
753
+ * The socket should already be connected (and authenticated, if needed).
754
+ * Prefer `SignalConnection.connect()` for convenience.
755
+ */
756
+ constructor(sock) {
757
+ this.sock = sock;
758
+ const leftover = _readlineBuf.get(sock);
759
+ if (leftover) {
760
+ this.buf = leftover;
761
+ _readlineBuf.delete(sock);
762
+ }
763
+ sock.on("data", (chunk) => this.onData(chunk));
764
+ sock.on("error", (err) => this.onSocketError(err));
765
+ sock.on("close", () => this.onSocketEnd());
766
+ sock.on("end", () => this.onSocketEnd());
767
+ this.drainLines();
768
+ }
769
+ /** Connect to a dflockd server and return a SignalConnection. */
770
+ static async connect(opts) {
771
+ const host = opts?.host ?? DEFAULT_HOST;
772
+ const port = opts?.port ?? DEFAULT_PORT;
773
+ const sock = await connect2(
774
+ host,
775
+ port,
776
+ opts?.tls,
777
+ opts?.auth,
778
+ opts?.connectTimeoutMs
779
+ );
780
+ return new _SignalConnection(sock);
781
+ }
782
+ /**
783
+ * Subscribe to signals matching `pattern`.
784
+ *
785
+ * Patterns support NATS-style wildcards:
786
+ * - `*` matches exactly one dot-separated token
787
+ * - `>` matches one or more trailing tokens
788
+ *
789
+ * If `group` is provided, the subscription joins a queue group where
790
+ * signals are load-balanced (round-robin) among group members.
791
+ */
792
+ async listen(pattern, group) {
793
+ validateKey(pattern);
794
+ if (group != null && /[\0\n\r]/.test(group)) {
795
+ throw new LockError(
796
+ "group must not contain NUL, newline, or carriage return characters"
797
+ );
798
+ }
799
+ const resp = await this.sendCmd("listen", pattern, group ?? "");
800
+ if (resp !== "ok") {
801
+ throw new LockError(`listen failed: '${resp}'`);
802
+ }
803
+ }
804
+ /**
805
+ * Unsubscribe from signals matching `pattern`.
806
+ * The `pattern` and `group` must match a previous `listen()` call.
807
+ */
808
+ async unlisten(pattern, group) {
809
+ validateKey(pattern);
810
+ if (group != null && /[\0\n\r]/.test(group)) {
811
+ throw new LockError(
812
+ "group must not contain NUL, newline, or carriage return characters"
813
+ );
814
+ }
815
+ const resp = await this.sendCmd("unlisten", pattern, group ?? "");
816
+ if (resp !== "ok") {
817
+ throw new LockError(`unlisten failed: '${resp}'`);
818
+ }
819
+ }
820
+ /**
821
+ * Publish a signal on a literal channel (no wildcards).
822
+ * Returns the number of listeners that received the signal.
823
+ */
824
+ async emit(channel, payload) {
825
+ validateKey(channel);
826
+ validatePayload(payload);
827
+ const resp = await this.sendCmd("signal", channel, payload);
828
+ if (!resp.startsWith("ok ")) {
829
+ throw new LockError(`signal failed: '${resp}'`);
830
+ }
831
+ const n = parseInt(resp.split(" ")[1], 10);
832
+ if (!Number.isFinite(n)) {
833
+ throw new LockError(`signal: bad delivery count: '${resp}'`);
834
+ }
835
+ return n;
836
+ }
837
+ /** Register a listener for incoming signals. */
838
+ onSignal(listener) {
839
+ this.signalListeners.push(listener);
840
+ }
841
+ /** Remove a previously registered signal listener. */
842
+ offSignal(listener) {
843
+ const idx = this.signalListeners.indexOf(listener);
844
+ if (idx >= 0) this.signalListeners.splice(idx, 1);
845
+ }
846
+ /**
847
+ * Async iterator that yields signals as they arrive.
848
+ * Terminates when the connection closes.
849
+ *
850
+ * ```ts
851
+ * for await (const sig of conn) {
852
+ * console.log(sig.channel, sig.payload);
853
+ * }
854
+ * ```
855
+ */
856
+ async *[Symbol.asyncIterator]() {
857
+ const buffer = [];
858
+ let waiter = null;
859
+ const listener = (sig) => {
860
+ buffer.push(sig);
861
+ if (waiter) {
862
+ const w = waiter;
863
+ waiter = null;
864
+ w();
865
+ }
866
+ };
867
+ const onEnd = () => {
868
+ if (waiter) {
869
+ const w = waiter;
870
+ waiter = null;
871
+ w();
872
+ }
873
+ };
874
+ this.onSignal(listener);
875
+ this.sock.once("close", onEnd);
876
+ try {
877
+ while (true) {
878
+ if (buffer.length > 0) {
879
+ yield buffer.shift();
880
+ continue;
881
+ }
882
+ if (this._closed) break;
883
+ await new Promise((resolve) => {
884
+ waiter = resolve;
885
+ });
886
+ }
887
+ while (buffer.length > 0) {
888
+ yield buffer.shift();
889
+ }
890
+ } finally {
891
+ this.offSignal(listener);
892
+ this.sock.removeListener("close", onEnd);
893
+ }
894
+ }
895
+ /** Close the connection (idempotent). */
896
+ close() {
897
+ if (this._closed) return;
898
+ this._closed = true;
899
+ this.sock.destroy();
900
+ this.rejectPending(new LockError("connection closed"));
901
+ }
902
+ /** Whether the connection is closed. */
903
+ get isClosed() {
904
+ return this._closed;
905
+ }
906
+ // ---- internals ----
907
+ onData(chunk) {
908
+ this.buf += chunk.toString("utf-8");
909
+ if (this.buf.length > MAX_LINE_LENGTH * 2) {
910
+ this.buf = "";
911
+ this._closed = true;
912
+ this.sock.destroy();
913
+ this.rejectPending(
914
+ new LockError("server response exceeded maximum buffer size")
915
+ );
916
+ return;
917
+ }
918
+ this.drainLines();
919
+ }
920
+ drainLines() {
921
+ let idx;
922
+ while ((idx = this.buf.indexOf("\n")) !== -1) {
923
+ const line = this.buf.slice(0, idx).replace(/\r$/, "");
924
+ this.buf = this.buf.slice(idx + 1);
925
+ this.handleLine(line);
926
+ }
927
+ }
928
+ handleLine(line) {
929
+ if (line.startsWith("sig ")) {
930
+ const rest = line.slice(4);
931
+ const spaceIdx = rest.indexOf(" ");
932
+ if (spaceIdx < 0) return;
933
+ const sig = {
934
+ channel: rest.slice(0, spaceIdx),
935
+ payload: rest.slice(spaceIdx + 1)
936
+ };
937
+ for (const listener of [...this.signalListeners]) {
938
+ try {
939
+ listener(sig);
940
+ } catch {
941
+ }
942
+ }
943
+ return;
944
+ }
945
+ if (this.responseResolve) {
946
+ const resolve = this.responseResolve;
947
+ this.responseResolve = null;
948
+ this.responseReject = null;
949
+ resolve(line);
950
+ }
951
+ }
952
+ onSocketError(err) {
953
+ this._closed = true;
954
+ this.rejectPending(err);
955
+ }
956
+ onSocketEnd() {
957
+ this._closed = true;
958
+ this.rejectPending(new LockError("server closed connection"));
959
+ }
960
+ rejectPending(err) {
961
+ if (this.responseReject) {
962
+ const reject = this.responseReject;
963
+ this.responseResolve = null;
964
+ this.responseReject = null;
965
+ reject(err);
966
+ }
967
+ }
968
+ sendCmd(cmd, key, arg) {
969
+ return new Promise((outerResolve, outerReject) => {
970
+ const execute = () => {
971
+ if (this._closed) {
972
+ this.cmdBusy = false;
973
+ this.dequeueNext();
974
+ outerReject(new LockError("connection closed"));
975
+ return;
976
+ }
977
+ let settled = false;
978
+ this.responseResolve = (line) => {
979
+ if (settled) return;
980
+ settled = true;
981
+ this.cmdBusy = false;
982
+ this.dequeueNext();
983
+ outerResolve(line);
984
+ };
985
+ this.responseReject = (err) => {
986
+ if (settled) return;
987
+ settled = true;
988
+ this.cmdBusy = false;
989
+ this.dequeueNext();
990
+ outerReject(err);
991
+ };
992
+ this.sock.write(encodeLines(cmd, key, arg), (err) => {
993
+ if (err && !settled) {
994
+ settled = true;
995
+ this.responseResolve = null;
996
+ this.responseReject = null;
997
+ this.cmdBusy = false;
998
+ this.dequeueNext();
999
+ outerReject(err);
1000
+ }
1001
+ });
1002
+ };
1003
+ if (this.cmdBusy) {
1004
+ this.cmdQueue.push(execute);
1005
+ } else {
1006
+ this.cmdBusy = true;
1007
+ execute();
1008
+ }
1009
+ });
1010
+ }
1011
+ dequeueNext() {
1012
+ const next = this.cmdQueue.shift();
1013
+ if (next) {
1014
+ this.cmdBusy = true;
1015
+ next();
1016
+ }
1017
+ }
1018
+ };
717
1019
  export {
718
1020
  AcquireTimeoutError,
719
1021
  AuthError,
720
1022
  DistributedLock,
721
1023
  DistributedSemaphore,
722
1024
  LockError,
1025
+ SignalConnection,
723
1026
  acquire,
724
1027
  enqueue,
1028
+ publish,
725
1029
  release,
726
1030
  renew,
727
1031
  semAcquire,