entangld 2.4.1 → 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/README.md CHANGED
@@ -1,903 +1,316 @@
1
1
  # Entangld
2
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")!)
3
+ [![Tests](https://github.com/Daxbot/entangld/actions/workflows/mocha-tests.yml/badge.svg)](https://github.com/Daxbot/entangld/actions/workflows/mocha-tests.yml)
4
+ [![npm](https://img.shields.io/npm/v/entangld)](https://www.npmjs.com/package/entangld)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
4
6
 
5
- ## Tests
7
+ Synchronized key-value stores with RPCs and pub/sub events, in pure JavaScript with no runtime dependencies.
6
8
 
7
- ![example workflow](https://github.com/Daxbot/entangld/actions/workflows/mocha-tests.yml/badge.svg)
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
- ## Examples
10
- Basic use:
11
- ```js
12
- let e=new Entangld();
13
-
14
- // Simple set/get
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
- // Functions as values
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
- // Promises from functions
24
- e.set("number.eight",()=>{ return new Promise((resolve)=>{ resolve(8); }); });
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
- Pairing two data stores together:
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
- // Configure communications
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
- // Assign a function to a child key
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
- // Call the RPC from the parent
57
- parent.get("child.double.me", 2).then((val)=>{
33
+ const parent = new Entangld();
34
+ const child = new Entangld();
58
35
 
59
- // val == 4
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
- 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.
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
- // Throttle the subscription callbacks, so that the callback is only called every 2 sets
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
- parent.attach("client", sock); // "client" works for one client. Normally use uuid() or something
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
- parent.get("client.my.name")
114
- .then((val)=>{
51
+ ## Examples
115
52
 
116
- console.log("Client's name is "+val);
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
- let child=new Entangld();
130
- let client=new Sockhop.client();
60
+ await store.get("number.six"); // 6
61
+ await store.get("number"); // { six: 6 }
131
62
 
132
- // Connect client to child store
133
- child.transmit((msg)=>client.send(msg));
134
- client
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
- child.set("my.name", "Entangld");
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
- ## Raison d'etre
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
- ## Notes
79
+ ```js
80
+ child.set("sensor.read", async () => ({ temperature: 21 }));
81
+ child.set("math.double", (value) => value * 2);
155
82
 
156
- - 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.
157
- - 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()```
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
- parent.get(""); // Returns { "parent" : "data", "child" : {} }
167
- parent.get("child"); // Returns {"child" : "data"}
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"> &gt; upstream._namespaces.get(downstream) + &quot;.&quot; + 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.&lt;Subscription&gt;</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.&lt;Subscription&gt;</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.&lt;Subscription&gt;</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>&quot;\&quot;set\&quot;&quot;</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
- **Kind**: instance method of [<code>Entangld</code>](#Entangld)
437
- **Returns**: <code>Promise</code> - promise resolving to return value of the rpc
438
- **Throws**:
90
+ ### RPCs
439
91
 
440
- - [<code>RPCError</code>](#RPCError) if the rpc call experienced an error
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
- | Param | Type | Default | Description |
444
- | --- | --- | --- | --- |
445
- | path | <code>string</code> | | the path to query (like "system.voltage"). |
446
- | [args] | <code>Array</code> | <code>[]</code> | the arguments to pass to the RPC. |
447
-
448
- <a name="Entangld+get"></a>
449
-
450
- ### entangld.get(path, [params]) ⇒ <code>Promise</code>
451
- Get an object from the store.
452
-
453
- Note: using max_depth, especially large max_depth, involves a lot of
454
- recursion and may be expensive.
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
- | Param | Type | Description |
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
- <a name="Entangld_Message.event"></a>
112
+ Watch a path in any linked store. Paths can use `*` as a wildcard.
675
113
 
676
- ### Entangld_Message.event(path, value, uuid) ⇒ [<code>Entangld\_Message</code>](#Entangld_Message)
677
- Create an `event` message to return data to subscribe callbacks
678
-
679
- **Kind**: static method of [<code>Entangld\_Message</code>](#Entangld_Message)
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
- <a name="Entangld_Message.unsubscribe"></a>
119
+ child.set("motor.speed", 12);
689
120
 
690
- ### Entangld_Message.unsubscribe(uuid)
691
- Create an unsubscribe message for a subscription uuid
121
+ parent.unsubscribe(uuid);
692
122
 
693
- **Kind**: static method of [<code>Entangld\_Message</code>](#Entangld_Message)
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
- | Param | Type | Description |
696
- | --- | --- | --- |
697
- | uuid | <code>String</code> | the subscription uuid |
128
+ child.set("sensors.left", 3);
129
+ ```
698
130
 
699
- <a name="Subscription"></a>
131
+ Throttle a busy path, so that the callback only runs for one in every few changes.
700
132
 
701
- ## Subscription
702
- A datastore subscription object
133
+ ```js
134
+ let count = 0;
135
+ parent.subscribe("child.lidar.range", () => count++, { every: 2 });
703
136
 
704
- **Kind**: global class
137
+ for (const range of [ 1, 2, 3, 4 ]) child.set("lidar.range", range);
705
138
 
706
- * [Subscription](#Subscription)
707
- * [new Subscription(obj)](#new_Subscription_new)
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
- <a name="new_Subscription_new"></a>
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
- ### new Subscription(obj)
726
- Constructor
144
+ ```js
145
+ child.on("subscription", (path, uuid) => console.log("Watching", path)); // Watching camera.frame
727
146
 
728
- **Returns**: [<code>Subscription</code>](#Subscription) - - the subscription object
147
+ parent.subscribe("child.camera.frame", () => { });
148
+ ```
729
149
 
730
- | Param | Type | Description |
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
- <a name="Subscription+is_pass_through"></a>
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
- ### subscription.is\_pass\_through ⇒ <code>Boolean</code>
743
- Check if subscription is a `pass through` type
155
+ ```js
156
+ const dashboard = new Entangld();
157
+ const robot = new Entangld();
158
+ const motor = new Entangld();
744
159
 
745
- Pass throughs are as the links in a chain of subscriptions to allows
746
- subscriptions to remote datastores. One store acts as the `head`, where
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
- **Kind**: instance property of [<code>Subscription</code>](#Subscription)
751
- <a name="Subscription+is_terminal"></a>
163
+ dashboard.attach("robot", robot);
164
+ robot.attach("motor", motor);
752
165
 
753
- ### subscription.is\_terminal ⇒ <code>Boolean</code>
754
- Check if this subscription will be directly given data by a datastore
166
+ motor.set("speed", 2.5);
755
167
 
756
- **Kind**: instance property of [<code>Subscription</code>](#Subscription)
757
- <a name="Subscription+is_head"></a>
168
+ await dashboard.get("robot.motor.speed"); // 2.5
758
169
 
759
- ### subscription.is\_head ⇒ <code>Boolean</code>
760
- Check if this subscription will apply a user-supplied callback to data
170
+ dashboard.subscribe("robot.motor.speed", (path, value) => {
171
+ console.log(path, value); // robot.motor.speed 3
172
+ });
761
173
 
762
- **Kind**: instance property of [<code>Subscription</code>](#Subscription)
763
- <a name="Subscription+has_downstream"></a>
174
+ motor.set("speed", 3);
175
+ ```
764
176
 
765
- ### subscription.has\_downstream ⇒ <code>Boolean</code>
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
- It the subscription refers to a remote datastore (the downstream), this
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
- **Kind**: instance property of [<code>Subscription</code>](#Subscription)
773
- <a name="Subscription+has_upstream"></a>
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
- ### subscription.has\_upstream ⇒ <code>Boolean</code>
776
- Check if subscription has any upstream subscriptions
184
+ ```js
185
+ const { EntangldTimeoutError } = Entangld.error;
777
186
 
778
- It the subscription passes data back to a remote datastore (the upstream), this
779
- getter will return a true.
187
+ child.set("sensor.stuck", () => new Promise(() => { }));
780
188
 
781
- **Kind**: instance property of [<code>Subscription</code>](#Subscription)
782
- <a name="Subscription+call"></a>
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
- ### subscription.call()
785
- Apply this callback function
196
+ ### Cancelling with signals
786
197
 
787
- Note, this method also tracks the number of times that a callback
788
- function is called (if this subscription is terminal), so that if
789
- the subscriptions are throttled by specifying an `this.every`,
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
- This method also is safed when a callback function is not give (i.e.
795
- by the `this.static_copy()` method).
202
+ ```js
203
+ const controller = new AbortController();
204
+ const { signal } = controller;
796
205
 
797
- **Kind**: instance method of [<code>Subscription</code>](#Subscription)
798
- <a name="Subscription+matches_subscription"></a>
206
+ child.set("job.slow", () => new Promise(() => { }));
799
207
 
800
- ### subscription.matches\_subscription(sub) ⇒ <code>Boolean</code>
801
- Check if a different `Subscription` object matches this subscription
208
+ const job = parent.get("child.job.slow", undefined, { signal });
802
209
 
803
- **Kind**: instance method of [<code>Subscription</code>](#Subscription)
804
- **Returns**: <code>Boolean</code> - - True if the subscriptions match
210
+ parent.subscribe("child.job.progress", (path, value) => {
211
+ console.log("Progress", value); // Progress 50
212
+ }, { signal });
805
213
 
806
- | Param | Type | Description |
807
- | --- | --- | --- |
808
- | sub | [<code>Subscription</code>](#Subscription) | a different subscription |
214
+ child.set("job.progress", 50);
809
215
 
810
- <a name="Subscription+matches_event_message"></a>
216
+ // Ends the request and the subscription together
217
+ controller.abort(new Error("No longer needed"));
811
218
 
812
- ### subscription.matches\_event\_message(msg) ⇒ <code>Boolean</code>
813
- Check if an `event` message matches this subscription
219
+ try {
220
+ await job;
221
+ } catch (err) {
222
+ console.log(err.message); // No longer needed
223
+ }
814
224
 
815
- **Kind**: instance method of [<code>Subscription</code>](#Subscription)
816
- **Returns**: <code>Boolean</code> - - True if the message is associated with the subscription
225
+ child.set("job.progress", 100); // Nobody is told
226
+ ```
817
227
 
818
- | Param | Type | Description |
819
- | --- | --- | --- |
820
- | msg | [<code>Entangld\_Message</code>](#Entangld_Message) | a received message from a downstream datastore |
228
+ ### Over sockets
821
229
 
822
- <a name="Subscription+matches_unsubscribe_message"></a>
230
+ Each side of a socket gets its own store. The socket stands in for the store on the other side.
823
231
 
824
- ### subscription.matches\_unsubscribe\_message(msg) ⇒ <code>Boolean</code>
825
- Check if an `unsubscribe` message matches this subscription
232
+ ```js
233
+ const Entangld = require("entangld");
234
+ const Sockhop = require("sockhop");
826
235
 
827
- **Kind**: instance method of [<code>Subscription</code>](#Subscription)
828
- **Returns**: <code>Boolean</code> - - True if the message is associated with the subscription
236
+ // The server side
237
+ const parent = new Entangld();
238
+ const server = new Sockhop.server();
829
239
 
830
- | Param | Type | Description |
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
- <a name="Subscription+matches_path"></a>
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
- ### subscription.matches\_path(path) ⇒ <code>Boolean</code>
837
- Check if a provided path matches this path
247
+ console.log(await parent.get("client.my.name")); // Entangld
248
+ });
838
249
 
839
- **Kind**: instance method of [<code>Subscription</code>](#Subscription)
840
- **Returns**: <code>Boolean</code> - - true if the path matches
250
+ await server.listen();
841
251
 
842
- | Param | Type | Description |
843
- | --- | --- | --- |
844
- | path | <code>String</code> | a path string to check against |
252
+ // The client side
253
+ const child = new Entangld();
254
+ const client = new Sockhop.client();
845
255
 
846
- <a name="Subscription+matches_uuid"></a>
256
+ child.transmit((msg) => client.send(msg));
257
+ client.on("receive", (data) => child.receive(data));
847
258
 
848
- ### subscription.matches\_uuid(uuid) ⇒ <code>Boolean</code>
849
- Check if a provided uuid matches this uuid
259
+ child.set("my.name", "Entangld");
850
260
 
851
- **Kind**: instance method of [<code>Subscription</code>](#Subscription)
852
- **Returns**: <code>Boolean</code> - - true if the path matches
261
+ await client.connect();
262
+ ```
853
263
 
854
- | Param | Type | Description |
855
- | --- | --- | --- |
856
- | uuid | <code>uuidv4</code> | a uuid string to check against |
264
+ ## Good to know
857
265
 
858
- <a name="Subscription+is_beneath"></a>
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
- ### subscription.is\_beneath(path) ⇒ <code>Boolean</code>
861
- Check if subscription path is beneath a provided path
277
+ ## Deref mode
862
278
 
863
- **Kind**: instance method of [<code>Subscription</code>](#Subscription)
864
- **Returns**: <code>Boolean</code> - - true if the subscription is beneath the path
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
- | Param | Type | Description |
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
- <a name="Subscription+is_above"></a>
285
+ store.set("number.six", 6);
286
+ store.set("number.seven", () => 7);
871
287
 
872
- ### subscription.is\_above(path) ⇒ <code>Boolean</code>
873
- Check if subscription path is above a provided path
288
+ await store.get("number"); // { six: 6, seven: 7 }
289
+ ```
874
290
 
875
- **Kind**: instance method of [<code>Subscription</code>](#Subscription)
876
- **Returns**: <code>Boolean</code> - - true if the subscription is beneath the path
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
- | Param | Type | Description |
879
- | --- | --- | --- |
880
- | path | <code>String</code> | a path string to check against |
294
+ ## Documentation
881
295
 
882
- <a name="Subscription+static_copy"></a>
296
+ - [API reference](API.md)
297
+ - [Protocol](docs/protocol.md), a draft, including changes under consideration
883
298
 
884
- ### subscription.static\_copy() ⇒ [<code>Subscription</code>](#Subscription)
885
- Get a copy of this subscription without external references
299
+ ## Known limitations
886
300
 
887
- This creates a copy, except the upstream/downstream references
888
- are set to true (if they exist) or null (if they don't. Addtionally,
889
- the callback function is excluded.
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
- **Kind**: instance method of [<code>Subscription</code>](#Subscription)
892
- **Returns**: [<code>Subscription</code>](#Subscription) - a copy of this subscription object
307
+ ## Development
893
308
 
894
- ## TODO
895
- - Make sure incoming values request store doesn't build up
896
- - When querying a parent, perhaps there should be an option to also dump child stores located below that level (potentially resource intensive)
897
- - 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)
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
- MIT
315
+
316
+ [MIT](LICENSE)