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/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.
|
|
215
|
-
throw new LockError("acquireTimeoutS must be
|
|
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.
|
|
221
|
-
throw new LockError("leaseTtlS must be
|
|
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.
|
|
245
|
-
throw new LockError("leaseTtlS must be
|
|
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.
|
|
262
|
-
throw new LockError("leaseTtlS must be
|
|
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.
|
|
285
|
-
throw new LockError("waitTimeoutS must be
|
|
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.
|
|
393
|
-
throw new LockError("acquireTimeoutS must be
|
|
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.
|
|
396
|
-
throw new LockError("leaseTtlS must be
|
|
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,
|