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/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
|
+
};
|