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/lib/Message.js ADDED
@@ -0,0 +1,148 @@
1
+ const { v4:uuidv4 } = require("uuid");
2
+
3
+ /**
4
+ * Message class for Entangld.
5
+ *
6
+ * These messages are used for executing datastore operations between
7
+ * remote datastores. In these relationships, there is always an upstream
8
+ * and downstream pair, so that `get`, `set`, `push` and `subscribe` messages
9
+ * are created upstream and passed downstream while `event` and `value` messages
10
+ * are created downstream and are passed back up. `unsubscribe` events can travel
11
+ * in both directions. To maintain consistency, the internal `.path` attribute
12
+ * will always refer a path relative to the downstream datastore, since the
13
+ * downstream datastores do not necessarilly have access to the upstream store
14
+ * structure, and so cannot generally construct upstream paths. This means that
15
+ * the `.path` attribute is a `tree` relative to the upstream datastore, and
16
+ * the upstream path can be reconstructed as:
17
+ * ```javascript
18
+ * > upstream._namespaces.get(downstream) + "." + msg.path;
19
+ * ```
20
+ * Since `unsubscribe` messages can pass either upstream or downstream, the notion
21
+ * of a path is ill-defined, and so unsubscribe messages should have their `.path`
22
+ * attributes set to undefined or null.
23
+ *
24
+ * Most messages will also have a `.uuid` attribute. For `get`/`value` messages,
25
+ * this allows for the value to be properly linked back up with the original `get`
26
+ * message. For the `subscribe`/`unsubscribe`/`event` messages, this allows for
27
+ * callback functions to be trigger properly, and for unsubscribe messages to
28
+ * propogate both directions. `set`/`push` messages do not use the `.uuid` attribute
29
+ * since they require no response.
30
+ */
31
+ class Entangld_Message {
32
+
33
+ constructor({type, path, value, uuid, params, every}) {
34
+
35
+ this.type = type;
36
+ this.path = path;
37
+ this.value = value;
38
+ this.uuid = uuid;
39
+ this.params = params;
40
+ this.every = every;
41
+ }
42
+
43
+ // ----------------------
44
+ // Class Methods
45
+ // ----------------------
46
+ /**
47
+ * Create a `get` message for remote datastores
48
+ *
49
+ * @param {string} tree - the path relative to the remote datastore
50
+ * @param {*} [get_params=undefined] - any parameters to be passed to the
51
+ * remote datastore's local get function
52
+ * @return {Entangld_Message} - The get message to pass to the remote datastore
53
+ */
54
+ static get(tree, get_params) {
55
+ return new this({
56
+ type: "get",
57
+ path : tree, // path needs to be relative to downstream store
58
+ value : get_params,
59
+ uuid : uuidv4() // Gets generate a new uuid
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Create a `value` message in response to a `get` message
65
+ *
66
+ * @param {Entangld_Message} get_msg - the `get` message which this is in response to
67
+ * @param value - the value of the `get`
68
+ * @return {Entangld_Message} - The `value` message to pass back
69
+ */
70
+ static value(get_msg, value) {
71
+ return new this({
72
+ type: "value",
73
+ path : get_msg.path, // Resond with the get's path, which is relative to this store
74
+ value : value,
75
+ uuid : get_msg.uuid // Respond with the get's uuid
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Create a `set`/`push` message for a remote datastore
81
+ *
82
+ * @param {Object} obj - The parameter object for the set/push
83
+ * @param {string} obj.type - either "set" or "push"
84
+ * @param {string} obj.tree - the path (relative to the downstream datastore)
85
+ * @param {*} obj.value - the value to insert into the datastore
86
+ * @param {*} obj.params - any additional parameters
87
+ * @return {Entangld_Message} - the "set" or "push" message
88
+ */
89
+ static setpush({type = "set", tree, value, params}) {
90
+ if (!["set", "push"].includes(type)) { throw new Error(`Invalid type (${type}) for setpush`); }
91
+ return new this({
92
+ type: type,
93
+ path : tree,
94
+ value : value,
95
+ params : params
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Construct subscribe message
101
+ *
102
+ * @param {string} tree - the path (relative to the downstream datastore)
103
+ * @param {uuidv4} uuid - the subscription uuid
104
+ * @return {Entangld_Message} the `subscribe` message
105
+ */
106
+ static subscribe(tree, uuid, every = null) {
107
+ return new this({
108
+ type : "subscribe",
109
+ path : tree,
110
+ uuid : uuid,
111
+ every : every
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Create an `event` message to return data to subscribe callbacks
117
+ *
118
+ * @param {string} path - the path (relative to the downstream store)
119
+ * @param {*} value - the updated datastore value at the path
120
+ * @param {uuidv4} uuid - the uuid of the subscribe being triggered
121
+ * @return {Entangld_Message} the `event` message
122
+ */
123
+ static event(path, value, uuid) {
124
+ return new this({
125
+ type : "event",
126
+ path : path,
127
+ value : value,
128
+ uuid : uuid
129
+ });
130
+ }
131
+
132
+
133
+ /**
134
+ * Create an unsubscribe message for a subscription uuid
135
+ *
136
+ * @param {String} uuid - the subscription uuid
137
+ */
138
+ static unsubscribe(uuid) {
139
+ return new this({
140
+ type: "unsubscribe",
141
+ // path is ommitted from unsubscribe messages
142
+ uuid : uuid
143
+ });
144
+ }
145
+
146
+ }
147
+
148
+ module.exports = exports = Entangld_Message;
@@ -0,0 +1,216 @@
1
+
2
+ const {
3
+ is_beneath
4
+ } = require("./utils.js");
5
+
6
+ /**
7
+ * A datastore subscription object
8
+ */
9
+ class Subscription {
10
+ /**
11
+ * Constructor
12
+ *
13
+ * @param {Object} obj - the configuration object
14
+ * @param {string} obj.path - the datastore path (relative to this datastore)
15
+ * of the subscription
16
+ * @param {uuidv4} obj.uuid - the uuid of the subscription chain
17
+ * @param {function} obj.callback - the callback function, with signature (path, value),
18
+ * where path is relative to this datastore
19
+ * @param {(Entangld|null)} obj.downstream - the downstream datastore (if any)
20
+ * associated with this subscription
21
+ * @param {(Entangld|null)} obj.upstream - the upstream datastore (if any)
22
+ * associated with this subscription
23
+ * @param {(number|null)} obj.every - how many `set` messages to wait before calling callback
24
+ * @return {Subscription} - the subscription object
25
+ */
26
+ constructor({path, uuid, callback, downstream, upstream, every}) {
27
+ this.path = path;
28
+ this.downstream = downstream || null;
29
+ this.upstream = upstream || null;
30
+ this.uuid = uuid;
31
+ this.callback = callback;
32
+ this._has_cb = (typeof(callback) === "function");
33
+ this._is_counting = (this.downstream === null);
34
+ every = parseInt(every);
35
+ if ( isNaN(every) || every < 1 ) {
36
+ this.every = 1;
37
+ } else {
38
+ this.every = every;
39
+ }
40
+ this.index = -1; // guarentee that the first `set` triggers
41
+ }
42
+
43
+ /**
44
+ * Apply this callback function
45
+ *
46
+ * Note, this method also tracks the number of times that a callback
47
+ * function is called (if this subscription is terminal), so that if
48
+ * the subscriptions are throttled by specifying an `this.every`,
49
+ * this method will only call the callback function every `this.every`
50
+ * times it receives a `set` message. If this subscription is not
51
+ * terminal, then the callback function is called every time.
52
+ *
53
+ * This method also is safed when a callback function is not give (i.e.
54
+ * by the `this.static_copy()` method).
55
+ */
56
+ call(...args) {
57
+ if ( !this._has_cb ) return;
58
+ if ( this._is_counting ) {
59
+ this.index = (this.index + 1) % this.every;
60
+ if ( this.index !== 0 ) return;
61
+ }
62
+ this.callback(...args);
63
+ }
64
+
65
+ /**
66
+ * Check if subscription is a `pass through` type
67
+ *
68
+ * Pass throughs are as the links in a chain of subscriptions to allows
69
+ * subscriptions to remote datastores. One store acts as the `head`, where
70
+ * the callback function is registered, an all others are `path through` datastores
71
+ * which simply pass event messages back up to the head subscription.
72
+ *
73
+ * @return {Boolean}
74
+ */
75
+ get is_pass_through() {
76
+ return this.has_upstream;
77
+ }
78
+
79
+ /**
80
+ * Check if this subscription will be directly given data by a datastore
81
+ *
82
+ * @return {Boolean}
83
+ */
84
+ get is_terminal() {
85
+ return !this.has_downstream;
86
+ }
87
+
88
+ /**
89
+ * Check if this subscription will apply a user-supplied callback to data
90
+ *
91
+ * @return {Boolean}
92
+ */
93
+ get is_head() {
94
+ return !this.has_upstream;
95
+ }
96
+
97
+ /**
98
+ * Check if subscription has any downstream subscriptions
99
+ *
100
+ * It the subscription refers to a remote datastore (the downstream), this
101
+ * getter will return a true. Note that !this.has_downstream will check if
102
+ * the subscription is the `tail` subscription object in a subscription chain.
103
+ *
104
+ * @return {Boolean}
105
+ */
106
+ get has_downstream() {
107
+ return this.downstream !== null;
108
+ }
109
+
110
+ /**
111
+ * Check if subscription has any upstream subscriptions
112
+ *
113
+ * It the subscription passes data back to a remote datastore (the upstream), this
114
+ * getter will return a true.
115
+ *
116
+ * @return {Boolean}
117
+ */
118
+ get has_upstream() {
119
+ return this.upstream !== null;
120
+ }
121
+
122
+
123
+ /**
124
+ * Check if a different `Subscription` object matches this subscription
125
+ *
126
+ * @param {Subscription} sub - a different subscription
127
+ * @return {Boolean} - True if the subscriptions match
128
+ */
129
+ matches_subscription(sub) {
130
+ return this.matches_uuid(sub.uuid) && this.matches_path(sub.path);
131
+ }
132
+
133
+ /**
134
+ * Check if an `event` message matches this subscription
135
+ *
136
+ * @param {Entangld_Message} msg - a received message from a downstream datastore
137
+ * @return {Boolean} - True if the message is associated with the subscription
138
+ */
139
+ matches_event_message(msg) {
140
+ // should be above, otherwise events below a subscription won't
141
+ // trigger the callback. I.e. subscribing to `flower` won't
142
+ // trigger when someone sets `flower.color`
143
+ return this.matches_uuid(msg.uuid) && this.is_above(msg.path);
144
+ }
145
+
146
+ /**
147
+ * Check if an `unsubscribe` message matches this subscription
148
+ *
149
+ * @param {Entangld_Message} msg - a received message from a downstream datastore
150
+ * @return {Boolean} - True if the message is associated with the subscription
151
+ */
152
+ matches_unsubscribe_message(msg) {
153
+ return this.matches_uuid(msg.uuid);
154
+ }
155
+
156
+ /**
157
+ * Check if a provided path matches this path
158
+ *
159
+ * @param {String} path - a path string to check against
160
+ * @return {Boolean} - true if the path matches
161
+ */
162
+ matches_path(path) {
163
+ return this.path === path;
164
+ }
165
+
166
+ /**
167
+ * Check if a provided uuid matches this uuid
168
+ *
169
+ * @param {uuidv4} uuid - a uuid string to check against
170
+ * @return {Boolean} - true if the path matches
171
+ */
172
+ matches_uuid(uuid) {
173
+ return this.uuid === uuid;
174
+ }
175
+
176
+ /**
177
+ * Check if subscription path is beneath a provided path
178
+ *
179
+ * @param {String} path - a path string to check against
180
+ * @return {Boolean} - true if the subscription is beneath the path
181
+ */
182
+ is_beneath(path) {
183
+ return is_beneath(this.path, path, 'a');
184
+ }
185
+
186
+ /**
187
+ * Check if subscription path is above a provided path
188
+ *
189
+ * @param {String} path - a path string to check against
190
+ * @return {Boolean} - true if the subscription is beneath the path
191
+ */
192
+ is_above(path) {
193
+ return is_beneath(path, this.path, 'b');
194
+ }
195
+
196
+ /**
197
+ * Get a copy of this subscription without external references
198
+ *
199
+ * This creates a copy, except the upstream/downstream references
200
+ * are set to true (if they exist) or null (if they don't. Addtionally,
201
+ * the callback function is excluded.
202
+ *
203
+ * @return {Subscription} a copy of this subscription object
204
+ */
205
+ static_copy() {
206
+ return new Subscription({
207
+ path : this.path,
208
+ uuid : this.uuid,
209
+ downstream : (this.downstream === null ? null : true),
210
+ upstream : (this.upstream === null ? null : true),
211
+ every : this.every
212
+ });
213
+ }
214
+ }
215
+
216
+ module.exports = exports = Subscription;
package/lib/error.js ADDED
@@ -0,0 +1,43 @@
1
+
2
+
3
+ /**
4
+ * Error class for Entangld.
5
+ */
6
+ class EntangldError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+
10
+ this.name = this.constructor.name;
11
+ Error.captureStackTrace(this, this.constructor);
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Remote Error class for Entangld.
17
+ */
18
+ class RPCError extends Error {
19
+ static serialize_local_error(err) {
20
+ const json = {};
21
+ for ( const key of Object.getOwnPropertyNames(err) ) {
22
+ json[key] = err[key];
23
+ }
24
+ json.Type = err.constructor.name;
25
+ return json;
26
+ }
27
+
28
+ static from_remote_error(err_obj) {
29
+ const rpc_error = new this();
30
+ const old_stack = rpc_error.stack;
31
+ for ( const key of Object.keys(err_obj) ) {
32
+ rpc_error[key] = err_obj[key];
33
+ }
34
+
35
+ rpc_error.stack = `\n${rpc_error.stack}\n ∧∧∧ --- Remote\n ∨∨∨ --- Local\n${old_stack.split("\n").slice(1).join("\n")}`;
36
+ return rpc_error;
37
+ }
38
+ }
39
+
40
+ module.exports = exports = {
41
+ EntangldError,
42
+ RPCError,
43
+ };
package/lib/utils.js ADDED
@@ -0,0 +1,263 @@
1
+
2
+ /**
3
+ * Deep copy an object
4
+ *
5
+ * Uses JSON parse methods so things that don't work in JSON will disappear,
6
+ * with the special exception of functions which are replaced by their return
7
+ * values (or if the function returns a promise, the value that it resolves to).
8
+ *
9
+ * If you pass undefined, it will return a promise resolving to undefined.
10
+ *
11
+ * @private
12
+ * @param {object} original the object to copy.
13
+ * @return {Promise} a promise resolving to a completely new, de-referenced
14
+ * object containing only objects and values.
15
+ */
16
+ function dereferenced_copy(original) {
17
+
18
+ // Undefined passes through
19
+ if (original === undefined)
20
+ return new Promise((res) => { res(undefined); });
21
+
22
+ // Make a copy of o. It will be missing any functions
23
+ let copy = JSON.parse(JSON.stringify(original));
24
+
25
+ // A container to hold the copy (so we can have a recursive function
26
+ // replace it using a key)
27
+ let container = { "copy": copy };
28
+
29
+ // We will store our promises here
30
+ let promises = [];
31
+
32
+ // Recursively call all functions in o, placing their values in copy.
33
+ // Promises are tracked also.
34
+ function recurse(o, c, parent, key) {
35
+
36
+ // If o is a function, get its value and return a promise
37
+ if (typeof (o) == "function") {
38
+
39
+ // Call the function
40
+ let result = o();
41
+
42
+ // If the function itself returns a promise, save it and make it
43
+ // replace the object with a result
44
+ if (result && result.constructor && result.constructor.name
45
+ && result.constructor.name == "Promise") {
46
+
47
+ promises.push(promises, result);
48
+ result.then((val) => {
49
+
50
+ parent[key] = val;
51
+ });
52
+ } else {
53
+
54
+ // Replace the object directly
55
+ parent[key] = result;
56
+ }
57
+
58
+ return;
59
+ }
60
+
61
+
62
+ // If o is not an object, return
63
+ if (typeof (o) != "object") return;
64
+
65
+ // Otherwise, iterate keys and call ourselves recursively
66
+ for (let key in o) {
67
+
68
+ recurse(o[key], c[key], c, key);
69
+ }
70
+ }
71
+
72
+ recurse(original, copy, container, "copy");
73
+
74
+ // Wait for all promises to fulfil, then return the copy
75
+ return Promise.all(promises).then(() => Promise.resolve(copy));
76
+ }
77
+
78
+
79
+ /**
80
+ * Extract from path.
81
+ *
82
+ * Given an object, extracts a child object using a string path.
83
+ *
84
+ * @private
85
+ * @param {object} obj the object we are extracting from.
86
+ * @param {string} path the path to find beneath obj.
87
+ * @return {Array<object, string>} array containing [result, remaining_path].
88
+ * result will be undefined if path can't be found. If we encounter a function
89
+ * in our search, result will be the function and remaining_path will be the
90
+ * remaining part of path.
91
+ */
92
+ function extract_from_path(obj, path) {
93
+
94
+ // Empty path means get everything
95
+ if (path === "") return [obj, ""];
96
+
97
+
98
+ let keys = path.split(".");
99
+ let key;
100
+ let o = obj;
101
+
102
+ while ((key = keys.shift())) {
103
+
104
+ if (key in o) {
105
+
106
+ o = o[key];
107
+
108
+ // If o is a function, we can't go any farther
109
+ if (typeof (o) == "function") {
110
+
111
+ return [o, keys.join(".")];
112
+ }
113
+
114
+ } else {
115
+
116
+ return [undefined, ""];
117
+ }
118
+
119
+ }
120
+
121
+ return [o, ""];
122
+ }
123
+
124
+ /**
125
+ * Partial copy.
126
+ *
127
+ * Return a partial copy of an object (depth limited). DOES NOT DE-REFERENCE!
128
+ *
129
+ * @private
130
+ * @param {object} object the object to copy
131
+ * @param {number} max_depth the maximum depth to copy (0 returns object keys)
132
+ * @return {object} partial copy of an object limited by max_depth.
133
+ */
134
+ function partial_copy(o, max_depth) {
135
+
136
+ // Trivial case, return object untouched if no max_depth
137
+ if (typeof (max_depth) != "number") return o;
138
+
139
+ // If o is not an object, return it
140
+ if (typeof (o) != "object") return o;
141
+
142
+ let c = (Array.isArray(o)) ? [] : {};
143
+
144
+ // If max_depth has been exceeded, return the empty object or array
145
+ if (max_depth < 0) return c;
146
+
147
+ // Otherwise, iterate keys and call ourselves recursively
148
+ for (let key in o) {
149
+
150
+ if (typeof (o[key]) != "object") {
151
+
152
+ c[key] = o[key];
153
+
154
+ } else {
155
+
156
+ c[key] = partial_copy(o[key], max_depth - 1);
157
+ }
158
+ }
159
+
160
+ return c;
161
+ }
162
+
163
+ /**
164
+ * Is beneath
165
+ *
166
+ * Is `a` under `b`? E.g. is "system.bus.voltage" equal to or beneath "system.bus"?
167
+ *
168
+ * @private
169
+ * @param {string} a the string tested for "insideness"
170
+ * @param {string} b the string tested for "outsideness" (wildcard '*' allowed)
171
+ * @param {string} [allow_wildcard = false] allow wildcards, one of ('a', 'b', false). Default is false (no wildcard allowed)
172
+ * @return boolean
173
+ */
174
+ function is_beneath(a, b, allow_wildcard=false) {
175
+
176
+ // Everything is beneath the top ("")
177
+ if(b==="") return true;
178
+
179
+ // If paths are both blank, they are equal
180
+ if(b==="" && a==="") return true;
181
+
182
+ let A=a.split(".");
183
+ let B=b.split(".");
184
+
185
+ // A is not beneath B if any part is not the same. Check for this.
186
+ switch (allow_wildcard) {
187
+
188
+ case "a":
189
+ // Allow wildcard in A
190
+ while(A.length && B.length){
191
+
192
+ let A_part=A.shift();
193
+ if(A_part!=B.shift() && A_part != "*") return false;
194
+ }
195
+ break;
196
+
197
+ case "b":
198
+ // Allow wildcard in B
199
+ while(A.length && B.length){
200
+
201
+ let B_part=B.shift();
202
+ if(B_part!=A.shift() && B_part != "*") return false;
203
+ }
204
+ break;
205
+
206
+ default:
207
+ // No wildcards allowed
208
+ while(A.length && B.length){
209
+
210
+ if(A.shift()!=B.shift()) return false;
211
+ }
212
+ }
213
+
214
+ // A is not beneath B if B is longer
215
+ if(B.length) return false;
216
+
217
+ return true;
218
+ }
219
+
220
+ function parameter_parser(func_string) {
221
+ let out = "";
222
+ let reading = false;
223
+ let inside = 0;
224
+ for ( const char of func_string ) {
225
+ if ( !reading ) {
226
+ if ( char === "(" ) reading = true;
227
+ continue;
228
+ }
229
+ switch ( char ) {
230
+ case "(":
231
+ case "{":
232
+ case "[":
233
+ inside++;
234
+ break;
235
+ case ")":
236
+ case "}":
237
+ case "]":
238
+ inside--;
239
+ break;
240
+ case ",":
241
+ if ( inside == 0 ) {
242
+ out += "\x00";
243
+ continue;
244
+ } else {
245
+ out += char;
246
+ }
247
+ break;
248
+ default:
249
+ out += char;
250
+ }
251
+
252
+ if ( inside < 0 ) break;
253
+ }
254
+ return out.split("\x00").map((p)=>p.trim()).filter((p)=>p.length);
255
+ }
256
+
257
+ module.exports = exports = {
258
+ is_beneath,
259
+ extract_from_path,
260
+ partial_copy,
261
+ dereferenced_copy,
262
+ parameter_parser
263
+ };