entangld 2.6.0 → 2.7.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/.github/workflows/mocha-tests.yml +1 -1
- package/API.md +754 -0
- package/README.md +212 -799
- package/docs/protocol.md +484 -0
- package/generate_docs.js +3 -3
- package/lib/Datastore.js +143 -20
- package/lib/Message.js +10 -11
- package/lib/Subscription.js +23 -0
- package/lib/error.js +15 -0
- package/lib/uuid.js +51 -0
- package/package.json +4 -1
- package/test/signals.js +590 -0
- package/test/sockets.js +7 -2
- package/test/tests.js +10 -0
- package/test/timeouts.js +173 -0
- package/test/uuid.js +37 -0
- package/.gitmodules +0 -3
- package/README.hbs +0 -190
- package/tags +0 -244
package/lib/Datastore.js
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
const
|
|
1
|
+
const uuidv4 = require("./uuid.js");
|
|
2
2
|
const EventEmitter = require("events");
|
|
3
3
|
const uuid_regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4
|
+
const MAX_TIMEOUT_MS = 2147483647; // Longer setTimeout delays fire after 1 ms
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check that an optional signal looks like an AbortSignal
|
|
8
|
+
*
|
|
9
|
+
* @private
|
|
10
|
+
* @param {AbortSignal} [signal] the signal to check.
|
|
11
|
+
* @throws {TypeError} if signal is not undefined or an AbortSignal.
|
|
12
|
+
*/
|
|
13
|
+
function check_signal(signal) {
|
|
14
|
+
if (signal === undefined) return;
|
|
15
|
+
if (signal === null || typeof (signal.addEventListener) != "function"
|
|
16
|
+
|| typeof (signal.removeEventListener) != "function")
|
|
17
|
+
throw TypeError("signal must be an AbortSignal");
|
|
18
|
+
}
|
|
4
19
|
|
|
5
20
|
const Entangld_Message = require("./Message.js");
|
|
6
21
|
const Subscription = require("./Subscription.js");
|
|
7
|
-
const { EntangldError, RPCError } = require("./error.js");
|
|
22
|
+
const { EntangldError, EntangldTimeoutError, RPCError } = require("./error.js");
|
|
8
23
|
const {
|
|
9
24
|
is_beneath,
|
|
10
25
|
extract_from_path,
|
|
@@ -114,7 +129,7 @@ class Entangld extends EventEmitter {
|
|
|
114
129
|
|
|
115
130
|
// Re-subscribe
|
|
116
131
|
subscriptions.forEach(s => {
|
|
117
|
-
this._subscribe(s.path, s.callback, s.upstream, s.uuid, s.every);
|
|
132
|
+
this._subscribe(s.path, s.callback, s.upstream, s.uuid, s.every, s.signal);
|
|
118
133
|
});
|
|
119
134
|
}
|
|
120
135
|
|
|
@@ -195,7 +210,7 @@ class Entangld extends EventEmitter {
|
|
|
195
210
|
} else if (msg.type == "value") {
|
|
196
211
|
// Incoming value reply
|
|
197
212
|
let resolve = this._requests[msg.uuid];
|
|
198
|
-
resolve(msg.value);
|
|
213
|
+
if (resolve) resolve(msg.value);
|
|
199
214
|
|
|
200
215
|
//Clean up request
|
|
201
216
|
delete this._requests[msg.uuid];
|
|
@@ -405,11 +420,17 @@ class Entangld extends EventEmitter {
|
|
|
405
420
|
*
|
|
406
421
|
* @param {string} path the path to query (like "system.voltage").
|
|
407
422
|
* @param {Array} [args=[]] the arguments to pass to the RPC.
|
|
423
|
+
* @param {object} [options]
|
|
424
|
+
* @param {number} [options.timeout_ms] reject with an EntangldTimeoutError
|
|
425
|
+
* if no reply arrives within this many milliseconds. This stops the wait,
|
|
426
|
+
* not the RPC, which may still run. Omit to wait indefinitely.
|
|
427
|
+
* @param {AbortSignal} [options.signal] reject with signal.reason when
|
|
428
|
+
* this aborts. This also stops the wait, not the RPC.
|
|
408
429
|
* @throws {RPCError} if the rpc call experienced an error
|
|
409
430
|
* @return {Promise} promise resolving to return value of the rpc
|
|
410
431
|
*/
|
|
411
|
-
async call_rpc(path, args=[]) {
|
|
412
|
-
return this.get(path,args).then((args) => {
|
|
432
|
+
async call_rpc(path, args=[], { timeout_ms, signal }={}) {
|
|
433
|
+
return this.get(path,args,{ timeout_ms, signal }).then((args) => {
|
|
413
434
|
if ( !Array.isArray(args) || args.length != 2) throw new EntangldError(`Endpoint is not configured for RPC: '${path}'`);
|
|
414
435
|
if (args[0] === false) throw RPCError.from_remote_error(args[1]);
|
|
415
436
|
if ( typeof(args[0]) == "string" ) return args[0];
|
|
@@ -471,14 +492,31 @@ class Entangld extends EventEmitter {
|
|
|
471
492
|
* @param {string} path the path to query (like "system.voltage").
|
|
472
493
|
* @param {object} [params] the parameters to be passed to the remote
|
|
473
494
|
* function (RPC) or the maximum depth of the returned object (normal mode).
|
|
474
|
-
*
|
|
475
|
-
* @
|
|
495
|
+
* @param {object} [options]
|
|
496
|
+
* @param {number} [options.timeout_ms] reject with an EntangldTimeoutError
|
|
497
|
+
* if no value arrives within this many milliseconds. Applies to remote
|
|
498
|
+
* paths and to local functions. It is never sent to a remote store. Omit
|
|
499
|
+
* to wait indefinitely.
|
|
500
|
+
* @param {AbortSignal} [options.signal] reject with signal.reason when
|
|
501
|
+
* this aborts. If it has already aborted, nothing is sent and no function
|
|
502
|
+
* is called. This only stops the wait, not any work a remote store or
|
|
503
|
+
* local function has started.
|
|
504
|
+
*
|
|
505
|
+
* @throws {TypeError} if path is not a string, timeout_ms is not a
|
|
506
|
+
* positive number of at most 2147483647, or signal is not an AbortSignal.
|
|
476
507
|
* @return {Promise} promise resolving to the object at that path.
|
|
477
508
|
*/
|
|
478
|
-
get(path, params) {
|
|
509
|
+
get(path, params, { timeout_ms, signal }={}) {
|
|
479
510
|
// Sanity check
|
|
480
511
|
if (typeof (path) != "string")
|
|
481
512
|
throw TypeError("path must be a string");
|
|
513
|
+
if (timeout_ms !== undefined
|
|
514
|
+
&& !(Number.isFinite(timeout_ms) && timeout_ms > 0 && timeout_ms <= MAX_TIMEOUT_MS))
|
|
515
|
+
throw TypeError(`timeout_ms must be a positive number of at most ${MAX_TIMEOUT_MS}`);
|
|
516
|
+
check_signal(signal);
|
|
517
|
+
|
|
518
|
+
if (signal !== undefined && signal.aborted)
|
|
519
|
+
return Promise.reject(signal.reason);
|
|
482
520
|
|
|
483
521
|
// If an attach()ed store path masks (matches but is shorter than) the
|
|
484
522
|
// path, we are returning that store
|
|
@@ -501,7 +539,7 @@ class Entangld extends EventEmitter {
|
|
|
501
539
|
&& result.constructor.name == "Promise") ?
|
|
502
540
|
result : new Promise((res) => res(result));
|
|
503
541
|
|
|
504
|
-
return p.then((val) => {
|
|
542
|
+
return this._with_limits(p, path, { timeout_ms, signal }).then((val) => {
|
|
505
543
|
|
|
506
544
|
// If we have a remaining path, try to navigate it before
|
|
507
545
|
// returning
|
|
@@ -541,11 +579,65 @@ class Entangld extends EventEmitter {
|
|
|
541
579
|
// Request the data from the remote store
|
|
542
580
|
const msg = Entangld_Message.get(tree, params);
|
|
543
581
|
|
|
544
|
-
|
|
582
|
+
const reply = new Promise((res) => {
|
|
545
583
|
this._requests[msg.uuid] = res;
|
|
546
584
|
this._transmit(msg, obj);
|
|
547
585
|
});
|
|
548
586
|
|
|
587
|
+
return this._with_limits(reply, path, { timeout_ms, signal }, () => {
|
|
588
|
+
delete this._requests[msg.uuid];
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Reject if a promise does not settle in time, or a signal aborts first
|
|
594
|
+
*
|
|
595
|
+
* @private
|
|
596
|
+
* @param {Promise} promise the promise to wait on.
|
|
597
|
+
* @param {string} path the path being read, for the timeout error.
|
|
598
|
+
* @param {object} limits
|
|
599
|
+
* @param {number} [limits.timeout_ms] the time limit.
|
|
600
|
+
* @param {AbortSignal} [limits.signal] the signal to watch.
|
|
601
|
+
* @param {function} [on_limit] cleanup to run if a limit is reached.
|
|
602
|
+
*
|
|
603
|
+
* @return {Promise} the original promise if there are no limits, otherwise
|
|
604
|
+
* a promise which settles as the original does, or rejects when a limit
|
|
605
|
+
* is reached.
|
|
606
|
+
*/
|
|
607
|
+
_with_limits(promise, path, { timeout_ms, signal }, on_limit = () => { }) {
|
|
608
|
+
|
|
609
|
+
if (timeout_ms === undefined && signal === undefined) return promise;
|
|
610
|
+
|
|
611
|
+
let timer;
|
|
612
|
+
let on_abort;
|
|
613
|
+
|
|
614
|
+
const stop = () => {
|
|
615
|
+
clearTimeout(timer);
|
|
616
|
+
if (signal !== undefined) signal.removeEventListener("abort", on_abort);
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const limit = new Promise((_, reject) => {
|
|
620
|
+
const give_up = (err) => {
|
|
621
|
+
stop();
|
|
622
|
+
on_limit();
|
|
623
|
+
reject(err);
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
if (timeout_ms !== undefined) {
|
|
627
|
+
timer = setTimeout(() => {
|
|
628
|
+
give_up(new EntangldTimeoutError(path, timeout_ms));
|
|
629
|
+
}, timeout_ms);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (signal !== undefined) {
|
|
633
|
+
on_abort = () => give_up(signal.reason);
|
|
634
|
+
signal.addEventListener("abort", on_abort);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
promise.then(stop, stop);
|
|
639
|
+
|
|
640
|
+
return Promise.race([promise, limit]);
|
|
549
641
|
}
|
|
550
642
|
|
|
551
643
|
/**
|
|
@@ -582,14 +674,24 @@ class Entangld extends EventEmitter {
|
|
|
582
674
|
*
|
|
583
675
|
* @param {string} path the path to watch. Use of '*' is allowed as a wildcard (e.g. "system.*")
|
|
584
676
|
* @param {function} func the callback - will be of the form (path, value).
|
|
585
|
-
* @param {number|null} [
|
|
586
|
-
*
|
|
587
|
-
* @
|
|
677
|
+
* @param {number|null|object} [options=null] an options object, or the value of options.every.
|
|
678
|
+
* @param {number|null} [options.every=null] the number of `set` messages to wait before calling callback
|
|
679
|
+
* @param {AbortSignal} [options.signal] unsubscribe when this aborts. If it
|
|
680
|
+
* has already aborted, no subscription is created. The uuid is still
|
|
681
|
+
* returned, as it would be for a subscription which has since ended.
|
|
682
|
+
* Calling unsubscribe() after an abort throws, see unsubscribe().
|
|
683
|
+
*
|
|
684
|
+
* @throws {TypeError} if path is not a string, or signal is not an AbortSignal.
|
|
588
685
|
* @return {uuidv4} - the uuid of the subscription
|
|
589
686
|
*/
|
|
590
|
-
subscribe(path, func,
|
|
687
|
+
subscribe(path, func, options = null) {
|
|
688
|
+
|
|
689
|
+
const is_object = typeof (options) == "object" && options !== null;
|
|
690
|
+
const { every = null, signal } = is_object ? options : { every: options };
|
|
591
691
|
|
|
592
|
-
|
|
692
|
+
check_signal(signal);
|
|
693
|
+
|
|
694
|
+
return this._subscribe(path, func, null, null, every, signal);
|
|
593
695
|
}
|
|
594
696
|
|
|
595
697
|
/**
|
|
@@ -603,6 +705,8 @@ class Entangld extends EventEmitter {
|
|
|
603
705
|
* @param {function} func the callback - will be of the form (path, value).
|
|
604
706
|
* @param {(Entangld|null)} [upstream=null] the Engangld next upstream in the path.
|
|
605
707
|
* @param {(uuidv4|null)} [uuid=null] the UUID to use for this subscription.
|
|
708
|
+
* @param {number|null} [every=null] the number of `set` messages to wait before calling callback
|
|
709
|
+
* @param {AbortSignal} [signal] unsubscribe when this aborts.
|
|
606
710
|
*
|
|
607
711
|
* @emits {str} subscription - when this datastore is the terminal datastore of a
|
|
608
712
|
* subscription request, this datastore emits the path
|
|
@@ -610,7 +714,7 @@ class Entangld extends EventEmitter {
|
|
|
610
714
|
* @throws {TypeError} if path is not a string.
|
|
611
715
|
* @return {uuidv4} - the uuid of the subscription
|
|
612
716
|
*/
|
|
613
|
-
_subscribe(path, func, upstream = null, uuid = null, every = null) {
|
|
717
|
+
_subscribe(path, func, upstream = null, uuid = null, every = null, signal = undefined) {
|
|
614
718
|
|
|
615
719
|
|
|
616
720
|
// Supply a UUID if none provided
|
|
@@ -621,6 +725,8 @@ class Entangld extends EventEmitter {
|
|
|
621
725
|
if (typeof (path) != "string")
|
|
622
726
|
throw TypeError("path must be a string");
|
|
623
727
|
|
|
728
|
+
if (signal !== undefined && signal.aborted) return uuid;
|
|
729
|
+
|
|
624
730
|
let [obj, /*namespace*/, tree] = this._get_remote_object(path);
|
|
625
731
|
|
|
626
732
|
// Create the subscription object
|
|
@@ -644,6 +750,15 @@ class Entangld extends EventEmitter {
|
|
|
644
750
|
// Add the new subscription here
|
|
645
751
|
this._subscriptions.push(new_sub);
|
|
646
752
|
|
|
753
|
+
if (signal !== undefined) {
|
|
754
|
+
new_sub.watch_signal(signal, () => {
|
|
755
|
+
// Unlike unsubscribe(), finding nothing here isn't an error
|
|
756
|
+
this._unsubscribe(this._subscriptions.filter(
|
|
757
|
+
s => s.matches_uuid(uuid) && !s.is_pass_through
|
|
758
|
+
));
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
647
762
|
// Commission any subscribes downstream, if this is a remote subscription
|
|
648
763
|
if (new_sub.has_downstream) {
|
|
649
764
|
const msg = Entangld_Message.subscribe(tree, uuid, every);
|
|
@@ -682,6 +797,11 @@ class Entangld extends EventEmitter {
|
|
|
682
797
|
* path will be deleted, so if you have multiple subscriptions on a single path,
|
|
683
798
|
* and only want one of them to be removed, you must provide the uuid instead.
|
|
684
799
|
*
|
|
800
|
+
* Caution - this throws if the subscription has already ended. That includes
|
|
801
|
+
* a subscription ended by its signal, or a signal which had already aborted
|
|
802
|
+
* when subscribe() was called. If you mix signals with explicit unsubscribes,
|
|
803
|
+
* check `signal.aborted` first or catch the error.
|
|
804
|
+
*
|
|
685
805
|
* @param {(String|uuidv4)} path_or_uuid - the path (or uuid) to unwatch.
|
|
686
806
|
* @throws {EntangldError} if no subscriptions were found.
|
|
687
807
|
* @return {number} count of subscriptions removed.
|
|
@@ -766,9 +886,12 @@ class Entangld extends EventEmitter {
|
|
|
766
886
|
const uuids = subscriptions.map(s => s.uuid);
|
|
767
887
|
|
|
768
888
|
// Drop all subscriptions with matching uuid
|
|
769
|
-
this._subscriptions = this._subscriptions.filter(
|
|
770
|
-
|
|
771
|
-
|
|
889
|
+
this._subscriptions = this._subscriptions.filter((s) => {
|
|
890
|
+
if (!uuids.includes(s.uuid)) return true;
|
|
891
|
+
|
|
892
|
+
s.unwatch_signal();
|
|
893
|
+
return false;
|
|
894
|
+
});
|
|
772
895
|
|
|
773
896
|
// Notify the downstream for any deleted subscriptions that are remote
|
|
774
897
|
subscriptions.forEach((s) => {
|
package/lib/Message.js
CHANGED
|
@@ -1,31 +1,30 @@
|
|
|
1
|
-
const
|
|
1
|
+
const uuidv4 = require("./uuid.js");
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Message class for Entangld.
|
|
5
5
|
*
|
|
6
6
|
* These messages are used for executing datastore operations between
|
|
7
7
|
* remote datastores. In these relationships, there is always an upstream
|
|
8
|
-
* and downstream pair, so that `get`, `set`, `push` and `
|
|
9
|
-
* are created upstream and passed downstream while `event` and `value`
|
|
10
|
-
* are created downstream and are passed back up.
|
|
11
|
-
*
|
|
8
|
+
* and downstream pair, so that `get`, `set`, `push`, `subscribe` and `unsubscribe`
|
|
9
|
+
* messages are created upstream and passed downstream while `event` and `value`
|
|
10
|
+
* messages are created downstream and are passed back up. To maintain consistency,
|
|
11
|
+
* the internal `.path` attribute
|
|
12
12
|
* will always refer a path relative to the downstream datastore, since the
|
|
13
13
|
* downstream datastores do not necessarilly have access to the upstream store
|
|
14
14
|
* structure, and so cannot generally construct upstream paths. This means that
|
|
15
15
|
* the `.path` attribute is a `tree` relative to the upstream datastore, and
|
|
16
16
|
* the upstream path can be reconstructed as:
|
|
17
|
-
* ```
|
|
18
|
-
*
|
|
17
|
+
* ```js
|
|
18
|
+
* upstream._namespaces.get(downstream) + "." + msg.path;
|
|
19
19
|
* ```
|
|
20
|
-
* Since `unsubscribe` messages
|
|
21
|
-
*
|
|
22
|
-
* attributes set to undefined or null.
|
|
20
|
+
* Since `unsubscribe` messages identify a subscription by its `.uuid`, they should
|
|
21
|
+
* have their `.path` attributes set to undefined or null.
|
|
23
22
|
*
|
|
24
23
|
* Most messages will also have a `.uuid` attribute. For `get`/`value` messages,
|
|
25
24
|
* this allows for the value to be properly linked back up with the original `get`
|
|
26
25
|
* message. For the `subscribe`/`unsubscribe`/`event` messages, this allows for
|
|
27
26
|
* callback functions to be trigger properly, and for unsubscribe messages to
|
|
28
|
-
* propogate
|
|
27
|
+
* propogate down the chain. `set`/`push` messages do not use the `.uuid` attribute
|
|
29
28
|
* since they require no response.
|
|
30
29
|
*/
|
|
31
30
|
class Entangld_Message {
|
package/lib/Subscription.js
CHANGED
|
@@ -38,6 +38,29 @@ class Subscription {
|
|
|
38
38
|
this.every = every;
|
|
39
39
|
}
|
|
40
40
|
this.index = -1; // guarentee that the first `set` triggers
|
|
41
|
+
this.signal = undefined;
|
|
42
|
+
this._on_abort = undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Run a callback when a signal aborts
|
|
47
|
+
*
|
|
48
|
+
* @param {AbortSignal} signal - the signal to watch
|
|
49
|
+
* @param {function} on_abort - the callback
|
|
50
|
+
*/
|
|
51
|
+
watch_signal(signal, on_abort) {
|
|
52
|
+
this.signal = signal;
|
|
53
|
+
this._on_abort = on_abort;
|
|
54
|
+
signal.addEventListener("abort", on_abort);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Stop watching this subscription's signal, if it has one
|
|
59
|
+
*/
|
|
60
|
+
unwatch_signal() {
|
|
61
|
+
if (this.signal === undefined) return;
|
|
62
|
+
|
|
63
|
+
this.signal.removeEventListener("abort", this._on_abort);
|
|
41
64
|
}
|
|
42
65
|
|
|
43
66
|
/**
|
package/lib/error.js
CHANGED
|
@@ -12,6 +12,20 @@ class EntangldError extends Error {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Error for a request which got no reply in time.
|
|
17
|
+
*
|
|
18
|
+
* @extends EntangldError
|
|
19
|
+
*/
|
|
20
|
+
class EntangldTimeoutError extends EntangldError {
|
|
21
|
+
constructor(path, timeout_ms) {
|
|
22
|
+
super(`No reply for '${path}' within ${timeout_ms} ms`);
|
|
23
|
+
|
|
24
|
+
this.path = path;
|
|
25
|
+
this.timeout_ms = timeout_ms;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
/**
|
|
16
30
|
* Remote Error class for Entangld.
|
|
17
31
|
*/
|
|
@@ -39,5 +53,6 @@ class RPCError extends Error {
|
|
|
39
53
|
|
|
40
54
|
module.exports = exports = {
|
|
41
55
|
EntangldError,
|
|
56
|
+
EntangldTimeoutError,
|
|
42
57
|
RPCError,
|
|
43
58
|
};
|
package/lib/uuid.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A random (v4) uuid, in Node and in browsers.
|
|
3
|
+
*
|
|
4
|
+
* Browsers on secure pages and Node 19 and later have crypto.randomUUID() on
|
|
5
|
+
* globalThis. Node 18 has it in its crypto module, which the "browser" field in
|
|
6
|
+
* package.json leaves out of browser bundles. A browser page that isn't secure
|
|
7
|
+
* (plain http, not localhost) has only crypto.getRandomValues().
|
|
8
|
+
*
|
|
9
|
+
* @author Claude <noreply@anthropic.com>
|
|
10
|
+
* Reviewed-by: Jonathan D. B. Van Schenck <jvschenck@novadynamics.com>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A v4 uuid from 16 random bytes.
|
|
15
|
+
*
|
|
16
|
+
* @param {Uint8Array} bytes
|
|
17
|
+
* @return {string}
|
|
18
|
+
*/
|
|
19
|
+
function format_v4(bytes) {
|
|
20
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
|
|
21
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // RFC 4122 variant
|
|
22
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
23
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Pick the uuid source for this runtime.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} [web_crypto] - globalThis.crypto, if any.
|
|
30
|
+
* @param {Function} [load_node_crypto] - returns Node's crypto module, or {} in a browser bundle.
|
|
31
|
+
* @return {Function} returns a new uuid string.
|
|
32
|
+
*/
|
|
33
|
+
// randomUUID needs Node 14.17 anyway, which has globalThis
|
|
34
|
+
function make_uuid(web_crypto = globalThis.crypto, load_node_crypto = () => require("crypto")) { // eslint-disable-line node/no-unsupported-features/es-builtins
|
|
35
|
+
if ( typeof web_crypto?.randomUUID == "function" ) return () => web_crypto.randomUUID();
|
|
36
|
+
let node_crypto = {};
|
|
37
|
+
try {
|
|
38
|
+
node_crypto = load_node_crypto() || {};
|
|
39
|
+
} catch (err) {
|
|
40
|
+
// Not in Node
|
|
41
|
+
}
|
|
42
|
+
if ( typeof node_crypto.randomUUID == "function" ) return () => node_crypto.randomUUID();
|
|
43
|
+
if ( typeof web_crypto?.getRandomValues == "function" ) {
|
|
44
|
+
return () => format_v4(web_crypto.getRandomValues(new Uint8Array(16)));
|
|
45
|
+
}
|
|
46
|
+
throw new Error("entangld needs crypto.randomUUID or crypto.getRandomValues");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = make_uuid();
|
|
50
|
+
module.exports.make_uuid = make_uuid;
|
|
51
|
+
module.exports.format_v4 = format_v4;
|
package/package.json
CHANGED