entangld 2.1.6 → 2.3.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/.eslintrc.json +29 -0
- package/README.hbs +1 -0
- package/README.md +490 -421
- package/generate_docs.js +3 -3
- package/index.js +8 -1333
- package/lib/Datastore.js +855 -0
- package/lib/Message.js +148 -0
- package/lib/Subscription.js +216 -0
- package/lib/error.js +43 -0
- package/lib/utils.js +263 -0
- package/package.json +8 -9
- package/test/circular.js +3 -7
- package/test/events.js +39 -0
- package/test/sockets.js +19 -19
- package/test/subscriptions.js +51 -51
- package/test/tests.js +382 -383
package/index.js
CHANGED
|
@@ -1,1335 +1,10 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* remote datastores. In these relationships, there is always an upstream
|
|
10
|
-
* and downstream pair, so that `get`, `set`, `push` and `subscribe` messages
|
|
11
|
-
* are created upstream and passed downstream while `event` and `value` messages
|
|
12
|
-
* are created downstream and are passed back up. `unsubscribe` events can travel
|
|
13
|
-
* in both directions. To maintain consistency, the internal `.path` attribute
|
|
14
|
-
* will always refer a path relative to the downstream datastore, since the
|
|
15
|
-
* downstream datastores do not necessarilly have access to the upstream store
|
|
16
|
-
* structure, and so cannot generally construct upstream paths. This means that
|
|
17
|
-
* the `.path` attribute is a `tree` relative to the upstream datastore, and
|
|
18
|
-
* the upstream path can be reconstructed as:
|
|
19
|
-
* ```javascript
|
|
20
|
-
* > upstream._namespaces.get(downstream) + "." + msg.path;
|
|
21
|
-
* ```
|
|
22
|
-
* Since `unsubscribe` messages can pass either upstream or downstream, the notion
|
|
23
|
-
* of a path is ill-defined, and so unsubscribe messages should have their `.path`
|
|
24
|
-
* attributes set to undefined or null.
|
|
25
|
-
*
|
|
26
|
-
* Most messages will also have a `.uuid` attribute. For `get`/`value` messages,
|
|
27
|
-
* this allows for the value to be properly linked back up with the original `get`
|
|
28
|
-
* message. For the `subscribe`/`unsubscribe`/`event` messages, this allows for
|
|
29
|
-
* callback functions to be trigger properly, and for unsubscribe messages to
|
|
30
|
-
* propogate both directions. `set`/`push` messages do not use the `.uuid` attribute
|
|
31
|
-
* since they require no response.
|
|
32
|
-
*/
|
|
33
|
-
class Entangld_Message {
|
|
34
|
-
|
|
35
|
-
constructor({type, path, value, uuid, params, every}) {
|
|
36
|
-
|
|
37
|
-
this.type = type;
|
|
38
|
-
this.path = path;
|
|
39
|
-
this.value = value;
|
|
40
|
-
this.uuid = uuid;
|
|
41
|
-
this.params = params;
|
|
42
|
-
this.every = every;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ----------------------
|
|
46
|
-
// Class Methods
|
|
47
|
-
// ----------------------
|
|
48
|
-
/**
|
|
49
|
-
* Create a `get` message for remote datastores
|
|
50
|
-
*
|
|
51
|
-
* @param {string} tree - the path relative to the remote datastore
|
|
52
|
-
* @param {*} [get_params=undefined] - any parameters to be passed to the
|
|
53
|
-
* remote datastore's local get function
|
|
54
|
-
* @return {Entangld_Message} - The get message to pass to the remote datastore
|
|
55
|
-
*/
|
|
56
|
-
static get(tree, get_params) {
|
|
57
|
-
return new this({
|
|
58
|
-
type: "get",
|
|
59
|
-
path : tree, // path needs to be relative to downstream store
|
|
60
|
-
value : get_params,
|
|
61
|
-
uuid : uuidv4() // Gets generate a new uuid
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Create a `value` message in response to a `get` message
|
|
67
|
-
*
|
|
68
|
-
* @param {Entangld_Message} get_msg - the `get` message which this is in response to
|
|
69
|
-
* @param value - the value of the `get`
|
|
70
|
-
* @return {Entangld_Message} - The `value` message to pass back
|
|
71
|
-
*/
|
|
72
|
-
static value(get_msg, value) {
|
|
73
|
-
return new this({
|
|
74
|
-
type: "value",
|
|
75
|
-
path : get_msg.path, // Resond with the get's path, which is relative to this store
|
|
76
|
-
value : value,
|
|
77
|
-
uuid : get_msg.uuid // Respond with the get's uuid
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Create a `set`/`push` message for a remote datastore
|
|
83
|
-
*
|
|
84
|
-
* @param {Object} obj - The parameter object for the set/push
|
|
85
|
-
* @param {string} obj.type - either "set" or "push"
|
|
86
|
-
* @param {string} obj.tree - the path (relative to the downstream datastore)
|
|
87
|
-
* @param {*} obj.value - the value to insert into the datastore
|
|
88
|
-
* @param {*} obj.params - any additional parameters
|
|
89
|
-
* @return {Entangld_Message} - the "set" or "push" message
|
|
90
|
-
*/
|
|
91
|
-
static setpush({type = "set", tree, value, params}) {
|
|
92
|
-
if (!["set", "push"].includes(type)) { throw new Error(`Invalid type (${type}) for setpush`); }
|
|
93
|
-
return new this({
|
|
94
|
-
type: type,
|
|
95
|
-
path : tree,
|
|
96
|
-
value : value,
|
|
97
|
-
params : params
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Construct subscribe message
|
|
103
|
-
*
|
|
104
|
-
* @param {string} tree - the path (relative to the downstream datastore)
|
|
105
|
-
* @param {uuidv4} uuid - the subscription uuid
|
|
106
|
-
* @return {Entangld_Message} the `subscribe` message
|
|
107
|
-
*/
|
|
108
|
-
static subscribe(tree, uuid, every = null) {
|
|
109
|
-
return new this({
|
|
110
|
-
type : "subscribe",
|
|
111
|
-
path : tree,
|
|
112
|
-
uuid : uuid,
|
|
113
|
-
every : every
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Create an `event` message to return data to subscribe callbacks
|
|
119
|
-
*
|
|
120
|
-
* @param {string} path - the path (relative to the downstream store)
|
|
121
|
-
* @param {*} value - the updated datastore value at the path
|
|
122
|
-
* @param {uuidv4} uuid - the uuid of the subscribe being triggered
|
|
123
|
-
* @return {Entangld_Message} the `event` message
|
|
124
|
-
*/
|
|
125
|
-
static event(path, value, uuid) {
|
|
126
|
-
return new this({
|
|
127
|
-
type : "event",
|
|
128
|
-
path : path,
|
|
129
|
-
value : value,
|
|
130
|
-
uuid : uuid
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Create an unsubscribe message for a subscription uuid
|
|
137
|
-
*
|
|
138
|
-
* @param {String} uuid - the subscription uuid
|
|
139
|
-
*/
|
|
140
|
-
static unsubscribe(uuid) {
|
|
141
|
-
return new this({
|
|
142
|
-
type: "unsubscribe",
|
|
143
|
-
// path is ommitted from unsubscribe messages
|
|
144
|
-
uuid : uuid
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Error class for Entangld.
|
|
152
|
-
*/
|
|
153
|
-
class EntangldError extends Error {
|
|
154
|
-
constructor(message) {
|
|
155
|
-
super(message);
|
|
156
|
-
|
|
157
|
-
this.name = this.constructor.name;
|
|
158
|
-
Error.captureStackTrace(this, this.constructor);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* A datastore subscription object
|
|
165
|
-
*/
|
|
166
|
-
class Subscription {
|
|
167
|
-
/**
|
|
168
|
-
* Constructor
|
|
169
|
-
*
|
|
170
|
-
* @param {Object} obj - the configuration object
|
|
171
|
-
* @param {string} obj.path - the datastore path (relative to this datastore)
|
|
172
|
-
* of the subscription
|
|
173
|
-
* @param {uuidv4} obj.uuid - the uuid of the subscription chain
|
|
174
|
-
* @param {function} obj.callback - the callback function, with signature (path, value),
|
|
175
|
-
* where path is relative to this datastore
|
|
176
|
-
* @param {(Entangld|null)} obj.downstream - the downstream datastore (if any)
|
|
177
|
-
* associated with this subscription
|
|
178
|
-
* @param {(Entangld|null)} obj.upstream - the upstream datastore (if any)
|
|
179
|
-
* associated with this subscription
|
|
180
|
-
* @param {(number|null)} obj.every - how many `set` messages to wait before calling callback
|
|
181
|
-
* @return {Subscription} - the subscription object
|
|
182
|
-
*/
|
|
183
|
-
constructor({path, uuid, callback, downstream, upstream, every}) {
|
|
184
|
-
this.path = path;
|
|
185
|
-
this.downstream = downstream || null;
|
|
186
|
-
this.upstream = upstream || null;
|
|
187
|
-
this.uuid = uuid;
|
|
188
|
-
this.callback = callback;
|
|
189
|
-
this._has_cb = (typeof(callback) === "function");
|
|
190
|
-
this._is_counting = (this.downstream === null);
|
|
191
|
-
every = parseInt(every);
|
|
192
|
-
if ( isNaN(every) || every < 1 ) {
|
|
193
|
-
this.every = 1;
|
|
194
|
-
} else {
|
|
195
|
-
this.every = every;
|
|
196
|
-
}
|
|
197
|
-
this.index = -1; // guarentee that the first `set` triggers
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Apply this callback function
|
|
202
|
-
*
|
|
203
|
-
* Note, this method also tracks the number of times that a callback
|
|
204
|
-
* function is called (if this subscription is terminal), so that if
|
|
205
|
-
* the subscriptions are throttled by specifying an `this.every`,
|
|
206
|
-
* this method will only call the callback function every `this.every`
|
|
207
|
-
* times it receives a `set` message. If this subscription is not
|
|
208
|
-
* terminal, then the callback function is called every time.
|
|
209
|
-
*
|
|
210
|
-
* This method also is safed when a callback function is not give (i.e.
|
|
211
|
-
* by the `this.static_copy()` method).
|
|
212
|
-
*/
|
|
213
|
-
call(...args) {
|
|
214
|
-
if ( !this._has_cb ) return;
|
|
215
|
-
if ( this._is_counting ) {
|
|
216
|
-
this.index = (this.index + 1) % this.every;
|
|
217
|
-
if ( this.index !== 0 ) return;
|
|
218
|
-
}
|
|
219
|
-
this.callback(...args);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Check if subscription is a `pass through` type
|
|
224
|
-
*
|
|
225
|
-
* Pass throughs are as the links in a chain of subscriptions to allows
|
|
226
|
-
* subscriptions to remote datastores. One store acts as the `head`, where
|
|
227
|
-
* the callback function is registered, an all others are `path through` datastores
|
|
228
|
-
* which simply pass event messages back up to the head subscription.
|
|
229
|
-
*
|
|
230
|
-
* @return {Boolean}
|
|
231
|
-
*/
|
|
232
|
-
get is_pass_through() {
|
|
233
|
-
return this.has_upstream;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Check if this subscription will be directly given data by a datastore
|
|
238
|
-
*
|
|
239
|
-
* @return {Boolean}
|
|
240
|
-
*/
|
|
241
|
-
get is_terminal() {
|
|
242
|
-
return !this.has_downstream;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Check if this subscription will apply a user-supplied callback to data
|
|
247
|
-
*
|
|
248
|
-
* @return {Boolean}
|
|
249
|
-
*/
|
|
250
|
-
get is_head() {
|
|
251
|
-
return !this.has_upstream;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Check if subscription has any downstream subscriptions
|
|
256
|
-
*
|
|
257
|
-
* It the subscription refers to a remote datastore (the downstream), this
|
|
258
|
-
* getter will return a true. Note that !this.has_downstream will check if
|
|
259
|
-
* the subscription is the `tail` subscription object in a subscription chain.
|
|
260
|
-
*
|
|
261
|
-
* @return {Boolean}
|
|
262
|
-
*/
|
|
263
|
-
get has_downstream() {
|
|
264
|
-
return this.downstream !== null;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Check if subscription has any upstream subscriptions
|
|
269
|
-
*
|
|
270
|
-
* It the subscription passes data back to a remote datastore (the upstream), this
|
|
271
|
-
* getter will return a true.
|
|
272
|
-
*
|
|
273
|
-
* @return {Boolean}
|
|
274
|
-
*/
|
|
275
|
-
get has_upstream() {
|
|
276
|
-
return this.upstream !== null;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Check if a different `Subscription` object matches this subscription
|
|
282
|
-
*
|
|
283
|
-
* @param {Subscription} sub - a different subscription
|
|
284
|
-
* @return {Boolean} - True if the subscriptions match
|
|
285
|
-
*/
|
|
286
|
-
matches_subscription(sub) {
|
|
287
|
-
return this.matches_uuid(sub.uuid) && this.matches_path(sub.path);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Check if an `event` message matches this subscription
|
|
292
|
-
*
|
|
293
|
-
* @param {Entangld_Message} msg - a received message from a downstream datastore
|
|
294
|
-
* @return {Boolean} - True if the message is associated with the subscription
|
|
295
|
-
*/
|
|
296
|
-
matches_event_message(msg) {
|
|
297
|
-
// should be above, otherwise events below a subscription won't
|
|
298
|
-
// trigger the callback. I.e. subscribing to `flower` won't
|
|
299
|
-
// trigger when someone sets `flower.color`
|
|
300
|
-
return this.matches_uuid(msg.uuid) && this.is_above(msg.path);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Check if an `unsubscribe` message matches this subscription
|
|
305
|
-
*
|
|
306
|
-
* @param {Entangld_Message} msg - a received message from a downstream datastore
|
|
307
|
-
* @return {Boolean} - True if the message is associated with the subscription
|
|
308
|
-
*/
|
|
309
|
-
matches_unsubscribe_message(msg) {
|
|
310
|
-
return this.matches_uuid(msg.uuid);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Check if a provided path matches this path
|
|
315
|
-
*
|
|
316
|
-
* @param {String} path - a path string to check against
|
|
317
|
-
* @return {Boolean} - true if the path matches
|
|
318
|
-
*/
|
|
319
|
-
matches_path(path) {
|
|
320
|
-
return this.path === path;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Check if a provided uuid matches this uuid
|
|
325
|
-
*
|
|
326
|
-
* @param {uuidv4} uuid - a uuid string to check against
|
|
327
|
-
* @return {Boolean} - true if the path matches
|
|
328
|
-
*/
|
|
329
|
-
matches_uuid(uuid) {
|
|
330
|
-
return this.uuid === uuid;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Check if subscription path is beneath a provided path
|
|
335
|
-
*
|
|
336
|
-
* @param {String} path - a path string to check against
|
|
337
|
-
* @return {Boolean} - true if the subscription is beneath the path
|
|
338
|
-
*/
|
|
339
|
-
is_beneath(path) {
|
|
340
|
-
return is_beneath(this.path, path)
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Check if subscription path is above a provided path
|
|
345
|
-
*
|
|
346
|
-
* @param {String} path - a path string to check against
|
|
347
|
-
* @return {Boolean} - true if the subscription is beneath the path
|
|
348
|
-
*/
|
|
349
|
-
is_above(path) {
|
|
350
|
-
return is_beneath(path, this.path);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Get a copy of this subscription without external references
|
|
355
|
-
*
|
|
356
|
-
* This creates a copy, except the upstream/downstream references
|
|
357
|
-
* are set to true (if they exist) or null (if they don't. Addtionally,
|
|
358
|
-
* the callback function is excluded.
|
|
359
|
-
*
|
|
360
|
-
* @return {Subscription} a copy of this subscription object
|
|
361
|
-
*/
|
|
362
|
-
static_copy() {
|
|
363
|
-
return new Subscription({
|
|
364
|
-
path : this.path,
|
|
365
|
-
uuid : this.uuid,
|
|
366
|
-
downstream : (this.downstream === null ? null : true),
|
|
367
|
-
upstream : (this.upstream === null ? null : true),
|
|
368
|
-
every : this.every
|
|
369
|
-
})
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Deep copy an object
|
|
376
|
-
*
|
|
377
|
-
* Uses JSON parse methods so things that don't work in JSON will disappear,
|
|
378
|
-
* with the special exception of functions which are replaced by their return
|
|
379
|
-
* values (or if the function returns a promise, the value that it resolves to).
|
|
380
|
-
*
|
|
381
|
-
* If you pass undefined, it will return a promise resolving to undefined.
|
|
382
|
-
*
|
|
383
|
-
* @private
|
|
384
|
-
* @param {object} original the object to copy.
|
|
385
|
-
* @return {Promise} a promise resolving to a completely new, de-referenced
|
|
386
|
-
* object containing only objects and values.
|
|
387
|
-
*/
|
|
388
|
-
function dereferenced_copy(original) {
|
|
389
|
-
|
|
390
|
-
// Undefined passes through
|
|
391
|
-
if (original === undefined)
|
|
392
|
-
return new Promise((res) => { res(undefined); });
|
|
393
|
-
|
|
394
|
-
// Make a copy of o. It will be missing any functions
|
|
395
|
-
let copy = JSON.parse(JSON.stringify(original));
|
|
396
|
-
|
|
397
|
-
// A container to hold the copy (so we can have a recursive function
|
|
398
|
-
// replace it using a key)
|
|
399
|
-
let container = { "copy": copy };
|
|
400
|
-
|
|
401
|
-
// We will store our promises here
|
|
402
|
-
let promises = [];
|
|
403
|
-
|
|
404
|
-
// Recursively call all functions in o, placing their values in copy.
|
|
405
|
-
// Promises are tracked also.
|
|
406
|
-
function recurse(o, c, parent, key) {
|
|
407
|
-
|
|
408
|
-
// If o is a function, get its value and return a promise
|
|
409
|
-
if (typeof (o) == "function") {
|
|
410
|
-
|
|
411
|
-
// Call the function
|
|
412
|
-
let result = o();
|
|
413
|
-
|
|
414
|
-
// If the function itself returns a promise, save it and make it
|
|
415
|
-
// replace the object with a result
|
|
416
|
-
if (result && result.constructor && result.constructor.name
|
|
417
|
-
&& result.constructor.name == "Promise") {
|
|
418
|
-
|
|
419
|
-
promises.push(promises, result);
|
|
420
|
-
result.then((val) => {
|
|
421
|
-
|
|
422
|
-
parent[key] = val;
|
|
423
|
-
});
|
|
424
|
-
} else {
|
|
425
|
-
|
|
426
|
-
// Replace the object directly
|
|
427
|
-
parent[key] = result;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
// If o is not an object, return
|
|
435
|
-
if (typeof (o) != "object") return;
|
|
436
|
-
|
|
437
|
-
// Otherwise, iterate keys and call ourselves recursively
|
|
438
|
-
for (let key in o) {
|
|
439
|
-
|
|
440
|
-
recurse(o[key], c[key], c, key);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
recurse(original, copy, container, "copy");
|
|
445
|
-
|
|
446
|
-
// Wait for all promises to fulfil, then return the copy
|
|
447
|
-
return Promise.all(promises).then(() => Promise.resolve(copy));
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Extract from path.
|
|
453
|
-
*
|
|
454
|
-
* Given an object, extracts a child object using a string path.
|
|
455
|
-
*
|
|
456
|
-
* @private
|
|
457
|
-
* @param {object} obj the object we are extracting from.
|
|
458
|
-
* @param {string} path the path to find beneath obj.
|
|
459
|
-
* @return {Array<object, string>} array containing [result, remaining_path].
|
|
460
|
-
* result will be undefined if path can't be found. If we encounter a function
|
|
461
|
-
* in our search, result will be the function and remaining_path will be the
|
|
462
|
-
* remaining part of path.
|
|
463
|
-
*/
|
|
464
|
-
function extract_from_path(obj, path) {
|
|
465
|
-
|
|
466
|
-
// Empty path means get everything
|
|
467
|
-
if (path === "") return [obj, ""];
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
let keys = path.split(".");
|
|
471
|
-
let key;
|
|
472
|
-
let o = obj;
|
|
473
|
-
|
|
474
|
-
while ((key = keys.shift())) {
|
|
475
|
-
|
|
476
|
-
if (key in o) {
|
|
477
|
-
|
|
478
|
-
o = o[key];
|
|
479
|
-
|
|
480
|
-
// If o is a function, we can't go any farther
|
|
481
|
-
if (typeof (o) == "function") {
|
|
482
|
-
|
|
483
|
-
return [o, keys.join(".")];
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
} else {
|
|
487
|
-
|
|
488
|
-
return [undefined, ""];
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
return [o, ""];
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* Partial copy.
|
|
498
|
-
*
|
|
499
|
-
* Return a partial copy of an object (depth limited). DOES NOT DE-REFERENCE!
|
|
500
|
-
*
|
|
501
|
-
* @private
|
|
502
|
-
* @param {object} object the object to copy
|
|
503
|
-
* @param {number} max_depth the maximum depth to copy (0 returns object keys)
|
|
504
|
-
* @return {object} partial copy of an object limited by max_depth.
|
|
505
|
-
*/
|
|
506
|
-
function partial_copy(o, max_depth) {
|
|
507
|
-
|
|
508
|
-
// Trivial case, return object untouched if no max_depth
|
|
509
|
-
if (typeof (max_depth) != "number") return o;
|
|
510
|
-
|
|
511
|
-
// If o is not an object, return it
|
|
512
|
-
if (typeof (o) != "object") return o;
|
|
513
|
-
|
|
514
|
-
let c = (Array.isArray(o)) ? [] : {};
|
|
515
|
-
|
|
516
|
-
// If max_depth has been exceeded, return the empty object or array
|
|
517
|
-
if (max_depth < 0) return c;
|
|
518
|
-
|
|
519
|
-
// Otherwise, iterate keys and call ourselves recursively
|
|
520
|
-
for (let key in o) {
|
|
521
|
-
|
|
522
|
-
if (typeof (o[key]) != "object") {
|
|
523
|
-
|
|
524
|
-
c[key] = o[key];
|
|
525
|
-
|
|
526
|
-
} else {
|
|
527
|
-
|
|
528
|
-
c[key] = partial_copy(o[key], max_depth - 1);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
return c;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Is beneath
|
|
537
|
-
*
|
|
538
|
-
* Is `a` under `b`? E.g. is "system.bus.voltage" equal to or beneath "system.bus"?
|
|
539
|
-
*
|
|
540
|
-
* @private
|
|
541
|
-
* @param {string} a the string tested for "insideness"
|
|
542
|
-
* @param {string} b the string tested for "outsideness"
|
|
543
|
-
* @return boolean
|
|
544
|
-
*/
|
|
545
|
-
function is_beneath(a, b) {
|
|
546
|
-
|
|
547
|
-
// Everything is beneath the top ("")
|
|
548
|
-
if(b==="") return true;
|
|
549
|
-
|
|
550
|
-
// If paths are both blank, they are equal
|
|
551
|
-
if(b==="" && a==="") return true;
|
|
552
|
-
|
|
553
|
-
let A=a.split(".");
|
|
554
|
-
let B=b.split(".");
|
|
555
|
-
|
|
556
|
-
// A is not beneath B if any part is not the same
|
|
557
|
-
while(A.length && B.length){
|
|
558
|
-
|
|
559
|
-
if(A.shift()!=B.shift()) return false;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// A is not beneath B if B is longer
|
|
563
|
-
if(B.length) return false;
|
|
564
|
-
|
|
565
|
-
return true;
|
|
566
|
-
}
|
|
567
|
-
/**
|
|
568
|
-
* Synchronized Event Store
|
|
569
|
-
*
|
|
570
|
-
* @extends EventEmitter
|
|
571
|
-
*/
|
|
572
|
-
class Entangld extends EventEmitter {
|
|
573
|
-
|
|
574
|
-
constructor() {
|
|
575
|
-
super();
|
|
576
|
-
|
|
577
|
-
this._stores = new Map();
|
|
578
|
-
this._namespaces = new Map();
|
|
579
|
-
|
|
580
|
-
this._transmit = () => { };
|
|
581
|
-
this._local_data = {};
|
|
582
|
-
this._requests = {};
|
|
583
|
-
this._subscriptions = [];
|
|
584
|
-
|
|
585
|
-
// These are global but need to be accessible for various reasons
|
|
586
|
-
// (i.e. testing)
|
|
587
|
-
this._partial_copy = partial_copy;
|
|
588
|
-
this._dereferenced_copy = dereferenced_copy;
|
|
589
|
-
|
|
590
|
-
// If this is turned on, when someone .get()s an object we will query
|
|
591
|
-
// its children to see if they are functions. We will then call those
|
|
592
|
-
// functions and return their values. This should be the default
|
|
593
|
-
// behavior, but it is rather expensive. TODO
|
|
594
|
-
this._deref_mode = false;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/**
|
|
598
|
-
* Get namespaces
|
|
599
|
-
*
|
|
600
|
-
* @readonly
|
|
601
|
-
* @return {array} namespaces - an array of attached namespaces
|
|
602
|
-
*/
|
|
603
|
-
get namespaces() {
|
|
604
|
-
|
|
605
|
-
return Array.from(this._stores.keys());
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
/**
|
|
609
|
-
* Get list of subscriptions associated with this object
|
|
610
|
-
*
|
|
611
|
-
* Note, this will include `head`, `terminal` and `pass through` subscriptions,
|
|
612
|
-
* which can be checked using getter methods of the subscription object.
|
|
613
|
-
*
|
|
614
|
-
* @readonly
|
|
615
|
-
* @return {Subscription[]} array of Subscriptions associated with this object
|
|
616
|
-
*/
|
|
617
|
-
get subscriptions() {
|
|
618
|
-
return this._subscriptions.map(v => v.static_copy());
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
/**
|
|
622
|
-
* Get namespace for a store
|
|
623
|
-
*
|
|
624
|
-
* @readonly
|
|
625
|
-
* @return {string} namespace for the given store
|
|
626
|
-
*/
|
|
627
|
-
namespace(store) {
|
|
628
|
-
|
|
629
|
-
// return this._namespaces(store);
|
|
630
|
-
return this._namespaces.get(store);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/**
|
|
634
|
-
* Attach a namespace and a store
|
|
635
|
-
*
|
|
636
|
-
* @param {string} namespace a namespace for this store.
|
|
637
|
-
* @param {object} obj an object that will be sent along with "transmit"
|
|
638
|
-
* callbacks when we need something from this store.
|
|
639
|
-
* @throws {TypeError} if namespace/obj is null or empty.
|
|
640
|
-
* @throws {EntangldError} if you try to attach to the same namespace twice.
|
|
641
|
-
*/
|
|
642
|
-
attach(namespace, obj) {
|
|
643
|
-
|
|
644
|
-
// Sanity checks
|
|
645
|
-
if (!namespace)
|
|
646
|
-
throw TypeError("You cannot attach() a null or empty namespace");
|
|
647
|
-
|
|
648
|
-
if (!obj)
|
|
649
|
-
throw TypeError("You cannot attach() a null or empty object");
|
|
650
|
-
|
|
651
|
-
if (this._stores.has(namespace))
|
|
652
|
-
throw new EntangldError("You already attach()ed that namespace");
|
|
653
|
-
|
|
654
|
-
// Attach the store and namespace
|
|
655
|
-
this._stores.set(namespace, obj);
|
|
656
|
-
this._namespaces.set(obj, namespace);
|
|
657
|
-
|
|
658
|
-
// Create an empty local node so that this attach point is visible
|
|
659
|
-
this._set_local(namespace, {});
|
|
660
|
-
|
|
661
|
-
// Find and update any subscriptions that fall beneath the new namespace
|
|
662
|
-
const subscriptions = this._subscriptions.filter(
|
|
663
|
-
s => s.is_beneath(namespace)
|
|
664
|
-
);
|
|
665
|
-
|
|
666
|
-
// Clean up the old entries
|
|
667
|
-
this._unsubscribe(subscriptions);
|
|
668
|
-
|
|
669
|
-
// Re-subscribe
|
|
670
|
-
subscriptions.forEach(s => {
|
|
671
|
-
this._subscribe(s.path, s.callback, s.upstream, s.uuid, s.every);
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/**
|
|
676
|
-
* Detach a namespace / obj pair.
|
|
677
|
-
*
|
|
678
|
-
* If you only pass a namespace or a store, it will find the missing item
|
|
679
|
-
* before detaching.
|
|
680
|
-
*
|
|
681
|
-
* @param {string} [namespace] the namespace.
|
|
682
|
-
* @param {object} [obj] the store object.
|
|
683
|
-
* @return {boolean} true if the element existed and was removed.
|
|
684
|
-
* @throws {EntangldError} Error will be thrown if you don't pass at least
|
|
685
|
-
* one parameter.
|
|
686
|
-
*/
|
|
687
|
-
detach(namespace, obj) {
|
|
688
|
-
|
|
689
|
-
if (!namespace && !obj) {
|
|
690
|
-
const msg = "You must specify at least one of either namespace or "
|
|
691
|
-
+ "object when calling detach()"
|
|
692
|
-
throw new EntangldError(msg);
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
if (!obj) obj = this._stores.get(namespace);
|
|
696
|
-
if (!namespace) namespace = this._namespaces.get(obj);
|
|
697
|
-
|
|
698
|
-
// Detach this namespace placeholder from the local store
|
|
699
|
-
this._set_local(namespace, undefined);
|
|
700
|
-
|
|
701
|
-
return this._stores.delete(namespace)
|
|
702
|
-
&& this._namespaces.delete(obj);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
/**
|
|
706
|
-
* Transmit
|
|
707
|
-
*
|
|
708
|
-
* Specify a callback to be used so we can transmit data to another store.
|
|
709
|
-
* Callback will be passed (message, obj) where 'message' is an
|
|
710
|
-
* Entangld_Message object and obj is the object provided by attach().
|
|
711
|
-
*
|
|
712
|
-
* @param {function} func the callback function.
|
|
713
|
-
* @throws {TypeError} if func is not a function.
|
|
714
|
-
*/
|
|
715
|
-
transmit(func) {
|
|
716
|
-
if (typeof (func) != "function")
|
|
717
|
-
throw TypeError("func must be a function")
|
|
718
|
-
|
|
719
|
-
this._transmit = func;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
/**
|
|
723
|
-
* Receive
|
|
724
|
-
*
|
|
725
|
-
* Call this function with the data that was sent via the transmit()
|
|
726
|
-
* callback.
|
|
727
|
-
*
|
|
728
|
-
* @param {Entangld_Message} msg the message to process.
|
|
729
|
-
* @param {object} obj the attach() object where the message originted.
|
|
730
|
-
*
|
|
731
|
-
* @throws {ReferenceError} if event object was not provided.
|
|
732
|
-
* @throws {EntangldError} if an unknown message type was received.
|
|
733
|
-
*/
|
|
734
|
-
receive(msg, obj) {
|
|
735
|
-
|
|
736
|
-
if (msg.type == "set" || msg.type == "push") {
|
|
737
|
-
// Remote "set" request
|
|
738
|
-
this.set(msg.path, msg.value, msg.type, msg.params);
|
|
739
|
-
|
|
740
|
-
} else if (msg.type == "get") {
|
|
741
|
-
// Remote "get" request
|
|
742
|
-
this.get(msg.path, msg.value).then((val) => {
|
|
743
|
-
|
|
744
|
-
const response = Entangld_Message.value(msg,val)
|
|
745
|
-
|
|
746
|
-
this._transmit(response, obj);
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
} else if (msg.type == "value") {
|
|
750
|
-
// Incoming value reply
|
|
751
|
-
let resolve = this._requests[msg.uuid];
|
|
752
|
-
resolve(msg.value);
|
|
753
|
-
|
|
754
|
-
//Clean up request
|
|
755
|
-
delete this._requests[msg.uuid];
|
|
756
|
-
|
|
757
|
-
} else if (msg.type == "event") {
|
|
758
|
-
// Incoming event
|
|
759
|
-
if (obj === undefined)
|
|
760
|
-
throw new ReferenceError("receive() called without object");
|
|
761
|
-
|
|
762
|
-
// From our perspective the path is now prepended with the namespace
|
|
763
|
-
msg.path = this._namespaces.get(obj) + "." + msg.path;
|
|
764
|
-
|
|
765
|
-
// Find and dispatch any subscriptions
|
|
766
|
-
let count = 0;
|
|
767
|
-
for (let s of this._subscriptions) {
|
|
768
|
-
|
|
769
|
-
if (s.matches_event_message(msg)) {
|
|
770
|
-
|
|
771
|
-
// Call the callback
|
|
772
|
-
s.call(msg.path, msg.value);
|
|
773
|
-
count++;
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// No one is listening. This may happen if an event triggers while
|
|
778
|
-
// we are still unsubscribing, or if a downstream subscription gets
|
|
779
|
-
// orphaned
|
|
780
|
-
if (count === 0) {
|
|
781
|
-
|
|
782
|
-
// Reply with unsubscribe request
|
|
783
|
-
const response = Entangld_Message.unsubscribe(msg.uuid);
|
|
784
|
-
this._transmit(response, obj);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
} else if (msg.type == "subscribe") {
|
|
788
|
-
// Incoming remote subscription request
|
|
789
|
-
|
|
790
|
-
// Create a new subscription that simply transmits when triggered
|
|
791
|
-
// the "tree" from the subscribe message is scoped to be a "path"
|
|
792
|
-
// here in this datastore
|
|
793
|
-
this._subscribe(msg.path, (path, val) => {
|
|
794
|
-
// The "path" here is relative to this datastore, and can
|
|
795
|
-
// potentially be beneath msg.params.tree
|
|
796
|
-
const response = Entangld_Message.event(path,val,msg.uuid);
|
|
797
|
-
this._transmit(response, obj);
|
|
798
|
-
}, obj, msg.uuid, msg.every);
|
|
799
|
-
|
|
800
|
-
} else if (msg.type == "unsubscribe") {
|
|
801
|
-
// Incoming remote unsubscribe request
|
|
802
|
-
|
|
803
|
-
// Unsubscribe to any matching subscriptions.
|
|
804
|
-
this._unsubscribe(this._subscriptions.filter(
|
|
805
|
-
s => s.matches_unsubscribe_message(msg))
|
|
806
|
-
);
|
|
807
|
-
|
|
808
|
-
} else {
|
|
809
|
-
// Default
|
|
810
|
-
throw new EntangldError(
|
|
811
|
-
"Received unknown message: " + JSON.stringify(msg));
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
/**
|
|
817
|
-
* Push an object into an array in the store.
|
|
818
|
-
*
|
|
819
|
-
* Convenience method for set(path, o, "push").
|
|
820
|
-
*
|
|
821
|
-
* @param {string} path the path to set (like "system.fan.voltage").
|
|
822
|
-
* @param {object} data the object or function you want to store at path.
|
|
823
|
-
* @param {number} [limit] maximum size of the array. Older entries will be
|
|
824
|
-
* removed until the array size is less than or equal to limit.
|
|
825
|
-
*
|
|
826
|
-
* @throws {TypeError} if path is not a string.
|
|
827
|
-
*/
|
|
828
|
-
push(path, data, limit=null) {
|
|
829
|
-
|
|
830
|
-
this.set(path, data, "push", { limit });
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
/**
|
|
836
|
-
* Set an object into the store
|
|
837
|
-
*
|
|
838
|
-
* @param {string} path the path to set (like "system.fan.voltage").
|
|
839
|
-
* @param {object} data the object or function you want to store at path.
|
|
840
|
-
* @param {string} [operation_type="set"] whether to set or push the new
|
|
841
|
-
* data (push only works if the data item exists and is an array).
|
|
842
|
-
* @param {object} [params] additional parameters.
|
|
843
|
-
*
|
|
844
|
-
* @throws {TypeError} if path is not a string.
|
|
845
|
-
*/
|
|
846
|
-
set(path, data, operation_type="set", params={}) {
|
|
847
|
-
|
|
848
|
-
// Sanity check
|
|
849
|
-
if (typeof (path) != "string")
|
|
850
|
-
throw TypeError("path must be a string");
|
|
851
|
-
|
|
852
|
-
let [obj, , tree] = this._get_remote_object(path);
|
|
853
|
-
|
|
854
|
-
if (obj === undefined) {
|
|
855
|
-
// Set local or remote item
|
|
856
|
-
|
|
857
|
-
// Is this going to mess with an attached store?
|
|
858
|
-
for (let [namespace] of this._stores) {
|
|
859
|
-
|
|
860
|
-
if (is_beneath(namespace, path)) {
|
|
861
|
-
|
|
862
|
-
const msg = `Cannot set ${path} - doing so would overwrite `
|
|
863
|
-
+ `remote store attached at ${path}. `
|
|
864
|
-
+ "Please detach first"
|
|
865
|
-
throw new EntangldError(msg);
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
this._set_local(path, data, operation_type, params);
|
|
870
|
-
|
|
871
|
-
// Check subscriptions to see if we need to run an event
|
|
872
|
-
for (let s of this._subscriptions) {
|
|
873
|
-
|
|
874
|
-
// is this broken? if path="data.element" and s.path="data",
|
|
875
|
-
// then the callback will get triggered, but with the sub path,
|
|
876
|
-
// i.e. cb("data.element",data), except, the callbacks often
|
|
877
|
-
// don't check the path, so it will think it is getting a 'data'
|
|
878
|
-
// object, not a "data.path" object . . .
|
|
879
|
-
if (s.is_above(path)) s.call(path, data)
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
} else {
|
|
883
|
-
|
|
884
|
-
const msg = Entangld_Message.setpush({
|
|
885
|
-
type : operation_type,
|
|
886
|
-
tree : tree,
|
|
887
|
-
value : data,
|
|
888
|
-
params : params
|
|
889
|
-
})
|
|
890
|
-
|
|
891
|
-
this._transmit(msg, obj);
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
/**
|
|
898
|
-
* Get the remote store in a path
|
|
899
|
-
*
|
|
900
|
-
* If path contains a remote store, return the object as well as the
|
|
901
|
-
* relative path to the remote store
|
|
902
|
-
*
|
|
903
|
-
* Example: let A,B be stores. If A.attach("some.path.to.B", B) then
|
|
904
|
-
* _get_remote_object("some.path.to.B.data") will return
|
|
905
|
-
* [B, "some.path.to.B", "data"] and _get_remote_object("nonexistent.path")
|
|
906
|
-
* will return [undefined, undefined, path]
|
|
907
|
-
*
|
|
908
|
-
* @private
|
|
909
|
-
* @param {string} path the path to investigate.
|
|
910
|
-
*
|
|
911
|
-
* @return {array<object, string, string>} array whose elements are
|
|
912
|
-
* [obj, namespace, relative path below store]. If no store is found,
|
|
913
|
-
* return value is [undefined, undefined, path].
|
|
914
|
-
*/
|
|
915
|
-
_get_remote_object(path) {
|
|
916
|
-
|
|
917
|
-
// We don't know what part of path might be a store key. So we need to
|
|
918
|
-
// get all store keys and search (vs .get)
|
|
919
|
-
for (let [namespace, obj] of this._stores) {
|
|
920
|
-
|
|
921
|
-
if (is_beneath(path, namespace)) {
|
|
922
|
-
|
|
923
|
-
// Exact match means path is the root of the attached store
|
|
924
|
-
if (path.length == namespace.length) {
|
|
925
|
-
|
|
926
|
-
return [obj, namespace, ""];
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
// Path is longer than namespace. Make sure we match to a period
|
|
930
|
-
if (path.substr(namespace.length, 1) == ".") {
|
|
931
|
-
|
|
932
|
-
path = path.substr(namespace.length + 1)
|
|
933
|
-
return [obj, namespace, path];
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// Default: we did not find a store
|
|
939
|
-
return [undefined, undefined, path];
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
/**
|
|
943
|
-
* Get an object from the store.
|
|
944
|
-
*
|
|
945
|
-
* Note: using max_depth, especially large max_depth, involves a lot of
|
|
946
|
-
* recursion and may be expensive.
|
|
947
|
-
*
|
|
948
|
-
* @param {string} path the path to query (like "system.voltage").
|
|
949
|
-
* @param {object} [params] the parameters to be passed to the remote
|
|
950
|
-
* function (RPC) or the maximum depth of the returned object (normal mode).
|
|
951
|
-
*
|
|
952
|
-
* @throws {TypeError} if path is not a string.
|
|
953
|
-
* @return {Promise} promise resolving to the object at that path.
|
|
954
|
-
*/
|
|
955
|
-
get(path, params) {
|
|
956
|
-
// Sanity check
|
|
957
|
-
if (typeof (path) != "string")
|
|
958
|
-
throw TypeError("path must be a string");
|
|
959
|
-
|
|
960
|
-
// If an attach()ed store path masks (matches but is shorter than) the
|
|
961
|
-
// path, we are returning that store
|
|
962
|
-
let [obj, , tree] = this._get_remote_object(path);
|
|
963
|
-
|
|
964
|
-
// If object is undefined, we are getting a local item
|
|
965
|
-
if (obj === undefined) {
|
|
966
|
-
|
|
967
|
-
// Get a reference to the object
|
|
968
|
-
let [data, remaining_path] = extract_from_path(
|
|
969
|
-
this._local_data, path);
|
|
970
|
-
|
|
971
|
-
// If it is a function, call it
|
|
972
|
-
if (typeof (data) == "function") {
|
|
973
|
-
|
|
974
|
-
let result = data(params);
|
|
975
|
-
|
|
976
|
-
// Get a promise resolving to the result
|
|
977
|
-
let p = (result && result.constructor && result.constructor.name
|
|
978
|
-
&& result.constructor.name == "Promise") ?
|
|
979
|
-
result : new Promise((res) => res(result));
|
|
980
|
-
|
|
981
|
-
return p.then((val) => {
|
|
982
|
-
|
|
983
|
-
// If we have a remaining path, try to navigate it before
|
|
984
|
-
// returning
|
|
985
|
-
[data, remaining_path] = extract_from_path(
|
|
986
|
-
val, remaining_path);
|
|
987
|
-
|
|
988
|
-
return Promise.resolve(
|
|
989
|
-
(remaining_path === "")? data : undefined);
|
|
990
|
-
});
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
// Path does not point to function. So use params as a max depth
|
|
994
|
-
// and return a Promise resolving to a dereferenced partial copy
|
|
995
|
-
if (typeof (params) == "number") {
|
|
996
|
-
|
|
997
|
-
if (this._deref_mode) {
|
|
998
|
-
|
|
999
|
-
return dereferenced_copy(partial_copy(data, params));
|
|
1000
|
-
} else {
|
|
1001
|
-
|
|
1002
|
-
return new Promise(
|
|
1003
|
-
(res) => res(partial_copy(data, params)));
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
// Default: return a promise resolving to the entire object as-is
|
|
1008
|
-
if (this._deref_mode) {
|
|
1009
|
-
|
|
1010
|
-
return dereferenced_copy(data);
|
|
1011
|
-
|
|
1012
|
-
} else {
|
|
1013
|
-
|
|
1014
|
-
return new Promise((res) => res(data));
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
// Request the data from the remote store
|
|
1019
|
-
const msg = Entangld_Message.get(tree, params);
|
|
1020
|
-
|
|
1021
|
-
return new Promise((res) => {
|
|
1022
|
-
this._requests[msg.uuid] = res;
|
|
1023
|
-
this._transmit(msg, obj);
|
|
1024
|
-
});
|
|
1025
|
-
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
/**
|
|
1029
|
-
* Subscribe to change events for a path
|
|
1030
|
-
*
|
|
1031
|
-
* If objects at or below this path change, you will get a callback
|
|
1032
|
-
*
|
|
1033
|
-
* Subscriptions to keys within attach()ed stores are remote subscriptions.
|
|
1034
|
-
* If several stores are attached in some kind of arrangement, a given key
|
|
1035
|
-
* may actually traverse multiple stores! Since each store only knows its
|
|
1036
|
-
* immediate neighbors - and has no introspection into those neighbors - each
|
|
1037
|
-
* store is only able to keeps track of the neighbor on each side with
|
|
1038
|
-
* respect to a particular path and has no knowledge of the eventual
|
|
1039
|
-
* endpoints. This means that subscribing across several datstores is accomplished
|
|
1040
|
-
* by daisy-chaining 2-way subscriptions across each datastore interface.
|
|
1041
|
-
*
|
|
1042
|
-
* For example, let's suppose capital letters represent Entangld stores and
|
|
1043
|
-
* lowercase letters are actual objects. Then the path "A.B.c.d.E.F.g.h"
|
|
1044
|
-
* will represent a subscription that traverses four Entangld stores.
|
|
1045
|
-
* From the point of view of a store in the middle - say, E - the "upstream"
|
|
1046
|
-
* is B and the "downstream" is F.
|
|
1047
|
-
*
|
|
1048
|
-
* Each store involved keeps track of any subscriptions with which it is
|
|
1049
|
-
* involved. It tracks the upstream and downstream, and the uuid of the
|
|
1050
|
-
* subscription. The uuid is the same across all stores for a given
|
|
1051
|
-
* subscription. For a particular store, the upstream is null if it is the
|
|
1052
|
-
* original link in the chain (called the `head`), and the downstream is
|
|
1053
|
-
* null if this store owns the endpoint value (called the `tail`). Any
|
|
1054
|
-
* subscription which is not the head of a chain is called a `pass through`
|
|
1055
|
-
* subscription, because it exist only to pass `event` messages back up the
|
|
1056
|
-
* chain to the head (where the user-provided callback function exists).
|
|
1057
|
-
* subscriptions can be checked to see if they are `pass through` type via
|
|
1058
|
-
* the getter `sub.is_pass_through`.
|
|
1059
|
-
*
|
|
1060
|
-
* @param {string} path the path to watch.
|
|
1061
|
-
* @param {function} func the callback - will be of the form (path, value).
|
|
1062
|
-
* @param {number|null} [every=null] the number of `set` messages to wait before calling callback
|
|
1063
|
-
*
|
|
1064
|
-
* @throws {TypeError} if path is not a string.
|
|
1065
|
-
* @return {uuidv4} - the uuid of the subscription
|
|
1066
|
-
*/
|
|
1067
|
-
subscribe(path, func, every = null) {
|
|
1068
|
-
|
|
1069
|
-
return this._subscribe(path, func, null, null, every);
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
/**
|
|
1073
|
-
* Create a subscription
|
|
1074
|
-
*
|
|
1075
|
-
* This is an internal function that allows us to specify the new
|
|
1076
|
-
* subscription's upstream and UUID.
|
|
1077
|
-
*
|
|
1078
|
-
* @private
|
|
1079
|
-
* @param {string} path the path to watch.
|
|
1080
|
-
* @param {function} func the callback - will be of the form (path, value).
|
|
1081
|
-
* @param {(Entangld|null)} [upstream=null] the Engangld next upstream in the path.
|
|
1082
|
-
* @param {(uuidv4|null)} [uuid=null] the UUID to use for this subscription.
|
|
1083
|
-
*
|
|
1084
|
-
* @emits {str} subscription - when this datastore is the terminal datastore of a
|
|
1085
|
-
* subscription request, this datastore emits the path
|
|
1086
|
-
* and uuid.
|
|
1087
|
-
* @throws {TypeError} if path is not a string.
|
|
1088
|
-
* @return {uuidv4} - the uuid of the subscription
|
|
1089
|
-
*/
|
|
1090
|
-
_subscribe(path, func, upstream = null, uuid = null, every = null) {
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
// Supply a UUID if none provided
|
|
1094
|
-
uuid = uuid || uuidv4();
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
// Sanity check
|
|
1098
|
-
if (typeof (path) != "string")
|
|
1099
|
-
throw TypeError("path must be a string");
|
|
1100
|
-
|
|
1101
|
-
let [obj, /*namespace*/, tree] = this._get_remote_object(path);
|
|
1102
|
-
|
|
1103
|
-
// Create the subscription object
|
|
1104
|
-
let new_sub = new Subscription({
|
|
1105
|
-
path : path,
|
|
1106
|
-
downstream : obj,
|
|
1107
|
-
upstream : upstream,
|
|
1108
|
-
uuid : uuid,
|
|
1109
|
-
callback : func,
|
|
1110
|
-
every : every
|
|
1111
|
-
})
|
|
1112
|
-
// Filter out any residual subscriptions
|
|
1113
|
-
// Note, residual subscriptions should only exist if a remote datastore (connected via a
|
|
1114
|
-
// socket, or something like that) re-attaches this datastore after the connection between
|
|
1115
|
-
// the two was uncerimonally destroyed, otherwise the internal clean up of the attach
|
|
1116
|
-
// should have propegated down to prevent this.
|
|
1117
|
-
// Note, this should still allow circular references, since "match_subscriptions" requires
|
|
1118
|
-
// both the uuid and path to match
|
|
1119
|
-
this._subscriptions = this._subscriptions.filter(sub => !new_sub.matches_subscription(sub));
|
|
1120
|
-
|
|
1121
|
-
// Add the new subscription here
|
|
1122
|
-
this._subscriptions.push(new_sub);
|
|
1123
|
-
|
|
1124
|
-
// Commission any subscribes downstream, if this is a remote subscription
|
|
1125
|
-
if (new_sub.has_downstream) {
|
|
1126
|
-
const msg = Entangld_Message.subscribe(tree, uuid, every);
|
|
1127
|
-
this._transmit(msg, obj);
|
|
1128
|
-
|
|
1129
|
-
} else { // this is the terminal datastore, so emit subscription received
|
|
1130
|
-
this.emit("subscription", path, uuid)
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
return new_sub.uuid
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
/**
|
|
1137
|
-
* Check for subscription
|
|
1138
|
-
*
|
|
1139
|
-
* Are we subscribed to a particular remote path?
|
|
1140
|
-
*
|
|
1141
|
-
* @param {String} subscription the subscription to check for.
|
|
1142
|
-
*
|
|
1143
|
-
* @return {Boolean} true if we are subscribed.
|
|
1144
|
-
*/
|
|
1145
|
-
subscribed_to(path) {
|
|
1146
|
-
|
|
1147
|
-
for (let s of this._subscriptions) {
|
|
1148
|
-
|
|
1149
|
-
if (s.matches_path(path)) return true;
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
return false;
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
/**
|
|
1156
|
-
* Unubscribe to change events for a given path or uuid.
|
|
1157
|
-
*
|
|
1158
|
-
* Caution - if a path is provided, _all_ events belonging to you with that
|
|
1159
|
-
* path will be deleted, so if you have multiple subscriptions on a single path,
|
|
1160
|
-
* and only want one of them to be removed, you must provide the uuid instead.
|
|
1161
|
-
*
|
|
1162
|
-
* @param {(String|uuidv4)} path_or_uuid - the path (or uuid) to unwatch.
|
|
1163
|
-
* @throws {EntangldError} if no subscriptions were found.
|
|
1164
|
-
* @return {number} count of subscriptions removed.
|
|
1165
|
-
*/
|
|
1166
|
-
unsubscribe(path_or_uuid) {
|
|
1167
|
-
|
|
1168
|
-
let matching_subs = [];
|
|
1169
|
-
|
|
1170
|
-
if (path_or_uuid.match(uuid_regex)) {
|
|
1171
|
-
// `path_or_uuid` is a uuid, so find any matching subs with that uuid
|
|
1172
|
-
matching_subs = this._subscriptions.filter(
|
|
1173
|
-
s => s.matches_uuid(path_or_uuid) && !s.is_pass_through
|
|
1174
|
-
);
|
|
1175
|
-
} else {
|
|
1176
|
-
// the arg is a path
|
|
1177
|
-
matching_subs = this._subscriptions.filter(
|
|
1178
|
-
// search for subs which both have the correct path AND are not
|
|
1179
|
-
// pass through, so that pass through subs don't get orphaned
|
|
1180
|
-
s => s.matches_path(path_or_uuid) && !s.is_pass_through
|
|
1181
|
-
);
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
// Throw an error if none found
|
|
1185
|
-
if (matching_subs.length == 0) {
|
|
1186
|
-
|
|
1187
|
-
throw new EntangldError(`unsubscribe found no subscriptions matching ${path_or_uuid}`);
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
this._unsubscribe(matching_subs);
|
|
1191
|
-
|
|
1192
|
-
return matching_subs.length;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
/**
|
|
1196
|
-
* Unsubscribe tree.
|
|
1197
|
-
*
|
|
1198
|
-
* Remove any subscriptions that are beneath a path.
|
|
1199
|
-
*
|
|
1200
|
-
* @param {string} path the tree to unwatch.
|
|
1201
|
-
*
|
|
1202
|
-
* @throws {EntangldError} error if there are stores we cannot detach
|
|
1203
|
-
* (i.e. they belong to someone else / upstream != null)
|
|
1204
|
-
*/
|
|
1205
|
-
unsubscribe_tree(path) {
|
|
1206
|
-
|
|
1207
|
-
let matching_subscriptions = this._subscriptions.filter(
|
|
1208
|
-
// Get all the subs beneath this path, which are not pass throughs,
|
|
1209
|
-
// so that subscriptions don't get orphaned
|
|
1210
|
-
s => s.is_beneath(path) && !s.is_pass_through
|
|
1211
|
-
);
|
|
1212
|
-
|
|
1213
|
-
this._unsubscribe(matching_subscriptions);
|
|
1214
|
-
|
|
1215
|
-
// Error on any remaining `pass through` subscriptions
|
|
1216
|
-
matching_subscriptions = this._subscriptions.filter(
|
|
1217
|
-
s => s.is_beneath(path)
|
|
1218
|
-
)
|
|
1219
|
-
|
|
1220
|
-
if (matching_subscriptions.length != 0) {
|
|
1221
|
-
|
|
1222
|
-
const msg = `Unable to completely unsubscribe the tree '${path}' `
|
|
1223
|
-
+ "because some subscriptions are remote or passthrough "
|
|
1224
|
-
+ "(we are not the owner, just a downstream)"
|
|
1225
|
-
|
|
1226
|
-
throw new EntangldError(msg);
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
/**
|
|
1231
|
-
* Unubscribe to change events
|
|
1232
|
-
*
|
|
1233
|
-
* This is an internal function that does not check for safety. It simply
|
|
1234
|
-
* deletes the requested paths and notifies any downstream.
|
|
1235
|
-
*
|
|
1236
|
-
* @private
|
|
1237
|
-
* @param {Subscription[]} subscriptions an array of objects containing at minimum
|
|
1238
|
-
* uuid and upstream keys
|
|
1239
|
-
*/
|
|
1240
|
-
_unsubscribe(subscriptions) {
|
|
1241
|
-
|
|
1242
|
-
// Get a list of uuids to remove
|
|
1243
|
-
const uuids = subscriptions.map(s => s.uuid);
|
|
1244
|
-
|
|
1245
|
-
// Drop all subscriptions with matching uuid
|
|
1246
|
-
this._subscriptions = this._subscriptions.filter(
|
|
1247
|
-
s => !(uuids.includes(s.uuid))
|
|
1248
|
-
);
|
|
1249
|
-
|
|
1250
|
-
// Notify the downstream for any deleted subscriptions that are remote
|
|
1251
|
-
subscriptions.forEach((s) => {
|
|
1252
|
-
if (!s.has_downstream) { // emit unsubscription
|
|
1253
|
-
this.emit("unsubscription", s.path, s.uuid);
|
|
1254
|
-
} else { // Notify downstreams
|
|
1255
|
-
const msg = Entangld_Message.unsubscribe(s.uuid);
|
|
1256
|
-
this._transmit(msg, s.downstream);
|
|
1257
|
-
}
|
|
1258
|
-
});
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
/**
|
|
1262
|
-
* Set local object
|
|
1263
|
-
*
|
|
1264
|
-
* Sets object into local data store.
|
|
1265
|
-
*
|
|
1266
|
-
* @private
|
|
1267
|
-
* @param {string} path the path at which to store the object.
|
|
1268
|
-
* @param {object} data the object to store. If undefined, it unsets path.
|
|
1269
|
-
* @param {string} [op_type] whether to set or push the new
|
|
1270
|
-
* data (push only works if the data item exists and is an array).
|
|
1271
|
-
* @param {object} [params] additional parameters.
|
|
1272
|
-
*
|
|
1273
|
-
* @throws {EntangldError} if attempting to set root store to a non-object.
|
|
1274
|
-
* @throws {TypeError} if attempting to push to a non-array.
|
|
1275
|
-
*/
|
|
1276
|
-
_set_local(path, data, op_type="set", params={}) {
|
|
1277
|
-
// Empty path means set everything
|
|
1278
|
-
if (path === "") {
|
|
1279
|
-
if(data === undefined)
|
|
1280
|
-
data = {}
|
|
1281
|
-
|
|
1282
|
-
// Sanity check
|
|
1283
|
-
if (typeof (data) != "object") {
|
|
1284
|
-
const msg = "You are trying to set the root store to something "
|
|
1285
|
-
+ `(${typeof (data)}) besides an object!`
|
|
1286
|
-
|
|
1287
|
-
throw EntangldError(msg);
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
this._local_data = data;
|
|
1291
|
-
return;
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
let elements = path.split(".");
|
|
1295
|
-
let last = elements.pop();
|
|
1296
|
-
let pointer = this._local_data;
|
|
1297
|
-
|
|
1298
|
-
for (let el of elements) {
|
|
1299
|
-
|
|
1300
|
-
if (!pointer[el]) pointer[el] = {};
|
|
1301
|
-
pointer = pointer[el];
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
// Handle unset operations
|
|
1305
|
-
if (data === undefined) {
|
|
1306
|
-
|
|
1307
|
-
delete (pointer[last]);
|
|
1308
|
-
return;
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
if (op_type == "push") {
|
|
1312
|
-
|
|
1313
|
-
if (pointer[last] && typeof (pointer[last].push) == "function") {
|
|
1314
|
-
|
|
1315
|
-
pointer[last].push(data);
|
|
1316
|
-
if(params.limit) {
|
|
1317
|
-
while(pointer[last].length > params.limit)
|
|
1318
|
-
pointer[last].shift(data);
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
} else {
|
|
1322
|
-
|
|
1323
|
-
throw new TypeError("You cannot .push() to that object");
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
} else {
|
|
1327
|
-
|
|
1328
|
-
// Default to set
|
|
1329
|
-
pointer[last] = data;
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1
|
+
const Entangld = require("./lib/Datastore.js");
|
|
2
|
+
|
|
3
|
+
// Attach objects for destructuring requires
|
|
4
|
+
Entangld.Datastore = require("./lib/Datastore.js");
|
|
5
|
+
Entangld.error = require("./lib/error.js");
|
|
6
|
+
Entangld.Message = require("./lib/Message.js");
|
|
7
|
+
Entangld.Subscription = require("./lib/Subscription.js");
|
|
8
|
+
Entangld.utils = require("./lib/utils.js");
|
|
1334
9
|
|
|
1335
10
|
module.exports = exports = Entangld;
|