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.
@@ -0,0 +1,173 @@
1
+ const assert = require("assert");
2
+ const Entangld = require("../lib/Datastore.js");
3
+ const { EntangldError, EntangldTimeoutError } = require("../lib/error.js");
4
+
5
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
6
+
7
+ // Resolve to the error a promise rejects with, or fail if it resolves
8
+ function rejection(promise) {
9
+ return promise.then(
10
+ (val) => assert.fail(`Expected a rejection, got ${val}`),
11
+ (err) => err
12
+ );
13
+ }
14
+
15
+ // Attach store b to store a. Messages from a are delivered only if allow(msg)
16
+ function linked_stores(allow = () => true) {
17
+ const a = new Entangld();
18
+ const b = new Entangld();
19
+
20
+ a.transmit((msg, store) => {
21
+ if (allow(msg)) store.receive(msg, a);
22
+ });
23
+ b.transmit((msg, store) => store.receive(msg, b));
24
+
25
+ a.attach("b", b);
26
+ b.attach("a", a);
27
+
28
+ return { a, b };
29
+ }
30
+
31
+ describe("Timeouts", () => {
32
+
33
+ describe("get", () => {
34
+
35
+ it("rejects if a remote store never replies", async () => {
36
+ const { a } = linked_stores(() => false);
37
+
38
+ const err = await rejection(a.get("b.value", undefined, { timeout_ms: 10 }));
39
+
40
+ assert(err instanceof EntangldTimeoutError);
41
+ assert(err instanceof EntangldError);
42
+ assert.strictEqual(err.name, "EntangldTimeoutError");
43
+ assert.strictEqual(err.path, "b.value");
44
+ assert.strictEqual(err.timeout_ms, 10);
45
+ });
46
+
47
+ it("removes the request after timing out", async () => {
48
+ const { a } = linked_stores(() => false);
49
+
50
+ await rejection(a.get("b.value", undefined, { timeout_ms: 10 }));
51
+
52
+ assert.strictEqual(Object.keys(a._requests).length, 0);
53
+ });
54
+
55
+ it("resolves if the reply beats the timeout", async () => {
56
+ const { a, b } = linked_stores();
57
+ b.set("value", 7);
58
+
59
+ const value = await a.get("b.value", undefined, { timeout_ms: 1000 });
60
+
61
+ assert.strictEqual(value, 7);
62
+ assert.strictEqual(Object.keys(a._requests).length, 0);
63
+ });
64
+
65
+ it("ignores a reply which arrives after the timeout", async () => {
66
+ const { a, b } = linked_stores(() => false);
67
+ const sent = [];
68
+ a.transmit((msg) => sent.push(msg));
69
+
70
+ await rejection(a.get("b.value", undefined, { timeout_ms: 10 }));
71
+
72
+ assert.doesNotThrow(() => {
73
+ a.receive({ type: "value", uuid: sent[0].uuid, value: 1 }, b);
74
+ });
75
+ });
76
+
77
+ it("passes params through and keeps options off the wire", async () => {
78
+ const sent = [];
79
+ const { a, b } = linked_stores((msg) => {
80
+ sent.push(JSON.stringify(msg));
81
+ return true;
82
+ });
83
+ b.set("echo", (params) => params);
84
+
85
+ const value = await a.get("b.echo", ["x"], { timeout_ms: 1000 });
86
+
87
+ assert.deepStrictEqual(value, ["x"]);
88
+ assert(!sent.join("").includes("timeout_ms"));
89
+ });
90
+
91
+ it("rejects if a local function never settles", async () => {
92
+ const a = new Entangld();
93
+ a.set("hang", () => new Promise(() => { }));
94
+
95
+ const err = await rejection(a.get("hang", undefined, { timeout_ms: 10 }));
96
+
97
+ assert(err instanceof EntangldTimeoutError);
98
+ });
99
+
100
+ it("resolves if a local function beats the timeout", async () => {
101
+ const a = new Entangld();
102
+ a.set("slow", () => wait(1).then(() => 8));
103
+
104
+ assert.strictEqual(await a.get("slow", undefined, { timeout_ms: 1000 }), 8);
105
+ });
106
+
107
+ it("keeps the original error if a local function rejects", async () => {
108
+ const a = new Entangld();
109
+ a.set("bad", () => Promise.reject(new Error("boom")));
110
+
111
+ const err = await rejection(a.get("bad", undefined, { timeout_ms: 1000 }));
112
+
113
+ assert.strictEqual(err.message, "boom");
114
+ });
115
+
116
+ it("returns local data", async () => {
117
+ const a = new Entangld();
118
+ a.set("value", 3);
119
+
120
+ assert.strictEqual(await a.get("value", undefined, { timeout_ms: 10 }), 3);
121
+ });
122
+
123
+ it("never settles a lost reply without timeout_ms", async () => {
124
+ const { a } = linked_stores(() => false);
125
+ let settled = false;
126
+
127
+ a.get("b.value").finally(() => {
128
+ settled = true;
129
+ });
130
+ await wait(20);
131
+
132
+ assert.strictEqual(settled, false);
133
+ assert.strictEqual(Object.keys(a._requests).length, 1);
134
+ });
135
+
136
+ it("throws on an invalid timeout_ms", () => {
137
+ const a = new Entangld();
138
+
139
+ for (const timeout_ms of [0, -1, NaN, Infinity, 2147483648, "5", null]) {
140
+ assert.throws(() => a.get("value", undefined, { timeout_ms }), TypeError);
141
+ }
142
+ });
143
+ });
144
+
145
+ describe("call_rpc", () => {
146
+
147
+ it("rejects if a remote store never replies", async () => {
148
+ const { a } = linked_stores(() => false);
149
+
150
+ const err = await rejection(a.call_rpc("b.rpc", [1], { timeout_ms: 10 }));
151
+
152
+ assert(err instanceof EntangldTimeoutError);
153
+ assert.strictEqual(err.path, "b.rpc");
154
+ });
155
+
156
+ it("returns the result if the reply beats the timeout", async () => {
157
+ const { a, b } = linked_stores();
158
+ b.set_rpc("double", (value) => value * 2);
159
+
160
+ const value = await a.call_rpc("b.double", [4], { timeout_ms: 1000 });
161
+
162
+ assert.strictEqual(value, 8);
163
+ });
164
+
165
+ it("rejects on an invalid timeout_ms", async () => {
166
+ const a = new Entangld();
167
+
168
+ const err = await rejection(a.call_rpc("rpc", [], { timeout_ms: 0 }));
169
+
170
+ assert(err instanceof TypeError);
171
+ });
172
+ });
173
+ });
package/test/uuid.js ADDED
@@ -0,0 +1,37 @@
1
+ const assert = require("assert");
2
+ const uuidv4 = require("../lib/uuid.js");
3
+ const { make_uuid } = uuidv4;
4
+
5
+ const V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
6
+ const node_crypto = require("crypto");
7
+
8
+ describe("uuid", () => {
9
+ it("makes distinct v4 uuids", () => {
10
+ const ids = new Set(Array.from({ length: 1000 }, () => uuidv4()));
11
+ assert.strictEqual(ids.size, 1000);
12
+ for ( const id of ids ) assert.match(id, V4);
13
+ });
14
+
15
+ it("uses globalThis.crypto.randomUUID when there is one (browsers, Node 19+)", () => {
16
+ const uuid = make_uuid({ randomUUID: () => "from-web-crypto" }, () => assert.fail("Node's crypto was loaded"));
17
+ assert.strictEqual(uuid(), "from-web-crypto");
18
+ });
19
+
20
+ it("falls back to Node's crypto module (Node 18)", () => {
21
+ const uuid = make_uuid(null, () => ({ randomUUID: () => "from-node-crypto" }));
22
+ assert.strictEqual(uuid(), "from-node-crypto");
23
+ });
24
+
25
+ it("builds one from getRandomValues on a page that isn't secure", () => {
26
+ // In a browser bundle, the "browser" field turns require("crypto") into {}
27
+ const page = { getRandomValues: (a) => node_crypto.getRandomValues(a) };
28
+ const uuid = make_uuid(page, () => ({}));
29
+ const ids = new Set(Array.from({ length: 1000 }, () => uuid()));
30
+ assert.strictEqual(ids.size, 1000);
31
+ for ( const id of ids ) assert.match(id, V4);
32
+ });
33
+
34
+ it("says what's missing when there's no crypto at all", () => {
35
+ assert.throws(() => make_uuid(null, () => { throw new Error("no module"); }), /crypto.randomUUID or crypto.getRandomValues/);
36
+ });
37
+ });
package/.gitmodules DELETED
@@ -1,3 +0,0 @@
1
- [submodule "libraries/unix/extern/json"]
2
- path = libraries/unix/extern/json
3
- url = https://github.com/nlohmann/json.git
package/README.hbs DELETED
@@ -1,190 +0,0 @@
1
- # Entangld
2
-
3
- Synchronized key-value stores with RPCs and pub/sub events. Works over sockets (try it with [Sockhop](https://www.npmjs.com/package/sockhop "Sockhop on NPM")!)
4
-
5
- ## Examples
6
- Basic use:
7
- ```js
8
- let e=new Entangld();
9
-
10
- // Simple set/get
11
- e.set("number.six",6);
12
- e.get("number.six").then((val)=>{}); // val==6
13
-
14
- // Functions as values
15
- e._deref_mode=true; // Or pass to constructor: new Entangld({ deref_mode: true });
16
- e.set("number.seven",()=>{ return 7;});
17
- e.get("number").then((val)=>{}); // val => { six:6, seven, 7}
18
-
19
- // Promises from functions
20
- e.set("number.eight",()=>{ return new Promise((resolve)=>{ resolve(8); }); });
21
- e.get("number.eight").then((val)=>{}); // val==8
22
-
23
- // Even dereference beneath functions
24
- e.set("eenie.meenie",()=>{ return {"miney": "moe"}; });
25
- e.get("eenie.meenie.miney").then((val)=>{}); // val=="moe"
26
-
27
- ```
28
-
29
- Pairing two data stores together:
30
- ```js
31
- let parent=new Entangld();
32
- let child=new Entangld();
33
-
34
- // Attach child namespace
35
- parent.attach("child",child);
36
-
37
- // Configure communications
38
- parent.transmit((msg, store) => store.receive(msg,parent)); // store will always be child
39
- child.transmit((msg, store) => store.receive(msg, child)); // store will always be parent
40
-
41
- // Set something in the child...
42
- child.set("system.voltage",33);
43
-
44
- // Get it back in the parent
45
- parent.get("child.system.voltage"); // == 33
46
- ```
47
- Using getter functions as RPC:
48
- ```js
49
- // Assign a function to a child key
50
- child.set("double.me",(param=0)=>param*2); // Or we could return a Promise instead of a value, if we wanted to!
51
-
52
- // Call the RPC from the parent
53
- parent.get("child.double.me", 2).then((val)=>{
54
-
55
- // val == 4
56
- });
57
-
58
- ```
59
- Note in this example how we set a default value for this getter function (0). This is because when _deref_mode is ```true``` this getter will be called without any arguments.
60
-
61
- Pub/sub (remote events):
62
- ```js
63
- // Assign an event callback
64
- parent.subscribe("child.system.voltage",(path, val)=>{
65
-
66
- // path=="child.system.voltage"
67
- // val==21
68
- });
69
-
70
- // Trigger a callback on the parent
71
- child.set("system.voltage",21);
72
-
73
-
74
- // Listen on the child for when the parent subscribes
75
- child.on("subscription", ( path, uuid ) => console.log("Parent subscribed to :" + path));
76
- parent.subscribe("child.system.voltage"); // Child prints: "Parent subscribed to : system.voltage"
77
-
78
- // Throttle the subscription callbacks, so that the callback is only called every 2 sets
79
- let counter = 0;
80
- parent.subscribe("child.rapid.data", () => counter += 1, 2);
81
- child.set("rapid.data", 1) // triggers callback
82
- child.set("rapid.data", 1) // doesn't trigger callback
83
- child.set("rapid.data", 1) // triggers callback
84
- child.set("rapid.data", 1) // doesn't trigger callback
85
- console.log( counter ); // === 2
86
-
87
- ```
88
- Over sockets:
89
- ```js
90
- const Sockhop=require("sockhop");
91
- const Entangld=require("entangld");
92
-
93
-
94
- /**
95
- * Parent / server setup
96
- */
97
-
98
- let parent=new Entangld();
99
- let server=new Sockhop.server();
100
-
101
- // Connect server to parent store
102
- parent.transmit((msg, store)=>server.send(store, msg));
103
- server
104
- .on("receive",(data, meta)=>parent.receive(data, meta.sock)) // Use the socket as the data store handle
105
- .on('connect',(sock)=>{
106
-
107
- parent.attach("client", sock); // "client" works for one client. Normally use uuid() or something
108
-
109
- parent.get("client.my.name")
110
- .then((val)=>{
111
-
112
- console.log("Client's name is "+val);
113
- server.close();
114
- });
115
- })
116
- .on('disconnect', (sock)=>parent.detach(null, sock))
117
- .on('error', (e)=>console.log("Sockhop error: "+e))
118
- .listen();
119
-
120
-
121
- /**
122
- * Child / client setup
123
- */
124
-
125
- let child=new Entangld();
126
- let client=new Sockhop.client();
127
-
128
- // Connect client to child store
129
- child.transmit((msg)=>client.send(msg));
130
- client
131
- .on("receive", (data, meta)=>child.receive(data))
132
- .on("connect", ()=>{
133
- // attach() to parent is optional, if we plan to get() parent items
134
- })
135
- .on("error", (e)=>console.log("Sockhop error: "+e))
136
- .connect();
137
-
138
- child.set("my.name", "Entangld");
139
- ```
140
-
141
-
142
- ## Raison d'etre
143
- Any object can store values. And a Map can store values keyed to objects. But what if you want to....
144
-
145
- - Have your store synchronized with multiple data sources (other stores)?
146
- - Over a network?
147
- - Subscribe to events within the datastore?
148
- - Support subscription wildcards?
149
-
150
- ## Notes
151
-
152
- - As of version 2.0.0, subscriptions all have uuids (which are the same across a chain of subscription objects, in the case of subscriptions to remote datastores), which supports multiple (remote or local) subscriptions to the same endpoint, but with different callback functions. The uuid of a subscription is available in the subscription object (a list of which is exposed from the datastore via `store.owned_subscriptions()`), and is also returned from the method call `store.subscribe("path.to.data")`. Having access to this uuid also allows the user to cancel specific subscriptions. The method call `store.unsubscribe(...)` will now either accept a path or a uuid. For paths, the method will terminate _all_ subscriptions matching that path, while for uuids, only the specific subscription matching that uuid will be canceled.
153
- - As of version 1.5.0, subscriptions may be created across multiple chained datastores. Subscription removal now acts on all subscriptions matching (but not beneath) a given path. To unsubscribe a tree, use ```unsubscribe_tree()```
154
- - As of version 1.2.0, attached datastores may be located at arbitrary paths, not just in the root:
155
- ```parent.attach("child.goes.here", child);```
156
- - Also as of 1.2.0, attach()ed child stores will appear in the parent as placeholders (empty objects) when .get() is called on paths within the parent. For example:
157
- ```
158
- child.set("",{"child" : "data" });
159
- parent.set("",{"parent" : "data"});
160
- parent.attach("child", child);
161
-
162
- parent.get(""); // Returns { "parent" : "data", "child" : {} }
163
- parent.get("child"); // Returns {"child" : "data"}
164
- ```
165
- This is because we would have to perform recursive child queries to show you a complete tree. This is left for a future version.
166
- - If you ```.set()``` a function, that function may return a value or a Promise. If it returns a promise, that promise will be returned directly to you when you call ```.get()```
167
- - As of version 1.4.0, you may subscribe() to a local event. This should probably be eventually replaced with native events. In other words, instead of ```.subscribe("a.b.c", callback)``` we should use ```.on("path.a.b.c", callback)```
168
-
169
- ## _deref_mode
170
- If you attach a key to a getter function instead of a value, that function would never be called until you request that key directly (i.e. querying the parent of that key would not reveal that that key exists). This changed in 1.2.1, when _deref_mode was introduced. If you set _deref_mode to true, it will iterate all leaves and try to call all functions. Those that return Promise will have their Promise resolved before the result is actually returned.
171
-
172
- This is pretty cool, and after consideration it is probably the way this thing should work all the time. However it also introduces two problems which are not yet resolved (//TODO):
173
-
174
- First, in an effort to not accidentally mutate the original data set, a copy is made. This is somewhat inefficient.
175
- Second, when the copy is made, JSON.parse/JSON.stringify are used. This means that leaves consisting of Map() or the like are just erased.
176
-
177
- If these two issues can be resolved at some point, _deref_mode will probably be turned on permanently. Honestly, for remote stores operating over sockets it's probably not a huge issue. More to the point are local stores where the user might be storing non JSON-compatible items.
178
-
179
- {{>main}}
180
-
181
- ## TODO
182
- - Make sure incoming values request store doesn't build up
183
- - When querying a parent, perhaps there should be an option to also dump child stores located below that level (potentially resource intensive)
184
- - Fix _deref_mode so it doesn't "strip" the returned object by turning everything to JSON and back (inefficient and it's basically mutating the result silently)
185
- - Fix unit tests so they are completely independent (tests often depend on prior test ending with a particular state)
186
- - Detaching a store does not unsubscribe to any subscriptions from or through that store and may therefore leave things in a dirty or unstable state. We could unsubscribe_tree(store_name) but that would not take care of passthrough subscriptions (those that don't belong to us)
187
- - unsubscribe_tree() needs to have a test ensuring errors are thrown if not all subs can be removed
188
-
189
- ## License
190
- MIT