entangld 2.6.0 → 2.7.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/.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 +142 -19
- package/lib/Message.js +9 -10
- package/lib/Subscription.js +23 -0
- package/lib/error.js +15 -0
- package/package.json +1 -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/.gitmodules +0 -3
- package/README.hbs +0 -190
- package/tags +0 -244
package/README.md
CHANGED
|
@@ -1,903 +1,316 @@
|
|
|
1
1
|
# Entangld
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/Daxbot/entangld/actions/workflows/mocha-tests.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/entangld)
|
|
5
|
+
[](LICENSE)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
Synchronized key-value stores with RPCs and pub/sub events, in pure JavaScript with no runtime dependencies.
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
Entangld links stores together so that each one can read, write, call and watch data in the others, using ordinary
|
|
10
|
+
paths. Stores can be in the same process, or spread across a network. It doesn't care how they're connected: you give it
|
|
11
|
+
a way to send a message, and it does the rest.
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
e.set("number.six",6);
|
|
16
|
-
e.get("number.six").then((val)=>{}); // val==6
|
|
13
|
+
- **One path for everything.** `get("robot.motor.speed")` works the same whether the data is local or several stores away.
|
|
14
|
+
- **Functions as values.** Store a function at a path, and reading the path calls it.
|
|
15
|
+
- **RPCs.** Call functions in other stores, and get their results and errors back.
|
|
16
|
+
- **Subscriptions.** Watch a path or a wildcard in any linked store, with optional throttling.
|
|
17
|
+
- **Any transport.** Sockets, IPC, or a function call. [Sockhop](https://www.npmjs.com/package/sockhop) works well.
|
|
18
|
+
- **Timeouts and cancellation.** Give up on a request with a timeout or an `AbortSignal`.
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
e._deref_mode=true; // Or pass to constructor: new Entangld({ deref_mode: true });
|
|
20
|
-
e.set("number.seven",()=>{ return 7;});
|
|
21
|
-
e.get("number").then((val)=>{}); // val => { six:6, seven, 7}
|
|
20
|
+
## Install
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
e.get("number.eight").then((val)=>{}); // val==8
|
|
26
|
-
|
|
27
|
-
// Even dereference beneath functions
|
|
28
|
-
e.set("eenie.meenie",()=>{ return {"miney": "moe"}; });
|
|
29
|
-
e.get("eenie.meenie.miney").then((val)=>{}); // val=="moe"
|
|
30
|
-
|
|
22
|
+
```sh
|
|
23
|
+
npm install entangld
|
|
31
24
|
```
|
|
32
25
|
|
|
33
|
-
|
|
34
|
-
```js
|
|
35
|
-
let parent=new Entangld();
|
|
36
|
-
let child=new Entangld();
|
|
37
|
-
|
|
38
|
-
// Attach child namespace
|
|
39
|
-
parent.attach("child",child);
|
|
26
|
+
## Quick start
|
|
40
27
|
|
|
41
|
-
|
|
42
|
-
parent.transmit((msg, store) => store.receive(msg,parent)); // store will always be child
|
|
43
|
-
child.transmit((msg, store) => store.receive(msg, child)); // store will always be parent
|
|
28
|
+
Link two stores, then use one to reach into the other.
|
|
44
29
|
|
|
45
|
-
// Set something in the child...
|
|
46
|
-
child.set("system.voltage",33);
|
|
47
|
-
|
|
48
|
-
// Get it back in the parent
|
|
49
|
-
parent.get("child.system.voltage"); // == 33
|
|
50
|
-
```
|
|
51
|
-
Using getter functions as RPC:
|
|
52
30
|
```js
|
|
53
|
-
|
|
54
|
-
child.set("double.me",(param=0)=>param*2); // Or we could return a Promise instead of a value, if we wanted to!
|
|
31
|
+
const Entangld = require("entangld");
|
|
55
32
|
|
|
56
|
-
|
|
57
|
-
|
|
33
|
+
const parent = new Entangld();
|
|
34
|
+
const child = new Entangld();
|
|
58
35
|
|
|
59
|
-
|
|
60
|
-
|
|
36
|
+
// Tell each store how to reach the other. Here that's a function call, but it could be a socket
|
|
37
|
+
parent.transmit((msg, store) => store.receive(msg, parent));
|
|
38
|
+
child.transmit((msg, store) => store.receive(msg, child));
|
|
61
39
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
Pub/sub (remote events):
|
|
66
|
-
```js
|
|
67
|
-
// Assign an event callback
|
|
68
|
-
parent.subscribe("child.system.voltage",(path, val)=>{
|
|
69
|
-
|
|
70
|
-
// path=="child.system.voltage"
|
|
71
|
-
// val==21
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// Trigger a callback on the parent
|
|
75
|
-
child.set("system.voltage",21);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// Listen on the child for when the parent subscribes
|
|
79
|
-
child.on("subscription", ( path, uuid ) => console.log("Parent subscribed to :" + path));
|
|
80
|
-
parent.subscribe("child.system.voltage"); // Child prints: "Parent subscribed to : system.voltage"
|
|
40
|
+
// Make the child visible at a path in the parent
|
|
41
|
+
parent.attach("child", child);
|
|
81
42
|
|
|
82
|
-
|
|
83
|
-
let counter = 0;
|
|
84
|
-
parent.subscribe("child.rapid.data", () => counter += 1, 2);
|
|
85
|
-
child.set("rapid.data", 1) // triggers callback
|
|
86
|
-
child.set("rapid.data", 1) // doesn't trigger callback
|
|
87
|
-
child.set("rapid.data", 1) // triggers callback
|
|
88
|
-
child.set("rapid.data", 1) // doesn't trigger callback
|
|
89
|
-
console.log( counter ); // === 2
|
|
43
|
+
child.set("system.voltage", 33);
|
|
90
44
|
|
|
45
|
+
await parent.get("child.system.voltage"); // 33
|
|
91
46
|
```
|
|
92
|
-
Over sockets:
|
|
93
|
-
```js
|
|
94
|
-
const Sockhop=require("sockhop");
|
|
95
|
-
const Entangld=require("entangld");
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Parent / server setup
|
|
100
|
-
*/
|
|
101
|
-
|
|
102
|
-
let parent=new Entangld();
|
|
103
|
-
let server=new Sockhop.server();
|
|
104
|
-
|
|
105
|
-
// Connect server to parent store
|
|
106
|
-
parent.transmit((msg, store)=>server.send(store, msg));
|
|
107
|
-
server
|
|
108
|
-
.on("receive",(data, meta)=>parent.receive(data, meta.sock)) // Use the socket as the data store handle
|
|
109
|
-
.on('connect',(sock)=>{
|
|
110
47
|
|
|
111
|
-
|
|
48
|
+
The examples below use `await`, so they need to run inside an `async` function. Examples which use `parent` and `child`
|
|
49
|
+
assume the stores above.
|
|
112
50
|
|
|
113
|
-
|
|
114
|
-
.then((val)=>{
|
|
51
|
+
## Examples
|
|
115
52
|
|
|
116
|
-
|
|
117
|
-
server.close();
|
|
118
|
-
});
|
|
119
|
-
})
|
|
120
|
-
.on('disconnect', (sock)=>parent.detach(null, sock))
|
|
121
|
-
.on('error', (e)=>console.log("Sockhop error: "+e))
|
|
122
|
-
.listen();
|
|
53
|
+
### Reading and writing
|
|
123
54
|
|
|
55
|
+
```js
|
|
56
|
+
const store = new Entangld();
|
|
124
57
|
|
|
125
|
-
|
|
126
|
-
* Child / client setup
|
|
127
|
-
*/
|
|
58
|
+
store.set("number.six", 6);
|
|
128
59
|
|
|
129
|
-
|
|
130
|
-
|
|
60
|
+
await store.get("number.six"); // 6
|
|
61
|
+
await store.get("number"); // { six: 6 }
|
|
131
62
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
.on("receive", (data, meta)=>child.receive(data))
|
|
136
|
-
.on("connect", ()=>{
|
|
137
|
-
// attach() to parent is optional, if we plan to get() parent items
|
|
138
|
-
})
|
|
139
|
-
.on("error", (e)=>console.log("Sockhop error: "+e))
|
|
140
|
-
.connect();
|
|
63
|
+
// Write to a linked store with the same path you'd read it with
|
|
64
|
+
parent.set("child.system.led", true);
|
|
65
|
+
await child.get("system.led"); // true
|
|
141
66
|
|
|
142
|
-
|
|
67
|
+
// Add to an array, keeping the 2 newest entries
|
|
68
|
+
child.set("log", []);
|
|
69
|
+
parent.push("child.log", "first", 2);
|
|
70
|
+
parent.push("child.log", "second", 2);
|
|
71
|
+
parent.push("child.log", "third", 2);
|
|
72
|
+
await child.get("log"); // [ "second", "third" ]
|
|
143
73
|
```
|
|
144
74
|
|
|
75
|
+
### Functions as values
|
|
145
76
|
|
|
146
|
-
|
|
147
|
-
Any object can store values. And a Map can store values keyed to objects. But what if you want to....
|
|
148
|
-
|
|
149
|
-
- Have your store synchronized with multiple data sources (other stores)?
|
|
150
|
-
- Over a network?
|
|
151
|
-
- Subscribe to events within the datastore?
|
|
152
|
-
- Support subscription wildcards?
|
|
77
|
+
A function at a path runs when the path is read. It can return a value or a promise, and you can read beneath its result.
|
|
153
78
|
|
|
154
|
-
|
|
79
|
+
```js
|
|
80
|
+
child.set("sensor.read", async () => ({ temperature: 21 }));
|
|
81
|
+
child.set("math.double", (value) => value * 2);
|
|
155
82
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
- As of version 1.2.0, attached datastores may be located at arbitrary paths, not just in the root:
|
|
159
|
-
```parent.attach("child.goes.here", child);```
|
|
160
|
-
- 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:
|
|
161
|
-
```
|
|
162
|
-
child.set("",{"child" : "data" });
|
|
163
|
-
parent.set("",{"parent" : "data"});
|
|
164
|
-
parent.attach("child", child);
|
|
83
|
+
await parent.get("child.sensor.read"); // { temperature: 21 }
|
|
84
|
+
await parent.get("child.sensor.read.temperature"); // 21
|
|
165
85
|
|
|
166
|
-
|
|
167
|
-
parent.get("child");
|
|
86
|
+
// The second argument is passed to the function
|
|
87
|
+
await parent.get("child.math.double", 4); // 8
|
|
168
88
|
```
|
|
169
|
-
This is because we would have to perform recursive child queries to show you a complete tree. This is left for a future version.
|
|
170
|
-
- 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()```
|
|
171
|
-
- 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)```
|
|
172
|
-
|
|
173
|
-
## _deref_mode
|
|
174
|
-
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.
|
|
175
|
-
|
|
176
|
-
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):
|
|
177
|
-
|
|
178
|
-
First, in an effort to not accidentally mutate the original data set, a copy is made. This is somewhat inefficient.
|
|
179
|
-
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.
|
|
180
|
-
|
|
181
|
-
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.
|
|
182
|
-
|
|
183
|
-
## Classes
|
|
184
|
-
|
|
185
|
-
<dl>
|
|
186
|
-
<dt><a href="#Entangld">Entangld</a> ⇐ <code>EventEmitter</code></dt>
|
|
187
|
-
<dd><p>Synchronized Event Store</p>
|
|
188
|
-
</dd>
|
|
189
|
-
<dt><a href="#EntangldError">EntangldError</a></dt>
|
|
190
|
-
<dd><p>Error class for Entangld.</p>
|
|
191
|
-
</dd>
|
|
192
|
-
<dt><a href="#RPCError">RPCError</a></dt>
|
|
193
|
-
<dd><p>Remote Error class for Entangld.</p>
|
|
194
|
-
</dd>
|
|
195
|
-
<dt><a href="#Entangld_Message">Entangld_Message</a></dt>
|
|
196
|
-
<dd><p>Message class for Entangld.</p>
|
|
197
|
-
<p>These messages are used for executing datastore operations between
|
|
198
|
-
remote datastores. In these relationships, there is always an upstream
|
|
199
|
-
and downstream pair, so that <code>get</code>, <code>set</code>, <code>push</code> and <code>subscribe</code> messages
|
|
200
|
-
are created upstream and passed downstream while <code>event</code> and <code>value</code> messages
|
|
201
|
-
are created downstream and are passed back up. <code>unsubscribe</code> events can travel
|
|
202
|
-
in both directions. To maintain consistency, the internal <code>.path</code> attribute
|
|
203
|
-
will always refer a path relative to the downstream datastore, since the
|
|
204
|
-
downstream datastores do not necessarilly have access to the upstream store
|
|
205
|
-
structure, and so cannot generally construct upstream paths. This means that
|
|
206
|
-
the <code>.path</code> attribute is a <code>tree</code> relative to the upstream datastore, and
|
|
207
|
-
the upstream path can be reconstructed as:</p>
|
|
208
|
-
<pre><code class="language-javascript"> > upstream._namespaces.get(downstream) + "." + msg.path;
|
|
209
|
-
</code></pre>
|
|
210
|
-
<p>Since <code>unsubscribe</code> messages can pass either upstream or downstream, the notion
|
|
211
|
-
of a path is ill-defined, and so unsubscribe messages should have their <code>.path</code>
|
|
212
|
-
attributes set to undefined or null.</p>
|
|
213
|
-
<p>Most messages will also have a <code>.uuid</code> attribute. For <code>get</code>/<code>value</code> messages,
|
|
214
|
-
this allows for the value to be properly linked back up with the original <code>get</code>
|
|
215
|
-
message. For the <code>subscribe</code>/<code>unsubscribe</code>/<code>event</code> messages, this allows for
|
|
216
|
-
callback functions to be trigger properly, and for unsubscribe messages to
|
|
217
|
-
propogate both directions. <code>set</code>/<code>push</code> messages do not use the <code>.uuid</code> attribute
|
|
218
|
-
since they require no response.</p>
|
|
219
|
-
</dd>
|
|
220
|
-
<dt><a href="#Subscription">Subscription</a></dt>
|
|
221
|
-
<dd><p>A datastore subscription object</p>
|
|
222
|
-
</dd>
|
|
223
|
-
</dl>
|
|
224
|
-
|
|
225
|
-
<a name="Entangld"></a>
|
|
226
|
-
|
|
227
|
-
## Entangld ⇐ <code>EventEmitter</code>
|
|
228
|
-
Synchronized Event Store
|
|
229
|
-
|
|
230
|
-
**Kind**: global class
|
|
231
|
-
**Extends**: <code>EventEmitter</code>
|
|
232
|
-
|
|
233
|
-
* [Entangld](#Entangld) ⇐ <code>EventEmitter</code>
|
|
234
|
-
* [.namespaces](#Entangld+namespaces) ⇒ <code>array</code>
|
|
235
|
-
* [.subscriptions](#Entangld+subscriptions) ⇒ [<code>Array.<Subscription></code>](#Subscription)
|
|
236
|
-
* [.namespace()](#Entangld+namespace) ⇒ <code>string</code>
|
|
237
|
-
* [.attach(namespace, obj)](#Entangld+attach)
|
|
238
|
-
* [.detach([namespace], [obj])](#Entangld+detach) ⇒ <code>boolean</code>
|
|
239
|
-
* [.transmit(func)](#Entangld+transmit)
|
|
240
|
-
* [.receive(msg, obj)](#Entangld+receive)
|
|
241
|
-
* [.push(path, data, [limit])](#Entangld+push)
|
|
242
|
-
* [.set(path, data, [operation_type], [params])](#Entangld+set)
|
|
243
|
-
* [.set_rpc(path, func, opts)](#Entangld+set_rpc)
|
|
244
|
-
* [.set_rpc_class(object, key_description_pairs, opts)](#Entangld+set_rpc_class)
|
|
245
|
-
* [.call_rpc(path, [args])](#Entangld+call_rpc) ⇒ <code>Promise</code>
|
|
246
|
-
* [.get(path, [params])](#Entangld+get) ⇒ <code>Promise</code>
|
|
247
|
-
* [.subscribe(path, func, [every])](#Entangld+subscribe) ⇒ <code>uuidv4</code>
|
|
248
|
-
* [.subscribed_to(subscription)](#Entangld+subscribed_to) ⇒ <code>Boolean</code>
|
|
249
|
-
* [.unsubscribe(path_or_uuid)](#Entangld+unsubscribe) ⇒ <code>number</code>
|
|
250
|
-
* [.unsubscribe_tree(path)](#Entangld+unsubscribe_tree)
|
|
251
|
-
|
|
252
|
-
<a name="Entangld+namespaces"></a>
|
|
253
|
-
|
|
254
|
-
### entangld.namespaces ⇒ <code>array</code>
|
|
255
|
-
Get namespaces
|
|
256
|
-
|
|
257
|
-
**Kind**: instance property of [<code>Entangld</code>](#Entangld)
|
|
258
|
-
**Returns**: <code>array</code> - namespaces - an array of attached namespaces
|
|
259
|
-
**Read only**: true
|
|
260
|
-
<a name="Entangld+subscriptions"></a>
|
|
261
|
-
|
|
262
|
-
### entangld.subscriptions ⇒ [<code>Array.<Subscription></code>](#Subscription)
|
|
263
|
-
Get list of subscriptions associated with this object
|
|
264
|
-
|
|
265
|
-
Note, this will include `head`, `terminal` and `pass through` subscriptions,
|
|
266
|
-
which can be checked using getter methods of the subscription object.
|
|
267
|
-
|
|
268
|
-
**Kind**: instance property of [<code>Entangld</code>](#Entangld)
|
|
269
|
-
**Returns**: [<code>Array.<Subscription></code>](#Subscription) - array of Subscriptions associated with this object
|
|
270
|
-
**Read only**: true
|
|
271
|
-
<a name="Entangld+namespace"></a>
|
|
272
|
-
|
|
273
|
-
### entangld.namespace() ⇒ <code>string</code>
|
|
274
|
-
Get namespace for a store
|
|
275
|
-
|
|
276
|
-
**Kind**: instance method of [<code>Entangld</code>](#Entangld)
|
|
277
|
-
**Returns**: <code>string</code> - namespace for the given store
|
|
278
|
-
**Read only**: true
|
|
279
|
-
<a name="Entangld+attach"></a>
|
|
280
|
-
|
|
281
|
-
### entangld.attach(namespace, obj)
|
|
282
|
-
Attach a namespace and a store
|
|
283
|
-
|
|
284
|
-
**Kind**: instance method of [<code>Entangld</code>](#Entangld)
|
|
285
|
-
**Throws**:
|
|
286
|
-
|
|
287
|
-
- <code>TypeError</code> if namespace/obj is null or empty.
|
|
288
|
-
- [<code>EntangldError</code>](#EntangldError) if you try to attach to the same namespace twice.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
| Param | Type | Description |
|
|
292
|
-
| --- | --- | --- |
|
|
293
|
-
| namespace | <code>string</code> | a namespace for this store. |
|
|
294
|
-
| obj | <code>object</code> | an object that will be sent along with "transmit" callbacks when we need something from this store. |
|
|
295
|
-
|
|
296
|
-
<a name="Entangld+detach"></a>
|
|
297
|
-
|
|
298
|
-
### entangld.detach([namespace], [obj]) ⇒ <code>boolean</code>
|
|
299
|
-
Detach a namespace / obj pair.
|
|
300
|
-
|
|
301
|
-
If you only pass a namespace or a store, it will find the missing item
|
|
302
|
-
before detaching.
|
|
303
|
-
|
|
304
|
-
**Kind**: instance method of [<code>Entangld</code>](#Entangld)
|
|
305
|
-
**Returns**: <code>boolean</code> - true if the element existed and was removed.
|
|
306
|
-
**Throws**:
|
|
307
|
-
|
|
308
|
-
- [<code>EntangldError</code>](#EntangldError) Error will be thrown if you don't pass at least
|
|
309
|
-
one parameter.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
| Param | Type | Description |
|
|
313
|
-
| --- | --- | --- |
|
|
314
|
-
| [namespace] | <code>string</code> | the namespace. |
|
|
315
|
-
| [obj] | <code>object</code> | the store object. |
|
|
316
|
-
|
|
317
|
-
<a name="Entangld+transmit"></a>
|
|
318
|
-
|
|
319
|
-
### entangld.transmit(func)
|
|
320
|
-
Transmit
|
|
321
|
-
|
|
322
|
-
Specify a callback to be used so we can transmit data to another store.
|
|
323
|
-
Callback will be passed (message, obj) where 'message' is an
|
|
324
|
-
Entangld_Message object and obj is the object provided by attach().
|
|
325
|
-
|
|
326
|
-
**Kind**: instance method of [<code>Entangld</code>](#Entangld)
|
|
327
|
-
**Throws**:
|
|
328
|
-
|
|
329
|
-
- <code>TypeError</code> if func is not a function.
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
| Param | Type | Description |
|
|
333
|
-
| --- | --- | --- |
|
|
334
|
-
| func | <code>function</code> | the callback function. |
|
|
335
|
-
|
|
336
|
-
<a name="Entangld+receive"></a>
|
|
337
|
-
|
|
338
|
-
### entangld.receive(msg, obj)
|
|
339
|
-
Receive
|
|
340
|
-
|
|
341
|
-
Call this function with the data that was sent via the transmit()
|
|
342
|
-
callback.
|
|
343
|
-
|
|
344
|
-
**Kind**: instance method of [<code>Entangld</code>](#Entangld)
|
|
345
|
-
**Throws**:
|
|
346
|
-
|
|
347
|
-
- <code>ReferenceError</code> if event object was not provided.
|
|
348
|
-
- [<code>EntangldError</code>](#EntangldError) if an unknown message type was received.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
| Param | Type | Description |
|
|
352
|
-
| --- | --- | --- |
|
|
353
|
-
| msg | [<code>Entangld\_Message</code>](#Entangld_Message) | the message to process. |
|
|
354
|
-
| obj | <code>object</code> | the attach() object where the message originted. |
|
|
355
|
-
|
|
356
|
-
<a name="Entangld+push"></a>
|
|
357
|
-
|
|
358
|
-
### entangld.push(path, data, [limit])
|
|
359
|
-
Push an object into an array in the store.
|
|
360
|
-
|
|
361
|
-
Convenience method for set(path, o, "push").
|
|
362
|
-
|
|
363
|
-
**Kind**: instance method of [<code>Entangld</code>](#Entangld)
|
|
364
|
-
**Throws**:
|
|
365
|
-
|
|
366
|
-
- <code>TypeError</code> if path is not a string.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
| Param | Type | Default | Description |
|
|
370
|
-
| --- | --- | --- | --- |
|
|
371
|
-
| path | <code>string</code> | | the path to set (like "system.fan.voltage"). |
|
|
372
|
-
| data | <code>object</code> | | the object or function you want to store at path. |
|
|
373
|
-
| [limit] | <code>number</code> | <code></code> | maximum size of the array. Older entries will be removed until the array size is less than or equal to limit. |
|
|
374
|
-
|
|
375
|
-
<a name="Entangld+set"></a>
|
|
376
|
-
|
|
377
|
-
### entangld.set(path, data, [operation_type], [params])
|
|
378
|
-
Set an object into the store
|
|
379
|
-
|
|
380
|
-
**Kind**: instance method of [<code>Entangld</code>](#Entangld)
|
|
381
|
-
**Throws**:
|
|
382
|
-
|
|
383
|
-
- <code>TypeError</code> if path is not a string.
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
| Param | Type | Default | Description |
|
|
387
|
-
| --- | --- | --- | --- |
|
|
388
|
-
| path | <code>string</code> | | the path to set (like "system.fan.voltage"). |
|
|
389
|
-
| data | <code>object</code> | | the object or function you want to store at path. |
|
|
390
|
-
| [operation_type] | <code>string</code> | <code>"\"set\""</code> | whether to set or push the new data (push only works if the data item exists and is an array). |
|
|
391
|
-
| [params] | <code>object</code> | | additional parameters. |
|
|
392
|
-
|
|
393
|
-
<a name="Entangld+set_rpc"></a>
|
|
394
|
-
|
|
395
|
-
### entangld.set\_rpc(path, func, opts)
|
|
396
|
-
Set a function as an RPC in the datastore
|
|
397
|
-
|
|
398
|
-
**Kind**: instance method of [<code>Entangld</code>](#Entangld)
|
|
399
|
-
**Throws**:
|
|
400
|
-
|
|
401
|
-
- <code>TypeError</code> if path is not a string.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
| Param | Type | Default | Description |
|
|
405
|
-
| --- | --- | --- | --- |
|
|
406
|
-
| path | <code>string</code> | | the path to set (like "system.fan.voltage"). |
|
|
407
|
-
| func | <code>function</code> | | the function you want to store at path. |
|
|
408
|
-
| opts | <code>object</code> | | |
|
|
409
|
-
| [opts.description] | <code>string</code> | <code>null</code> | description for the function |
|
|
410
|
-
| [opts.function_string] | <code>string</code> | <code>null</code> | optional string of function source code to pass to parameter_parser |
|
|
411
|
-
|
|
412
|
-
<a name="Entangld+set_rpc_class"></a>
|
|
413
|
-
|
|
414
|
-
### entangld.set\_rpc\_class(object, key_description_pairs, opts)
|
|
415
|
-
Set a class instance as an RPC object in the datastore
|
|
416
|
-
|
|
417
|
-
**Kind**: instance method of [<code>Entangld</code>](#Entangld)
|
|
418
|
-
**Throws**:
|
|
419
|
-
|
|
420
|
-
- <code>TypeError</code> if path is not a string.
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
| Param | Type | Default | Description |
|
|
424
|
-
| --- | --- | --- | --- |
|
|
425
|
-
| object | <code>\*</code> | | The object to attach |
|
|
426
|
-
| key_description_pairs | <code>Array</code> | | An array of key, description pairs |
|
|
427
|
-
| opts | <code>object</code> | | |
|
|
428
|
-
| [opts.description] | <code>string</code> | <code>null</code> | description for the function |
|
|
429
|
-
| [opts.function_string] | <code>string</code> | <code>null</code> | optional string of function source code to pass to parameter_parser |
|
|
430
|
-
|
|
431
|
-
<a name="Entangld+call_rpc"></a>
|
|
432
|
-
|
|
433
|
-
### entangld.call\_rpc(path, [args]) ⇒ <code>Promise</code>
|
|
434
|
-
Call an RPC procedure in the datastore
|
|
435
89
|
|
|
436
|
-
|
|
437
|
-
**Returns**: <code>Promise</code> - promise resolving to return value of the rpc
|
|
438
|
-
**Throws**:
|
|
90
|
+
### RPCs
|
|
439
91
|
|
|
440
|
-
|
|
92
|
+
`set_rpc` and `call_rpc` are the better choice for a function which can fail, because errors reach the caller. A plain
|
|
93
|
+
function can't report an error to another store, so its caller would never get a reply.
|
|
441
94
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
**Kind**: instance method of [<code>Entangld</code>](#Entangld)
|
|
457
|
-
**Returns**: <code>Promise</code> - promise resolving to the object at that path.
|
|
458
|
-
**Throws**:
|
|
459
|
-
|
|
460
|
-
- <code>TypeError</code> if path is not a string.
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
| Param | Type | Description |
|
|
464
|
-
| --- | --- | --- |
|
|
465
|
-
| path | <code>string</code> | the path to query (like "system.voltage"). |
|
|
466
|
-
| [params] | <code>object</code> | the parameters to be passed to the remote function (RPC) or the maximum depth of the returned object (normal mode). |
|
|
467
|
-
|
|
468
|
-
<a name="Entangld+subscribe"></a>
|
|
469
|
-
|
|
470
|
-
### entangld.subscribe(path, func, [every]) ⇒ <code>uuidv4</code>
|
|
471
|
-
Subscribe to change events for a path
|
|
472
|
-
|
|
473
|
-
If objects at or below this path change, you will get a callback
|
|
474
|
-
|
|
475
|
-
Subscriptions to keys within attach()ed stores are remote subscriptions.
|
|
476
|
-
If several stores are attached in some kind of arrangement, a given key
|
|
477
|
-
may actually traverse multiple stores! Since each store only knows its
|
|
478
|
-
immediate neighbors - and has no introspection into those neighbors - each
|
|
479
|
-
store is only able to keeps track of the neighbor on each side with
|
|
480
|
-
respect to a particular path and has no knowledge of the eventual
|
|
481
|
-
endpoints. This means that subscribing across several datstores is accomplished
|
|
482
|
-
by daisy-chaining 2-way subscriptions across each datastore interface.
|
|
483
|
-
|
|
484
|
-
For example, let's suppose capital letters represent Entangld stores and
|
|
485
|
-
lowercase letters are actual objects. Then the path "A.B.c.d.E.F.g.h"
|
|
486
|
-
will represent a subscription that traverses four Entangld stores.
|
|
487
|
-
From the point of view of a store in the middle - say, E - the "upstream"
|
|
488
|
-
is B and the "downstream" is F.
|
|
489
|
-
|
|
490
|
-
Each store involved keeps track of any subscriptions with which it is
|
|
491
|
-
involved. It tracks the upstream and downstream, and the uuid of the
|
|
492
|
-
subscription. The uuid is the same across all stores for a given
|
|
493
|
-
subscription. For a particular store, the upstream is null if it is the
|
|
494
|
-
original link in the chain (called the `head`), and the downstream is
|
|
495
|
-
null if this store owns the endpoint value (called the `tail`). Any
|
|
496
|
-
subscription which is not the head of a chain is called a `pass through`
|
|
497
|
-
subscription, because it exist only to pass `event` messages back up the
|
|
498
|
-
chain to the head (where the user-provided callback function exists).
|
|
499
|
-
subscriptions can be checked to see if they are `pass through` type via
|
|
500
|
-
the getter `sub.is_pass_through`.
|
|
501
|
-
|
|
502
|
-
**Kind**: instance method of [<code>Entangld</code>](#Entangld)
|
|
503
|
-
**Returns**: <code>uuidv4</code> - - the uuid of the subscription
|
|
504
|
-
**Throws**:
|
|
505
|
-
|
|
506
|
-
- <code>TypeError</code> if path is not a string.
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
| Param | Type | Default | Description |
|
|
510
|
-
| --- | --- | --- | --- |
|
|
511
|
-
| path | <code>string</code> | | the path to watch. Use of '*' is allowed as a wildcard (e.g. "system.*") |
|
|
512
|
-
| func | <code>function</code> | | the callback - will be of the form (path, value). |
|
|
513
|
-
| [every] | <code>number</code> \| <code>null</code> | <code></code> | the number of `set` messages to wait before calling callback |
|
|
514
|
-
|
|
515
|
-
<a name="Entangld+subscribed_to"></a>
|
|
516
|
-
|
|
517
|
-
### entangld.subscribed\_to(subscription) ⇒ <code>Boolean</code>
|
|
518
|
-
Check for subscription
|
|
519
|
-
|
|
520
|
-
Are we subscribed to a particular remote path?
|
|
521
|
-
|
|
522
|
-
**Kind**: instance method of [<code>Entangld</code>](#Entangld)
|
|
523
|
-
**Returns**: <code>Boolean</code> - true if we are subscribed.
|
|
524
|
-
|
|
525
|
-
| Param | Type | Description |
|
|
526
|
-
| --- | --- | --- |
|
|
527
|
-
| subscription | <code>String</code> | the subscription to check for. |
|
|
528
|
-
|
|
529
|
-
<a name="Entangld+unsubscribe"></a>
|
|
530
|
-
|
|
531
|
-
### entangld.unsubscribe(path_or_uuid) ⇒ <code>number</code>
|
|
532
|
-
Unubscribe to change events for a given path or uuid.
|
|
533
|
-
|
|
534
|
-
Caution - if a path is provided, _all_ events belonging to you with that
|
|
535
|
-
path will be deleted, so if you have multiple subscriptions on a single path,
|
|
536
|
-
and only want one of them to be removed, you must provide the uuid instead.
|
|
537
|
-
|
|
538
|
-
**Kind**: instance method of [<code>Entangld</code>](#Entangld)
|
|
539
|
-
**Returns**: <code>number</code> - count of subscriptions removed.
|
|
540
|
-
**Throws**:
|
|
541
|
-
|
|
542
|
-
- [<code>EntangldError</code>](#EntangldError) if no subscriptions were found.
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
| Param | Type | Description |
|
|
546
|
-
| --- | --- | --- |
|
|
547
|
-
| path_or_uuid | <code>String</code> \| <code>uuidv4</code> | the path (or uuid) to unwatch. |
|
|
548
|
-
|
|
549
|
-
<a name="Entangld+unsubscribe_tree"></a>
|
|
550
|
-
|
|
551
|
-
### entangld.unsubscribe\_tree(path)
|
|
552
|
-
Unsubscribe tree.
|
|
553
|
-
|
|
554
|
-
Remove any subscriptions that are beneath a path.
|
|
555
|
-
|
|
556
|
-
**Kind**: instance method of [<code>Entangld</code>](#Entangld)
|
|
557
|
-
**Throws**:
|
|
558
|
-
|
|
559
|
-
- [<code>EntangldError</code>](#EntangldError) error if there are stores we cannot detach
|
|
560
|
-
(i.e. they belong to someone else / upstream != null)
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
| Param | Type | Description |
|
|
564
|
-
| --- | --- | --- |
|
|
565
|
-
| path | <code>string</code> | the tree to unwatch. |
|
|
566
|
-
|
|
567
|
-
<a name="EntangldError"></a>
|
|
568
|
-
|
|
569
|
-
## EntangldError
|
|
570
|
-
Error class for Entangld.
|
|
571
|
-
|
|
572
|
-
**Kind**: global class
|
|
573
|
-
<a name="RPCError"></a>
|
|
574
|
-
|
|
575
|
-
## RPCError
|
|
576
|
-
Remote Error class for Entangld.
|
|
577
|
-
|
|
578
|
-
**Kind**: global class
|
|
579
|
-
<a name="Entangld_Message"></a>
|
|
580
|
-
|
|
581
|
-
## Entangld\_Message
|
|
582
|
-
Message class for Entangld.
|
|
583
|
-
|
|
584
|
-
These messages are used for executing datastore operations between
|
|
585
|
-
remote datastores. In these relationships, there is always an upstream
|
|
586
|
-
and downstream pair, so that `get`, `set`, `push` and `subscribe` messages
|
|
587
|
-
are created upstream and passed downstream while `event` and `value` messages
|
|
588
|
-
are created downstream and are passed back up. `unsubscribe` events can travel
|
|
589
|
-
in both directions. To maintain consistency, the internal `.path` attribute
|
|
590
|
-
will always refer a path relative to the downstream datastore, since the
|
|
591
|
-
downstream datastores do not necessarilly have access to the upstream store
|
|
592
|
-
structure, and so cannot generally construct upstream paths. This means that
|
|
593
|
-
the `.path` attribute is a `tree` relative to the upstream datastore, and
|
|
594
|
-
the upstream path can be reconstructed as:
|
|
595
|
-
```javascript
|
|
596
|
-
> upstream._namespaces.get(downstream) + "." + msg.path;
|
|
95
|
+
```js
|
|
96
|
+
child.set_rpc("math.divide", (a, b) => {
|
|
97
|
+
if (b === 0) throw new Error("Can't divide by zero");
|
|
98
|
+
return a / b;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await parent.call_rpc("child.math.divide", [ 10, 2 ]); // 5
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
await parent.call_rpc("child.math.divide", [ 10, 0 ]);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.log(err.message); // Can't divide by zero
|
|
107
|
+
}
|
|
597
108
|
```
|
|
598
|
-
Since `unsubscribe` messages can pass either upstream or downstream, the notion
|
|
599
|
-
of a path is ill-defined, and so unsubscribe messages should have their `.path`
|
|
600
|
-
attributes set to undefined or null.
|
|
601
|
-
|
|
602
|
-
Most messages will also have a `.uuid` attribute. For `get`/`value` messages,
|
|
603
|
-
this allows for the value to be properly linked back up with the original `get`
|
|
604
|
-
message. For the `subscribe`/`unsubscribe`/`event` messages, this allows for
|
|
605
|
-
callback functions to be trigger properly, and for unsubscribe messages to
|
|
606
|
-
propogate both directions. `set`/`push` messages do not use the `.uuid` attribute
|
|
607
|
-
since they require no response.
|
|
608
|
-
|
|
609
|
-
**Kind**: global class
|
|
610
|
-
|
|
611
|
-
* [Entangld_Message](#Entangld_Message)
|
|
612
|
-
* [.get(tree, [get_params])](#Entangld_Message.get) ⇒ [<code>Entangld\_Message</code>](#Entangld_Message)
|
|
613
|
-
* [.value(get_msg, value)](#Entangld_Message.value) ⇒ [<code>Entangld\_Message</code>](#Entangld_Message)
|
|
614
|
-
* [.setpush(obj)](#Entangld_Message.setpush) ⇒ [<code>Entangld\_Message</code>](#Entangld_Message)
|
|
615
|
-
* [.subscribe(tree, uuid)](#Entangld_Message.subscribe) ⇒ [<code>Entangld\_Message</code>](#Entangld_Message)
|
|
616
|
-
* [.event(path, value, uuid)](#Entangld_Message.event) ⇒ [<code>Entangld\_Message</code>](#Entangld_Message)
|
|
617
|
-
* [.unsubscribe(uuid)](#Entangld_Message.unsubscribe)
|
|
618
|
-
|
|
619
|
-
<a name="Entangld_Message.get"></a>
|
|
620
|
-
|
|
621
|
-
### Entangld_Message.get(tree, [get_params]) ⇒ [<code>Entangld\_Message</code>](#Entangld_Message)
|
|
622
|
-
Create a `get` message for remote datastores
|
|
623
|
-
|
|
624
|
-
**Kind**: static method of [<code>Entangld\_Message</code>](#Entangld_Message)
|
|
625
|
-
**Returns**: [<code>Entangld\_Message</code>](#Entangld_Message) - - The get message to pass to the remote datastore
|
|
626
|
-
|
|
627
|
-
| Param | Type | Description |
|
|
628
|
-
| --- | --- | --- |
|
|
629
|
-
| tree | <code>string</code> | the path relative to the remote datastore |
|
|
630
|
-
| [get_params] | <code>\*</code> | any parameters to be passed to the remote datastore's local get function |
|
|
631
|
-
|
|
632
|
-
<a name="Entangld_Message.value"></a>
|
|
633
|
-
|
|
634
|
-
### Entangld_Message.value(get_msg, value) ⇒ [<code>Entangld\_Message</code>](#Entangld_Message)
|
|
635
|
-
Create a `value` message in response to a `get` message
|
|
636
|
-
|
|
637
|
-
**Kind**: static method of [<code>Entangld\_Message</code>](#Entangld_Message)
|
|
638
|
-
**Returns**: [<code>Entangld\_Message</code>](#Entangld_Message) - - The `value` message to pass back
|
|
639
|
-
|
|
640
|
-
| Param | Type | Description |
|
|
641
|
-
| --- | --- | --- |
|
|
642
|
-
| get_msg | [<code>Entangld\_Message</code>](#Entangld_Message) | the `get` message which this is in response to |
|
|
643
|
-
| value | | the value of the `get` |
|
|
644
|
-
|
|
645
|
-
<a name="Entangld_Message.setpush"></a>
|
|
646
|
-
|
|
647
|
-
### Entangld_Message.setpush(obj) ⇒ [<code>Entangld\_Message</code>](#Entangld_Message)
|
|
648
|
-
Create a `set`/`push` message for a remote datastore
|
|
649
|
-
|
|
650
|
-
**Kind**: static method of [<code>Entangld\_Message</code>](#Entangld_Message)
|
|
651
|
-
**Returns**: [<code>Entangld\_Message</code>](#Entangld_Message) - - the "set" or "push" message
|
|
652
|
-
|
|
653
|
-
| Param | Type | Description |
|
|
654
|
-
| --- | --- | --- |
|
|
655
|
-
| obj | <code>Object</code> | The parameter object for the set/push |
|
|
656
|
-
| obj.type | <code>string</code> | either "set" or "push" |
|
|
657
|
-
| obj.tree | <code>string</code> | the path (relative to the downstream datastore) |
|
|
658
|
-
| obj.value | <code>\*</code> | the value to insert into the datastore |
|
|
659
|
-
| obj.params | <code>\*</code> | any additional parameters |
|
|
660
|
-
|
|
661
|
-
<a name="Entangld_Message.subscribe"></a>
|
|
662
|
-
|
|
663
|
-
### Entangld_Message.subscribe(tree, uuid) ⇒ [<code>Entangld\_Message</code>](#Entangld_Message)
|
|
664
|
-
Construct subscribe message
|
|
665
|
-
|
|
666
|
-
**Kind**: static method of [<code>Entangld\_Message</code>](#Entangld_Message)
|
|
667
|
-
**Returns**: [<code>Entangld\_Message</code>](#Entangld_Message) - the `subscribe` message
|
|
668
109
|
|
|
669
|
-
|
|
670
|
-
| --- | --- | --- |
|
|
671
|
-
| tree | <code>string</code> | the path (relative to the downstream datastore) |
|
|
672
|
-
| uuid | <code>uuidv4</code> | the subscription uuid |
|
|
110
|
+
### Subscriptions
|
|
673
111
|
|
|
674
|
-
|
|
112
|
+
Watch a path in any linked store. Paths can use `*` as a wildcard.
|
|
675
113
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
**Returns**: [<code>Entangld\_Message</code>](#Entangld_Message) - the `event` message
|
|
681
|
-
|
|
682
|
-
| Param | Type | Description |
|
|
683
|
-
| --- | --- | --- |
|
|
684
|
-
| path | <code>string</code> | the path (relative to the downstream store) |
|
|
685
|
-
| value | <code>\*</code> | the updated datastore value at the path |
|
|
686
|
-
| uuid | <code>uuidv4</code> | the uuid of the subscribe being triggered |
|
|
114
|
+
```js
|
|
115
|
+
const uuid = parent.subscribe("child.motor.speed", (path, value) => {
|
|
116
|
+
console.log(path, value); // child.motor.speed 12
|
|
117
|
+
});
|
|
687
118
|
|
|
688
|
-
|
|
119
|
+
child.set("motor.speed", 12);
|
|
689
120
|
|
|
690
|
-
|
|
691
|
-
Create an unsubscribe message for a subscription uuid
|
|
121
|
+
parent.unsubscribe(uuid);
|
|
692
122
|
|
|
693
|
-
|
|
123
|
+
// Watch everything beneath a path
|
|
124
|
+
parent.subscribe("child.sensors.*", (path, value) => {
|
|
125
|
+
console.log(path, value); // child.sensors.left 3
|
|
126
|
+
});
|
|
694
127
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
| uuid | <code>String</code> | the subscription uuid |
|
|
128
|
+
child.set("sensors.left", 3);
|
|
129
|
+
```
|
|
698
130
|
|
|
699
|
-
|
|
131
|
+
Throttle a busy path, so that the callback only runs for one in every few changes.
|
|
700
132
|
|
|
701
|
-
|
|
702
|
-
|
|
133
|
+
```js
|
|
134
|
+
let count = 0;
|
|
135
|
+
parent.subscribe("child.lidar.range", () => count++, { every: 2 });
|
|
703
136
|
|
|
704
|
-
|
|
137
|
+
for (const range of [ 1, 2, 3, 4 ]) child.set("lidar.range", range);
|
|
705
138
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
* [.is_pass_through](#Subscription+is_pass_through) ⇒ <code>Boolean</code>
|
|
709
|
-
* [.is_terminal](#Subscription+is_terminal) ⇒ <code>Boolean</code>
|
|
710
|
-
* [.is_head](#Subscription+is_head) ⇒ <code>Boolean</code>
|
|
711
|
-
* [.has_downstream](#Subscription+has_downstream) ⇒ <code>Boolean</code>
|
|
712
|
-
* [.has_upstream](#Subscription+has_upstream) ⇒ <code>Boolean</code>
|
|
713
|
-
* [.call()](#Subscription+call)
|
|
714
|
-
* [.matches_subscription(sub)](#Subscription+matches_subscription) ⇒ <code>Boolean</code>
|
|
715
|
-
* [.matches_event_message(msg)](#Subscription+matches_event_message) ⇒ <code>Boolean</code>
|
|
716
|
-
* [.matches_unsubscribe_message(msg)](#Subscription+matches_unsubscribe_message) ⇒ <code>Boolean</code>
|
|
717
|
-
* [.matches_path(path)](#Subscription+matches_path) ⇒ <code>Boolean</code>
|
|
718
|
-
* [.matches_uuid(uuid)](#Subscription+matches_uuid) ⇒ <code>Boolean</code>
|
|
719
|
-
* [.is_beneath(path)](#Subscription+is_beneath) ⇒ <code>Boolean</code>
|
|
720
|
-
* [.is_above(path)](#Subscription+is_above) ⇒ <code>Boolean</code>
|
|
721
|
-
* [.static_copy()](#Subscription+static_copy) ⇒ [<code>Subscription</code>](#Subscription)
|
|
139
|
+
console.log(count); // 2
|
|
140
|
+
```
|
|
722
141
|
|
|
723
|
-
|
|
142
|
+
A store can also see when others subscribe to it, which is useful for only producing data while someone is listening.
|
|
724
143
|
|
|
725
|
-
|
|
726
|
-
|
|
144
|
+
```js
|
|
145
|
+
child.on("subscription", (path, uuid) => console.log("Watching", path)); // Watching camera.frame
|
|
727
146
|
|
|
728
|
-
|
|
147
|
+
parent.subscribe("child.camera.frame", () => { });
|
|
148
|
+
```
|
|
729
149
|
|
|
730
|
-
|
|
731
|
-
| --- | --- | --- |
|
|
732
|
-
| obj | <code>Object</code> | the configuration object |
|
|
733
|
-
| obj.path | <code>string</code> | the datastore path (relative to this datastore) of the subscription |
|
|
734
|
-
| obj.uuid | <code>uuidv4</code> | the uuid of the subscription chain |
|
|
735
|
-
| obj.callback | <code>function</code> | the callback function, with signature (path, value), where path is relative to this datastore |
|
|
736
|
-
| obj.downstream | [<code>Entangld</code>](#Entangld) \| <code>null</code> | the downstream datastore (if any) associated with this subscription |
|
|
737
|
-
| obj.upstream | [<code>Entangld</code>](#Entangld) \| <code>null</code> | the upstream datastore (if any) associated with this subscription |
|
|
738
|
-
| obj.every | <code>number</code> \| <code>null</code> | how many `set` messages to wait before calling callback |
|
|
150
|
+
### Chains of stores
|
|
739
151
|
|
|
740
|
-
|
|
152
|
+
Stores can be linked in any arrangement. Each one only needs to know its neighbours, and paths pass straight through the
|
|
153
|
+
ones in between.
|
|
741
154
|
|
|
742
|
-
|
|
743
|
-
|
|
155
|
+
```js
|
|
156
|
+
const dashboard = new Entangld();
|
|
157
|
+
const robot = new Entangld();
|
|
158
|
+
const motor = new Entangld();
|
|
744
159
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
the callback function is registered, an all others are `path through` datastores
|
|
748
|
-
which simply pass event messages back up to the head subscription.
|
|
160
|
+
const link = (store) => store.transmit((msg, other) => other.receive(msg, store));
|
|
161
|
+
[ dashboard, robot, motor ].forEach(link);
|
|
749
162
|
|
|
750
|
-
|
|
751
|
-
|
|
163
|
+
dashboard.attach("robot", robot);
|
|
164
|
+
robot.attach("motor", motor);
|
|
752
165
|
|
|
753
|
-
|
|
754
|
-
Check if this subscription will be directly given data by a datastore
|
|
166
|
+
motor.set("speed", 2.5);
|
|
755
167
|
|
|
756
|
-
|
|
757
|
-
<a name="Subscription+is_head"></a>
|
|
168
|
+
await dashboard.get("robot.motor.speed"); // 2.5
|
|
758
169
|
|
|
759
|
-
|
|
760
|
-
|
|
170
|
+
dashboard.subscribe("robot.motor.speed", (path, value) => {
|
|
171
|
+
console.log(path, value); // robot.motor.speed 3
|
|
172
|
+
});
|
|
761
173
|
|
|
762
|
-
|
|
763
|
-
|
|
174
|
+
motor.set("speed", 3);
|
|
175
|
+
```
|
|
764
176
|
|
|
765
|
-
|
|
766
|
-
Check if subscription has any downstream subscriptions
|
|
177
|
+
Stores can also be attached deeper than the root, as in `parent.attach("fleet.robots.one", child)`.
|
|
767
178
|
|
|
768
|
-
|
|
769
|
-
getter will return a true. Note that !this.has_downstream will check if
|
|
770
|
-
the subscription is the `tail` subscription object in a subscription chain.
|
|
179
|
+
### Timeouts
|
|
771
180
|
|
|
772
|
-
|
|
773
|
-
|
|
181
|
+
A request waits forever if its reply is lost, for example if a link drops part way through. Set a limit, and it rejects
|
|
182
|
+
with an `EntangldTimeoutError` instead. This works on `get` and `call_rpc`.
|
|
774
183
|
|
|
775
|
-
|
|
776
|
-
|
|
184
|
+
```js
|
|
185
|
+
const { EntangldTimeoutError } = Entangld.error;
|
|
777
186
|
|
|
778
|
-
|
|
779
|
-
getter will return a true.
|
|
187
|
+
child.set("sensor.stuck", () => new Promise(() => { }));
|
|
780
188
|
|
|
781
|
-
|
|
782
|
-
|
|
189
|
+
try {
|
|
190
|
+
await parent.get("child.sensor.stuck", undefined, { timeout_ms: 100 });
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.log(err instanceof EntangldTimeoutError); // true
|
|
193
|
+
}
|
|
194
|
+
```
|
|
783
195
|
|
|
784
|
-
###
|
|
785
|
-
Apply this callback function
|
|
196
|
+
### Cancelling with signals
|
|
786
197
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
this method will only call the callback function every `this.every`
|
|
791
|
-
times it receives a `set` message. If this subscription is not
|
|
792
|
-
terminal, then the callback function is called every time.
|
|
198
|
+
An [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) ties a request or a subscription to
|
|
199
|
+
anything else, like a job finishing or a link dropping. An aborted request rejects with `signal.reason`, and an aborted
|
|
200
|
+
subscription ends.
|
|
793
201
|
|
|
794
|
-
|
|
795
|
-
|
|
202
|
+
```js
|
|
203
|
+
const controller = new AbortController();
|
|
204
|
+
const { signal } = controller;
|
|
796
205
|
|
|
797
|
-
|
|
798
|
-
<a name="Subscription+matches_subscription"></a>
|
|
206
|
+
child.set("job.slow", () => new Promise(() => { }));
|
|
799
207
|
|
|
800
|
-
|
|
801
|
-
Check if a different `Subscription` object matches this subscription
|
|
208
|
+
const job = parent.get("child.job.slow", undefined, { signal });
|
|
802
209
|
|
|
803
|
-
|
|
804
|
-
|
|
210
|
+
parent.subscribe("child.job.progress", (path, value) => {
|
|
211
|
+
console.log("Progress", value); // Progress 50
|
|
212
|
+
}, { signal });
|
|
805
213
|
|
|
806
|
-
|
|
807
|
-
| --- | --- | --- |
|
|
808
|
-
| sub | [<code>Subscription</code>](#Subscription) | a different subscription |
|
|
214
|
+
child.set("job.progress", 50);
|
|
809
215
|
|
|
810
|
-
|
|
216
|
+
// Ends the request and the subscription together
|
|
217
|
+
controller.abort(new Error("No longer needed"));
|
|
811
218
|
|
|
812
|
-
|
|
813
|
-
|
|
219
|
+
try {
|
|
220
|
+
await job;
|
|
221
|
+
} catch (err) {
|
|
222
|
+
console.log(err.message); // No longer needed
|
|
223
|
+
}
|
|
814
224
|
|
|
815
|
-
|
|
816
|
-
|
|
225
|
+
child.set("job.progress", 100); // Nobody is told
|
|
226
|
+
```
|
|
817
227
|
|
|
818
|
-
|
|
819
|
-
| --- | --- | --- |
|
|
820
|
-
| msg | [<code>Entangld\_Message</code>](#Entangld_Message) | a received message from a downstream datastore |
|
|
228
|
+
### Over sockets
|
|
821
229
|
|
|
822
|
-
|
|
230
|
+
Each side of a socket gets its own store. The socket stands in for the store on the other side.
|
|
823
231
|
|
|
824
|
-
|
|
825
|
-
|
|
232
|
+
```js
|
|
233
|
+
const Entangld = require("entangld");
|
|
234
|
+
const Sockhop = require("sockhop");
|
|
826
235
|
|
|
827
|
-
|
|
828
|
-
|
|
236
|
+
// The server side
|
|
237
|
+
const parent = new Entangld();
|
|
238
|
+
const server = new Sockhop.server();
|
|
829
239
|
|
|
830
|
-
|
|
831
|
-
| --- | --- | --- |
|
|
832
|
-
| msg | [<code>Entangld\_Message</code>](#Entangld_Message) | a received message from a downstream datastore |
|
|
240
|
+
parent.transmit((msg, socket) => server.send(socket, msg));
|
|
833
241
|
|
|
834
|
-
|
|
242
|
+
server.on("receive", (data, meta) => parent.receive(data, meta.socket));
|
|
243
|
+
server.on("disconnect", (socket) => parent.detach(null, socket));
|
|
244
|
+
server.on("connect", async (socket) => {
|
|
245
|
+
parent.attach("client", socket); // With several clients, use a unique name for each one
|
|
835
246
|
|
|
836
|
-
|
|
837
|
-
|
|
247
|
+
console.log(await parent.get("client.my.name")); // Entangld
|
|
248
|
+
});
|
|
838
249
|
|
|
839
|
-
|
|
840
|
-
**Returns**: <code>Boolean</code> - - true if the path matches
|
|
250
|
+
await server.listen();
|
|
841
251
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
252
|
+
// The client side
|
|
253
|
+
const child = new Entangld();
|
|
254
|
+
const client = new Sockhop.client();
|
|
845
255
|
|
|
846
|
-
|
|
256
|
+
child.transmit((msg) => client.send(msg));
|
|
257
|
+
client.on("receive", (data) => child.receive(data));
|
|
847
258
|
|
|
848
|
-
|
|
849
|
-
Check if a provided uuid matches this uuid
|
|
259
|
+
child.set("my.name", "Entangld");
|
|
850
260
|
|
|
851
|
-
|
|
852
|
-
|
|
261
|
+
await client.connect();
|
|
262
|
+
```
|
|
853
263
|
|
|
854
|
-
|
|
855
|
-
| --- | --- | --- |
|
|
856
|
-
| uuid | <code>uuidv4</code> | a uuid string to check against |
|
|
264
|
+
## Good to know
|
|
857
265
|
|
|
858
|
-
|
|
266
|
+
- **Timeouts and aborts only stop the wait.** They don't stop work which has already started in another store, so an RPC
|
|
267
|
+
which timed out may still have run. Take care before retrying one.
|
|
268
|
+
- **Choose a signal or `unsubscribe()` for a subscription, not both.** `unsubscribe()` throws if the subscription has
|
|
269
|
+
already ended, including when its signal aborted, even if that was before `subscribe()` was called. If you do mix them,
|
|
270
|
+
check `signal.aborted` first, or catch the error.
|
|
271
|
+
- **Unsubscribing by path ends every subscription on that path.** To end one, use the uuid from `subscribe()`. To end
|
|
272
|
+
everything beneath a path, use `unsubscribe_tree()`. The `subscriptions` getter lists what a store has.
|
|
273
|
+
- **Attached stores are placeholders in their parent.** Getting a parent path shows an attached store as an empty object.
|
|
274
|
+
Fetching the whole tree would need a request to every store. Query the attached path to see its contents.
|
|
275
|
+
- **Functions with parameters should have defaults.** In deref mode, functions are called with no arguments.
|
|
859
276
|
|
|
860
|
-
|
|
861
|
-
Check if subscription path is beneath a provided path
|
|
277
|
+
## Deref mode
|
|
862
278
|
|
|
863
|
-
|
|
864
|
-
|
|
279
|
+
A function normally runs only when its own path is read, so reading its parent doesn't include it. In deref mode, reading
|
|
280
|
+
a parent runs the functions beneath it too, and waits for any promises.
|
|
865
281
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
| path | <code>String</code> | a path string to check against |
|
|
282
|
+
```js
|
|
283
|
+
const store = new Entangld({ deref_mode: true });
|
|
869
284
|
|
|
870
|
-
|
|
285
|
+
store.set("number.six", 6);
|
|
286
|
+
store.set("number.seven", () => 7);
|
|
871
287
|
|
|
872
|
-
|
|
873
|
-
|
|
288
|
+
await store.get("number"); // { six: 6, seven: 7 }
|
|
289
|
+
```
|
|
874
290
|
|
|
875
|
-
|
|
876
|
-
|
|
291
|
+
This has two costs. Results are copied, which is slower, and the copy goes through JSON, so values like a `Map` are lost.
|
|
292
|
+
Over a socket that rarely matters, but it can for local stores holding values which aren't plain JSON.
|
|
877
293
|
|
|
878
|
-
|
|
879
|
-
| --- | --- | --- |
|
|
880
|
-
| path | <code>String</code> | a path string to check against |
|
|
294
|
+
## Documentation
|
|
881
295
|
|
|
882
|
-
|
|
296
|
+
- [API reference](API.md)
|
|
297
|
+
- [Protocol](docs/protocol.md), a draft, including changes under consideration
|
|
883
298
|
|
|
884
|
-
|
|
885
|
-
Get a copy of this subscription without external references
|
|
299
|
+
## Known limitations
|
|
886
300
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
301
|
+
- `detach()` doesn't end subscriptions from or through that store, and doesn't touch requests waiting on it.
|
|
302
|
+
- Querying a parent can't include the contents of attached stores.
|
|
303
|
+
- Deref mode turns values into JSON and back, which is inefficient and loses values like `Map`.
|
|
304
|
+
- Some unit tests depend on the state left by earlier tests.
|
|
305
|
+
- `unsubscribe_tree()` needs a test that it throws when it can't remove every subscription.
|
|
890
306
|
|
|
891
|
-
|
|
892
|
-
**Returns**: [<code>Subscription</code>](#Subscription) - a copy of this subscription object
|
|
307
|
+
## Development
|
|
893
308
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
- Fix unit tests so they are completely independent (tests often depend on prior test ending with a particular state)
|
|
899
|
-
- 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)
|
|
900
|
-
- unsubscribe_tree() needs to have a test ensuring errors are thrown if not all subs can be removed
|
|
309
|
+
```sh
|
|
310
|
+
npm test # Run the tests
|
|
311
|
+
npm run docs # Regenerate API.md from the source
|
|
312
|
+
```
|
|
901
313
|
|
|
902
314
|
## License
|
|
903
|
-
|
|
315
|
+
|
|
316
|
+
[MIT](LICENSE)
|