@soundworks/core 3.1.0-beta.1 → 3.1.0-beta.5
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/client/Client.js +1 -1
- package/client/PluginManager.js +3 -3
- package/client/Socket.js +70 -39
- package/common/ParameterBag.js +25 -13
- package/common/SharedState.js +62 -17
- package/common/SharedStateManagerClient.js +6 -0
- package/common/SharedStateManagerServer.js +6 -0
- package/common/SharedStatePrivate.js +96 -63
- package/common/shared-state-utils.js +5 -3
- package/common/sockets-encoder-decoder.js +1 -1
- package/package.json +12 -10
- package/server/Client.js +2 -4
- package/server/PluginManager.js +3 -3
- package/server/Server.js +41 -3
- package/server/utils/logger.js +24 -8
package/client/Client.js
CHANGED
|
@@ -118,7 +118,7 @@ class Client {
|
|
|
118
118
|
// url: '',
|
|
119
119
|
path: 'socket' // pingInterval: 5 * 1000,
|
|
120
120
|
|
|
121
|
-
}, config.env.websockets); // init communications
|
|
121
|
+
}, config.env.websockets); // init socket communications (string and binary)
|
|
122
122
|
|
|
123
123
|
await this.socket.init(this.type, this.config);
|
|
124
124
|
return Promise.resolve();
|
package/client/PluginManager.js
CHANGED
|
@@ -142,7 +142,7 @@ class PluginManager {
|
|
|
142
142
|
register(name, factory, options = {}, deps = []) {
|
|
143
143
|
if (this._registeredPlugins[name]) {
|
|
144
144
|
const ctor = factory(_AbstractPlugin.default);
|
|
145
|
-
throw new Error(`[soundworks:core]
|
|
145
|
+
throw new Error(`[soundworks:core] Plugin "${name}" of type "${ctor.name}" already registered`);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
const ctor = factory(_AbstractPlugin.default);
|
|
@@ -165,7 +165,7 @@ class PluginManager {
|
|
|
165
165
|
|
|
166
166
|
get(name, _experienceRequired = false) {
|
|
167
167
|
if (!this._registeredPlugins[name]) {
|
|
168
|
-
throw new Error(`Cannot get or require plugin "${name}", plugin is not registered
|
|
168
|
+
throw new Error(`[soundworks:core] Cannot get or require plugin "${name}", plugin is not registered
|
|
169
169
|
> registered plugins are:
|
|
170
170
|
${Object.keys(this._registeredPlugins).map(n => `> - ${n}\n`).join('')}
|
|
171
171
|
`);
|
|
@@ -173,7 +173,7 @@ ${Object.keys(this._registeredPlugins).map(n => `> - ${n}\n`).join('')}
|
|
|
173
173
|
|
|
174
174
|
|
|
175
175
|
if (_experienceRequired && this.signals.start.value === true) {
|
|
176
|
-
throw new Error(`Plugin "${name}" required after pluginManager start`);
|
|
176
|
+
throw new Error(`[soundworks:core] Plugin "${name}" required after pluginManager start`);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
if (!this._instances[name]) {
|
package/client/Socket.js
CHANGED
|
@@ -53,6 +53,13 @@ class Socket {
|
|
|
53
53
|
|
|
54
54
|
/** @private */
|
|
55
55
|
this.ws = null;
|
|
56
|
+
/**
|
|
57
|
+
* WebSocket instance (binary protocol - binaryType = 'arraybuffer').
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/** @private */
|
|
61
|
+
|
|
62
|
+
this.binaryWs = null;
|
|
56
63
|
this._stringListeners = new Map();
|
|
57
64
|
this._binaryListeners = new Map();
|
|
58
65
|
}
|
|
@@ -67,11 +74,11 @@ class Socket {
|
|
|
67
74
|
/** @private */
|
|
68
75
|
|
|
69
76
|
|
|
70
|
-
init(clientType, config) {
|
|
77
|
+
async init(clientType, config) {
|
|
71
78
|
// unique key that allows to associate the two sockets to the same client.
|
|
72
79
|
// note: the key is only used to pair to two sockets, so its usage is very
|
|
73
80
|
// limited in time therefore a random number should hopefully be sufficient.
|
|
74
|
-
const key = (Math.random() + '').replace(/^0
|
|
81
|
+
const key = (Math.random() + '').replace(/^0\./, ''); // open web sockets
|
|
75
82
|
|
|
76
83
|
let {
|
|
77
84
|
path
|
|
@@ -106,47 +113,71 @@ class Socket {
|
|
|
106
113
|
// ----------------------------------------------------------
|
|
107
114
|
|
|
108
115
|
const stringSocketUrl = `${url}?binary=0&${queryParams}`;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
116
|
+
await new Promise((resolve, reject) => {
|
|
117
|
+
const trySocket = async () => {
|
|
118
|
+
log(`[string socket] trying connection - url: ${stringSocketUrl}`);
|
|
119
|
+
const ws = new _isomorphicWs.default(stringSocketUrl);
|
|
120
|
+
ws.addEventListener('open', connectEvent => {
|
|
121
|
+
// parse incoming messages for pubsub
|
|
122
|
+
this.ws = ws;
|
|
123
|
+
this.ws.addEventListener('message', e => {
|
|
124
|
+
const [channel, args] = (0, _socketsEncoderDecoder.unpackStringMessage)(e.data);
|
|
125
|
+
|
|
126
|
+
this._emit(false, channel, ...args);
|
|
127
|
+
}); // broadcast all `WebSocket` native events
|
|
128
|
+
|
|
129
|
+
['close', 'error', 'upgrade', 'message'].forEach(eventName => {
|
|
130
|
+
this.ws.addEventListener(eventName, e => {
|
|
131
|
+
this._emit(false, eventName, e);
|
|
132
|
+
});
|
|
133
|
+
}); // forward open event
|
|
134
|
+
|
|
135
|
+
this._emit(false, 'open', connectEvent); // continue with raw socket
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
resolve();
|
|
139
|
+
}); // cf. https://github.com/collective-soundworks/soundworks/issues/17
|
|
140
|
+
|
|
141
|
+
ws.addEventListener('error', e => {
|
|
142
|
+
if (e.type === 'error' && e.error.code === 'ECONNREFUSED') {
|
|
143
|
+
console.log('[socket] connection refused - (retry in 1s)');
|
|
144
|
+
setTimeout(trySocket, 1000);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
trySocket();
|
|
150
|
+
}); // // ----------------------------------------------------------
|
|
151
|
+
// // init binary socket
|
|
152
|
+
// // ----------------------------------------------------------
|
|
128
153
|
|
|
129
154
|
const binarySocketUrl = `${url}?binary=1&${queryParams}`;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
155
|
+
await new Promise((resolve, reject) => {
|
|
156
|
+
log(`[binary socket] trying connection - url: ${binarySocketUrl}`);
|
|
157
|
+
const ws = new _isomorphicWs.default(binarySocketUrl);
|
|
158
|
+
ws.binaryType = 'arraybuffer';
|
|
159
|
+
ws.addEventListener('open', connectEvent => {
|
|
160
|
+
// parse incoming messages for pubsub
|
|
161
|
+
this.binaryWs = ws;
|
|
162
|
+
this.binaryWs.addEventListener('message', e => {
|
|
163
|
+
const [channel, data] = (0, _socketsEncoderDecoder.unpackBinaryMessage)(e.data);
|
|
164
|
+
|
|
165
|
+
this._emit(true, channel, data);
|
|
166
|
+
}); // broadcast all `WebSocket` native events
|
|
167
|
+
|
|
168
|
+
['close', 'error', 'upgrade', 'message'].forEach(eventName => {
|
|
169
|
+
this.binaryWs.addEventListener(eventName, e => {
|
|
170
|
+
this._emit(true, eventName, e);
|
|
171
|
+
});
|
|
172
|
+
}); // forward open event
|
|
173
|
+
|
|
174
|
+
this._emit(true, 'open', connectEvent);
|
|
175
|
+
|
|
176
|
+
resolve();
|
|
146
177
|
});
|
|
147
|
-
}); //
|
|
178
|
+
}); // everyone is connected
|
|
148
179
|
|
|
149
|
-
return Promise.
|
|
180
|
+
return Promise.resolve();
|
|
150
181
|
}
|
|
151
182
|
/** @private */
|
|
152
183
|
|
package/common/ParameterBag.js
CHANGED
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.
|
|
6
|
+
exports.types = exports.sharedOptions = exports.default = void 0;
|
|
7
7
|
|
|
8
|
-
var _lodash = _interopRequireDefault(require("lodash.
|
|
8
|
+
var _lodash = _interopRequireDefault(require("lodash.clonedeep"));
|
|
9
9
|
|
|
10
10
|
var _fastDeepEqual = _interopRequireDefault(require("fast-deep-equal"));
|
|
11
11
|
|
|
12
12
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
13
13
|
|
|
14
14
|
// import types from './types';
|
|
15
|
+
// import shallowClone from 'shallow-clone';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* @typedef {Object} server.SharedStateManagerServer~schema
|
|
@@ -182,6 +183,9 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
|
|
|
182
183
|
/**
|
|
183
184
|
* @typedef {Object} server.SharedStateManagerServer~schemaAnyDef
|
|
184
185
|
*
|
|
186
|
+
* Note that the `any` type always return a shallow copy of the state internal
|
|
187
|
+
* value. Mutating the returned value will not modify the internal state.
|
|
188
|
+
*
|
|
185
189
|
* @property {String} [type='any'] - Parameter of any type.
|
|
186
190
|
* @property {Mixed} default - Default value of the parameter.
|
|
187
191
|
* @property {Boolean} [nullable=false] - Define if the parameter is nullable. If
|
|
@@ -209,8 +213,8 @@ const sharedOptions = {
|
|
|
209
213
|
event: false,
|
|
210
214
|
// if event=true, nullable=true
|
|
211
215
|
metas: {},
|
|
212
|
-
filterChange: true
|
|
213
|
-
|
|
216
|
+
filterChange: true,
|
|
217
|
+
immediate: false
|
|
214
218
|
};
|
|
215
219
|
exports.sharedOptions = sharedOptions;
|
|
216
220
|
const types = {
|
|
@@ -423,10 +427,17 @@ class ParameterBag {
|
|
|
423
427
|
|
|
424
428
|
|
|
425
429
|
getValues() {
|
|
426
|
-
|
|
430
|
+
let values = {};
|
|
431
|
+
|
|
432
|
+
for (let name in this._values) {
|
|
433
|
+
values[name] = this.get(name);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return values;
|
|
427
437
|
}
|
|
428
438
|
/**
|
|
429
|
-
* Return the value of the given parameter.
|
|
439
|
+
* Return the value of the given parameter. If the parameter is of `any` type,
|
|
440
|
+
* a deep copy is returned.
|
|
430
441
|
*
|
|
431
442
|
* @param {String} name - Name of the parameter.
|
|
432
443
|
* @return {Mixed} - Value of the parameter.
|
|
@@ -438,7 +449,11 @@ class ParameterBag {
|
|
|
438
449
|
throw new ReferenceError(`[stateManager] Cannot get value of undefined parameter "${name}"`);
|
|
439
450
|
}
|
|
440
451
|
|
|
441
|
-
|
|
452
|
+
if (this._schema[name].type === 'any') {
|
|
453
|
+
return (0, _lodash.default)(this._values[name]);
|
|
454
|
+
} else {
|
|
455
|
+
return this._values[name];
|
|
456
|
+
}
|
|
442
457
|
}
|
|
443
458
|
/**
|
|
444
459
|
* Set the value of a parameter. If the value of the parameter is updated
|
|
@@ -473,17 +488,14 @@ class ParameterBag {
|
|
|
473
488
|
|
|
474
489
|
const currentValue = this._values[name];
|
|
475
490
|
const updated = !(0, _fastDeepEqual.default)(currentValue, value);
|
|
476
|
-
this._values[name] = value;
|
|
477
|
-
|
|
478
|
-
if (def.event === true) {
|
|
479
|
-
this._values[name] = null;
|
|
480
|
-
} // return tuple so that the state manager can handle the `filterChange` option
|
|
481
|
-
|
|
491
|
+
this._values[name] = value; // return tuple so that the state manager can handle the `filterChange` option
|
|
482
492
|
|
|
483
493
|
return [value, updated];
|
|
484
494
|
}
|
|
485
495
|
/**
|
|
486
496
|
* Reset a parameter to its initialization values. Reset all parameters if no argument.
|
|
497
|
+
* @note - prefer `state.set(state.getInitValues())`
|
|
498
|
+
* or `state.set(state.getDefaultValues())`
|
|
487
499
|
*
|
|
488
500
|
* @param {String} [name=null] - Name of the parameter to reset.
|
|
489
501
|
*/
|
package/common/SharedState.js
CHANGED
|
@@ -46,20 +46,18 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
46
46
|
this._onDetachCallbacks = new Set();
|
|
47
47
|
this._onDeleteCallbacks = new Set(); // add listener for state updates
|
|
48
48
|
|
|
49
|
-
client.transport.addListener(`${_sharedStateUtils.UPDATE_RESPONSE}-${id}-${this.remoteId}`, (reqId, updates, context) => {
|
|
50
|
-
const updated = this._commit(updates, context, true, true);
|
|
51
|
-
|
|
49
|
+
client.transport.addListener(`${_sharedStateUtils.UPDATE_RESPONSE}-${id}-${this.remoteId}`, async (reqId, updates, context) => {
|
|
50
|
+
const updated = await this._commit(updates, context, true, true);
|
|
52
51
|
(0, _sharedStateUtils.resolveRequest)(reqId, updated);
|
|
53
52
|
}); // retrieve values but do not propagate to subscriptions
|
|
54
53
|
|
|
55
|
-
client.transport.addListener(`${_sharedStateUtils.UPDATE_ABORT}-${id}-${this.remoteId}`, (reqId, updates, context) => {
|
|
56
|
-
const updated = this._commit(updates, context, false, true);
|
|
57
|
-
|
|
54
|
+
client.transport.addListener(`${_sharedStateUtils.UPDATE_ABORT}-${id}-${this.remoteId}`, async (reqId, updates, context) => {
|
|
55
|
+
const updated = await this._commit(updates, context, false, true);
|
|
58
56
|
(0, _sharedStateUtils.resolveRequest)(reqId, updated);
|
|
59
57
|
});
|
|
60
|
-
client.transport.addListener(`${_sharedStateUtils.UPDATE_NOTIFICATION}-${id}-${this.remoteId}`, (updates, context) => {
|
|
58
|
+
client.transport.addListener(`${_sharedStateUtils.UPDATE_NOTIFICATION}-${id}-${this.remoteId}`, async (updates, context) => {
|
|
61
59
|
// cf. https://github.com/collective-soundworks/soundworks/issues/18
|
|
62
|
-
this._commit(updates, context, true, false);
|
|
60
|
+
await this._commit(updates, context, true, false);
|
|
63
61
|
}); // ---------------------------------------------
|
|
64
62
|
// DELETE initiated by creator, or schema deleted
|
|
65
63
|
// ---------------------------------------------
|
|
@@ -145,7 +143,7 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
145
143
|
/** @private */
|
|
146
144
|
|
|
147
145
|
|
|
148
|
-
_commit(updates, context, propagate = true, initiator = false) {
|
|
146
|
+
async _commit(updates, context, propagate = true, initiator = false) {
|
|
149
147
|
const newValues = {};
|
|
150
148
|
const oldValues = {};
|
|
151
149
|
|
|
@@ -154,7 +152,11 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
154
152
|
immediate,
|
|
155
153
|
filterChange,
|
|
156
154
|
event
|
|
157
|
-
} = this._parameters.getSchema(name);
|
|
155
|
+
} = this._parameters.getSchema(name); // @note 20211209 - we have an issue here server-side, because if the value
|
|
156
|
+
// is an object or an array, the reference is shared by everybody, therefore
|
|
157
|
+
// `changed` is always false and the new value is never propagated...
|
|
158
|
+
// FIXED - `state.get` now returns a deep copy when `type` is `any`
|
|
159
|
+
|
|
158
160
|
|
|
159
161
|
const oldValue = this._parameters.get(name);
|
|
160
162
|
|
|
@@ -165,7 +167,7 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
165
167
|
// @note - we don't need to check filterChange here because the value
|
|
166
168
|
// has been updated in parameters on the `set` side so can rely on `changed`
|
|
167
169
|
// to avoid retrigger listeners.
|
|
168
|
-
// If the value has overriden by the server, `changed` will true
|
|
170
|
+
// If the value has been overriden by the server, `changed` will true
|
|
169
171
|
// anyway so it should behave correctly.
|
|
170
172
|
if (!changed || event) {
|
|
171
173
|
continue;
|
|
@@ -177,14 +179,48 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
177
179
|
} // if the `UPDATE_REQUEST` as been aborted by the server, do not propagate
|
|
178
180
|
|
|
179
181
|
|
|
182
|
+
let promises = [];
|
|
183
|
+
|
|
180
184
|
if (propagate && Object.keys(newValues).length > 0) {
|
|
181
|
-
this._subscriptions.forEach(listener =>
|
|
185
|
+
this._subscriptions.forEach(listener => {
|
|
186
|
+
promises.push(listener(newValues, oldValues, context));
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
await Promise.all(promises); // reset events to null after propagation of all listeners
|
|
191
|
+
|
|
192
|
+
for (let name in newValues) {
|
|
193
|
+
const {
|
|
194
|
+
event
|
|
195
|
+
} = this._parameters.getSchema(name);
|
|
196
|
+
|
|
197
|
+
if (event) {
|
|
198
|
+
this._parameters.set(name, null);
|
|
199
|
+
}
|
|
182
200
|
}
|
|
183
201
|
|
|
184
202
|
return newValues;
|
|
185
203
|
}
|
|
186
204
|
/**
|
|
187
|
-
* Updates values of the state.
|
|
205
|
+
* Updates values of the state. Wait for all subscriptions to be resolved
|
|
206
|
+
* before resolving itself, i.e.:
|
|
207
|
+
*
|
|
208
|
+
* ```js
|
|
209
|
+
* const a = await server.stateManager.create('a');
|
|
210
|
+
* let asyncCallbackCalled = false;
|
|
211
|
+
*
|
|
212
|
+
* a.subscribe(updates => {
|
|
213
|
+
* return new Promise(resolve => {
|
|
214
|
+
* setTimeout(() => {
|
|
215
|
+
* asyncCallbackCalled = true;
|
|
216
|
+
* resolve();
|
|
217
|
+
* }, 100);
|
|
218
|
+
* });
|
|
219
|
+
* });
|
|
220
|
+
*
|
|
221
|
+
* await a.set({ bool: true });
|
|
222
|
+
* assert.equal(asyncCallbackCalled, true);
|
|
223
|
+
* ```
|
|
188
224
|
*
|
|
189
225
|
* @async
|
|
190
226
|
* @param {Object} updates - key / value pairs of updates to apply to the state.
|
|
@@ -230,7 +266,7 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
230
266
|
|
|
231
267
|
const [newValue, changed] = this._parameters.set(name, updates[name]);
|
|
232
268
|
|
|
233
|
-
if (changed || filterChange === false) {
|
|
269
|
+
if (changed || filterChange === false || event) {
|
|
234
270
|
immediateOldValues[name] = oldValue;
|
|
235
271
|
immediateNewValues[name] = newValue;
|
|
236
272
|
propagateNow = true;
|
|
@@ -318,7 +354,7 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
318
354
|
* // do something
|
|
319
355
|
* }
|
|
320
356
|
* }
|
|
321
|
-
* }
|
|
357
|
+
* });
|
|
322
358
|
*
|
|
323
359
|
* @see {common.SharedState#set}
|
|
324
360
|
* @see {common.SharedState#subscribe}
|
|
@@ -329,6 +365,8 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
329
365
|
*
|
|
330
366
|
* @param {common.SharedState~subscribeCallback} callback - callback to execute
|
|
331
367
|
* when an update is applied on the state.
|
|
368
|
+
* @param {Boolean} [executeListener=false] - execute the given listener with
|
|
369
|
+
* current state values. (`oldValues` will be set to `{}`, and `context` to `null`)
|
|
332
370
|
*
|
|
333
371
|
* @example
|
|
334
372
|
* state.subscribe(async (newValues, oldValues) => {
|
|
@@ -337,13 +375,20 @@ ${JSON.stringify(initValues, null, 2)}`);
|
|
|
337
375
|
* // do something
|
|
338
376
|
* }
|
|
339
377
|
* }
|
|
340
|
-
* }
|
|
378
|
+
* });
|
|
341
379
|
*/
|
|
342
380
|
|
|
343
381
|
|
|
344
|
-
subscribe(listener) {
|
|
382
|
+
subscribe(listener, executeListener = false) {
|
|
345
383
|
this._subscriptions.add(listener);
|
|
346
384
|
|
|
385
|
+
if (executeListener === true) {
|
|
386
|
+
const currentValues = this.getValues();
|
|
387
|
+
const oldValues = {};
|
|
388
|
+
const context = null;
|
|
389
|
+
listener(currentValues, oldValues, context);
|
|
390
|
+
}
|
|
391
|
+
|
|
347
392
|
return () => {
|
|
348
393
|
this._subscriptions.delete(listener);
|
|
349
394
|
};
|
|
@@ -99,6 +99,12 @@ class SharedStateManagerClient {
|
|
|
99
99
|
list.forEach(([schemaName, stateId, nodeId]) => {
|
|
100
100
|
this._observeListeners.forEach(callback => callback(schemaName, stateId, nodeId));
|
|
101
101
|
});
|
|
102
|
+
}); // ---------------------------------------------
|
|
103
|
+
// Clear cache when schema is deleted
|
|
104
|
+
// ---------------------------------------------
|
|
105
|
+
|
|
106
|
+
this.client.transport.addListener(_sharedStateUtils.DELETE_SCHEMA, schemaName => {
|
|
107
|
+
this._cachedSchemas.delete(schemaName);
|
|
102
108
|
});
|
|
103
109
|
}
|
|
104
110
|
/**
|
|
@@ -273,6 +273,7 @@ class SharedStateManagerServer extends _SharedStateManagerClient.default {
|
|
|
273
273
|
|
|
274
274
|
|
|
275
275
|
deleteSchema(schemaName) {
|
|
276
|
+
// @note: deleting schema
|
|
276
277
|
for (let [id, state] of this._serverStatesById.entries()) {
|
|
277
278
|
if (state.schemaName === schemaName) {
|
|
278
279
|
for (let [remoteId, attached] of state._attachedClients.entries()) {
|
|
@@ -283,6 +284,11 @@ class SharedStateManagerServer extends _SharedStateManagerClient.default {
|
|
|
283
284
|
|
|
284
285
|
this._serverStatesById.delete(this.id);
|
|
285
286
|
}
|
|
287
|
+
} // clear schema cache of all connected clients
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
for (let client of this._clientByNodeId.values()) {
|
|
291
|
+
client.transport.emit(`${_sharedStateUtils.DELETE_SCHEMA}`, schemaName);
|
|
286
292
|
}
|
|
287
293
|
|
|
288
294
|
this._schemas.delete(schemaName); // delete registered hooks
|
|
@@ -7,6 +7,8 @@ exports.default = void 0;
|
|
|
7
7
|
|
|
8
8
|
var _ParameterBag = _interopRequireDefault(require("./ParameterBag.js"));
|
|
9
9
|
|
|
10
|
+
var _lodash = _interopRequireDefault(require("lodash.clonedeep"));
|
|
11
|
+
|
|
10
12
|
var _sharedStateUtils = require("./shared-state-utils");
|
|
11
13
|
|
|
12
14
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
@@ -41,83 +43,114 @@ class SharedStatePrivate {
|
|
|
41
43
|
// apply registered hooks
|
|
42
44
|
const hooks = this._manager._hooksBySchemaName.get(this.schemaName);
|
|
43
45
|
|
|
44
|
-
const values = this._parameters.getValues();
|
|
46
|
+
const values = this._parameters.getValues();
|
|
45
47
|
|
|
48
|
+
let hookAborted = false; // cf. https://github.com/collective-soundworks/soundworks/issues/45
|
|
46
49
|
|
|
47
50
|
for (let hook of hooks.values()) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// objects as dirty, because if the state is attached locally, we
|
|
61
|
-
// compare the Object instances instead of their values.
|
|
62
|
-
// @note - this should be made more robust but how?
|
|
63
|
-
const [newValue, changed] = this._parameters.set(name, updates[name]); // if `filterChange` is set to `false` we don't check if the value
|
|
64
|
-
// has been changed or not, it is always propagated to client states
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const {
|
|
68
|
-
filterChange
|
|
69
|
-
} = this._parameters.getSchema(name);
|
|
70
|
-
|
|
71
|
-
if (filterChange && changed || !filterChange) {
|
|
72
|
-
filteredUpdates[name] = newValue;
|
|
73
|
-
hasUpdates = true;
|
|
51
|
+
const result = await hook(updates, values, context);
|
|
52
|
+
|
|
53
|
+
if (result === null) {
|
|
54
|
+
// explicit abort if hook returns null
|
|
55
|
+
hookAborted = true;
|
|
56
|
+
break;
|
|
57
|
+
} else if (result === undefined) {
|
|
58
|
+
// implicit continue if hook returns undefined
|
|
59
|
+
continue;
|
|
60
|
+
} else {
|
|
61
|
+
// the hook returned an updates object
|
|
62
|
+
updates = result;
|
|
74
63
|
}
|
|
75
64
|
}
|
|
76
65
|
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
66
|
+
if (hookAborted === false) {
|
|
67
|
+
const filteredUpdates = {};
|
|
68
|
+
let hasUpdates = false;
|
|
69
|
+
|
|
70
|
+
for (let name in updates) {
|
|
71
|
+
// from v3.1.0 - the `filteredUpdates` check is made using 'fast-deep-equal'
|
|
72
|
+
// cf. https://github.com/epoberezkin/fast-deep-equal
|
|
73
|
+
// therefore unchanged objects are not considered changed
|
|
74
|
+
// nor propagated anymore.
|
|
75
|
+
// until v3.0.4 - we checked the `schema[name].type === 'any'`, to always consider
|
|
76
|
+
// objects as dirty, because if the state is attached locally, we
|
|
77
|
+
// compare the Object instances instead of their values.
|
|
78
|
+
// @note - this should be made more robust but how?
|
|
79
|
+
const [newValue, changed] = this._parameters.set(name, updates[name]); // if `filterChange` is set to `false` we don't check if the value
|
|
80
|
+
// has been changed or not, it is always propagated to client states
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
const {
|
|
84
|
+
event,
|
|
85
|
+
filterChange
|
|
86
|
+
} = this._parameters.getSchema(name); // if event type reset internal value to null
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if (event) {
|
|
90
|
+
this._parameters.set(name, null);
|
|
100
91
|
}
|
|
101
|
-
}
|
|
102
92
|
|
|
103
|
-
|
|
104
|
-
|
|
93
|
+
if (filterChange && changed || !filterChange) {
|
|
94
|
+
filteredUpdates[name] = newValue;
|
|
95
|
+
hasUpdates = true;
|
|
96
|
+
}
|
|
105
97
|
}
|
|
106
98
|
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
99
|
+
if (hasUpdates) {
|
|
100
|
+
// send response to requester
|
|
101
|
+
// client.transport.emit(`${UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, filteredUpdates);
|
|
102
|
+
// @note: we propagate server-side last, because as the server transport
|
|
103
|
+
// is synchronous it can break ordering if a subscription function makes
|
|
104
|
+
// itself an update in reaction to an update, therefore network messages
|
|
105
|
+
// order would be broken,
|
|
106
|
+
// we need to handle cases where:
|
|
107
|
+
// client state (client.id: 2) sends a request
|
|
108
|
+
// server attached state (client.id: -1) spot a problem and overrides the value
|
|
109
|
+
// we want the remote client (id: 2) to receive in the right order:
|
|
110
|
+
// * 1. the value it requested,
|
|
111
|
+
// * 2. the value overriden by the server-side attached state (id: -1)
|
|
112
|
+
// this problem could be solved properly with a reducer system:
|
|
113
|
+
// if (dirty) {
|
|
114
|
+
// -> call (async) reducer
|
|
115
|
+
// -> get values from reducer
|
|
116
|
+
//. -> dispatch to everybody
|
|
117
|
+
// }
|
|
118
|
+
for (let [peerRemoteId, peer] of this._attachedClients.entries()) {
|
|
119
|
+
// propagate notification to all other attached clients except server
|
|
120
|
+
if (remoteId !== peerRemoteId && peer.id !== -1) {
|
|
121
|
+
peer.transport.emit(`${_sharedStateUtils.UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`, filteredUpdates, context);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (client.id !== -1) {
|
|
126
|
+
client.transport.emit(`${_sharedStateUtils.UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, filteredUpdates, context);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (let [peerRemoteId, peer] of this._attachedClients.entries()) {
|
|
130
|
+
// propagate notification to server
|
|
131
|
+
if (remoteId !== peerRemoteId && peer.id === -1) {
|
|
132
|
+
peer.transport.emit(`${_sharedStateUtils.UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`, filteredUpdates, context);
|
|
133
|
+
}
|
|
111
134
|
}
|
|
112
|
-
}
|
|
113
135
|
|
|
114
|
-
|
|
115
|
-
|
|
136
|
+
if (client.id === -1) {
|
|
137
|
+
client.transport.emit(`${_sharedStateUtils.UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, filteredUpdates, context);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
// propagate back to the requester that the update has been aborted
|
|
141
|
+
// ignore all other attached clients.
|
|
142
|
+
client.transport.emit(`${_sharedStateUtils.UPDATE_ABORT}-${this.id}-${remoteId}`, reqId, updates, context);
|
|
116
143
|
}
|
|
117
144
|
} else {
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
145
|
+
// retrieve values from inner state (also handle immediate approriately)
|
|
146
|
+
const oldValues = {};
|
|
147
|
+
|
|
148
|
+
for (let name in updates) {
|
|
149
|
+
oldValues[name] = this._parameters.get(name);
|
|
150
|
+
} // aborted by hook (updates have been overriden to {})
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
client.transport.emit(`${_sharedStateUtils.UPDATE_ABORT}-${this.id}-${remoteId}`, reqId, oldValues, context);
|
|
121
154
|
}
|
|
122
155
|
});
|
|
123
156
|
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
+
exports.UPDATE_RESPONSE = exports.UPDATE_REQUEST = exports.UPDATE_NOTIFICATION = exports.UPDATE_ABORT = exports.SERVER_ID = exports.OBSERVE_RESPONSE = exports.OBSERVE_REQUEST = exports.OBSERVE_NOTIFICATION = exports.DETACH_RESPONSE = exports.DETACH_REQUEST = exports.DETACH_ERROR = exports.DELETE_SCHEMA = exports.DELETE_RESPONSE = exports.DELETE_REQUEST = exports.DELETE_NOTIFICATION = exports.DELETE_ERROR = exports.CREATE_RESPONSE = exports.CREATE_REQUEST = exports.CREATE_ERROR = exports.ATTACH_RESPONSE = exports.ATTACH_REQUEST = exports.ATTACH_ERROR = void 0;
|
|
6
7
|
exports.idGenerator = idGenerator;
|
|
7
|
-
exports.storeRequestPromise = storeRequestPromise;
|
|
8
|
-
exports.resolveRequest = resolveRequest;
|
|
9
8
|
exports.rejectRequest = rejectRequest;
|
|
10
|
-
exports.
|
|
9
|
+
exports.resolveRequest = resolveRequest;
|
|
10
|
+
exports.storeRequestPromise = storeRequestPromise;
|
|
11
11
|
// id of the server when owner of a state
|
|
12
12
|
const SERVER_ID = -1;
|
|
13
13
|
exports.SERVER_ID = SERVER_ID;
|
|
@@ -51,6 +51,8 @@ const UPDATE_ABORT = 's:u:ab';
|
|
|
51
51
|
exports.UPDATE_ABORT = UPDATE_ABORT;
|
|
52
52
|
const UPDATE_NOTIFICATION = 's:u:not';
|
|
53
53
|
exports.UPDATE_NOTIFICATION = UPDATE_NOTIFICATION;
|
|
54
|
+
const DELETE_SCHEMA = 's:d:s';
|
|
55
|
+
exports.DELETE_SCHEMA = DELETE_SCHEMA;
|
|
54
56
|
|
|
55
57
|
function* idGenerator() {
|
|
56
58
|
for (let i = 0; true; i++) {
|
|
@@ -4,8 +4,8 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.packBinaryMessage = packBinaryMessage;
|
|
7
|
-
exports.unpackBinaryMessage = unpackBinaryMessage;
|
|
8
7
|
exports.packStringMessage = packStringMessage;
|
|
8
|
+
exports.unpackBinaryMessage = unpackBinaryMessage;
|
|
9
9
|
exports.unpackStringMessage = unpackStringMessage;
|
|
10
10
|
|
|
11
11
|
require("fast-text-encoding");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soundworks/core",
|
|
3
|
-
"version": "3.1.0-beta.
|
|
3
|
+
"version": "3.1.0-beta.5",
|
|
4
4
|
"description": "full-stack javascript framework for distributed audio visual experiences on the web",
|
|
5
5
|
"authors": [
|
|
6
6
|
"Benjamin Matuszewski",
|
|
@@ -32,36 +32,38 @@
|
|
|
32
32
|
"dev": "npm run build && (npm run dev:client & npm run dev:server & npm run dev:common)"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"chalk": "^
|
|
35
|
+
"chalk": "^4.1.2",
|
|
36
36
|
"columnify": "^1.5.4",
|
|
37
37
|
"compression": "^1.7.1",
|
|
38
38
|
"debug": "^4.1.1",
|
|
39
39
|
"fast-deep-equal": "^3.1.3",
|
|
40
40
|
"fast-text-encoding": "^1.0.0",
|
|
41
41
|
"isomorphic-ws": "^4.0.1",
|
|
42
|
-
"
|
|
43
|
-
"keyv": "^
|
|
44
|
-
"keyv-file": "^0.1.13",
|
|
42
|
+
"keyv": "^4.0.3",
|
|
43
|
+
"keyv-file": "^0.2.0",
|
|
45
44
|
"lodash.clonedeep": "^4.5.0",
|
|
46
45
|
"lodash.merge": "^4.6.2",
|
|
47
46
|
"pem": "^1.12.5",
|
|
48
47
|
"polka": "^0.5.2",
|
|
49
48
|
"serve-static": "^1.13.2",
|
|
50
|
-
"
|
|
49
|
+
"uuid": "^8.3.2",
|
|
51
50
|
"window-or-global": "^1.0.1",
|
|
52
|
-
"ws": "^
|
|
51
|
+
"ws": "^8.2.3"
|
|
53
52
|
},
|
|
54
53
|
"devDependencies": {
|
|
55
54
|
"@babel/cli": "^7.4.4",
|
|
56
55
|
"@babel/core": "^7.4.5",
|
|
57
56
|
"@babel/preset-env": "^7.4.5",
|
|
58
|
-
"braintree-jsdoc-template": "^3.3.0",
|
|
59
57
|
"chai": "^4.2.0",
|
|
60
58
|
"chokidar": "^3.0.1",
|
|
61
|
-
"chokidar-cli": "^
|
|
59
|
+
"chokidar-cli": "^3.0.0",
|
|
62
60
|
"docdash": "^1.2.0",
|
|
63
61
|
"jsdoc": "^3.6.2",
|
|
64
62
|
"markdown-toc": "^1.2.0",
|
|
65
|
-
"mocha": "^
|
|
63
|
+
"mocha": "^9.1.3"
|
|
64
|
+
},
|
|
65
|
+
"optionalDependencies": {
|
|
66
|
+
"bufferutil": "^4.0.5",
|
|
67
|
+
"utf-8-validate": "^5.0.7"
|
|
66
68
|
}
|
|
67
69
|
}
|
package/server/Client.js
CHANGED
|
@@ -5,9 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.default = void 0;
|
|
7
7
|
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
8
|
+
var _uuid = require("uuid");
|
|
11
9
|
|
|
12
10
|
function* idGenerator() {
|
|
13
11
|
for (let i = 0; true; i++) {
|
|
@@ -47,7 +45,7 @@ class Client {
|
|
|
47
45
|
* @type {String}
|
|
48
46
|
*/
|
|
49
47
|
|
|
50
|
-
this.uuid = (0,
|
|
48
|
+
this.uuid = (0, _uuid.v4)();
|
|
51
49
|
/**
|
|
52
50
|
* Socket connection with the remote {@link client.Client}.
|
|
53
51
|
* @type {server.Socket}
|
package/server/PluginManager.js
CHANGED
|
@@ -87,7 +87,7 @@ class PluginManager {
|
|
|
87
87
|
register(name, factory = null, options = {}, deps = []) {
|
|
88
88
|
if (this._registeredPlugins[name]) {
|
|
89
89
|
const ctor = factory(_AbstractPlugin.default);
|
|
90
|
-
throw new Error(`[soundworks:core]
|
|
90
|
+
throw new Error(`[soundworks:core] Plugin "${name}" of type "${ctor.name}" already registered`);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
const ctor = factory(_AbstractPlugin.default);
|
|
@@ -106,7 +106,7 @@ class PluginManager {
|
|
|
106
106
|
|
|
107
107
|
get(name, _experience = null) {
|
|
108
108
|
if (!this._registeredPlugins[name]) {
|
|
109
|
-
throw new Error(`Cannot get or require plugin "${name}", plugin is not registered
|
|
109
|
+
throw new Error(`[soundworks:core] Cannot get or require plugin "${name}", plugin is not registered
|
|
110
110
|
> registered plugins are:
|
|
111
111
|
${Object.keys(this._registeredPlugins).map(n => `> - ${n}\n`).join('')}
|
|
112
112
|
`);
|
|
@@ -114,7 +114,7 @@ ${Object.keys(this._registeredPlugins).map(n => `> - ${n}\n`).join('')}
|
|
|
114
114
|
|
|
115
115
|
|
|
116
116
|
if (_experience && this.signals.start.value === true) {
|
|
117
|
-
throw new Error(`Plugin "${name}" required after pluginManager start`);
|
|
117
|
+
throw new Error(`[soundworks:core] Plugin "${name}" required after pluginManager start`);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
if (!this._instances[name]) {
|
package/server/Server.js
CHANGED
|
@@ -264,7 +264,37 @@ class Server {
|
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
this._clientConfigFunction = clientConfigFunction;
|
|
267
|
+
this._clientConfigFunction = clientConfigFunction; // basic http authentication
|
|
268
|
+
|
|
269
|
+
if (this.config.env.auth) {
|
|
270
|
+
this.router.use((req, res, next) => {
|
|
271
|
+
const isProtected = this.config.env.auth.clients.map(type => req.path.endsWith(`/${type}`)).reduce((acc, value) => acc || value, false);
|
|
272
|
+
|
|
273
|
+
if (isProtected) {
|
|
274
|
+
// authentication middleware
|
|
275
|
+
const auth = this.config.env.auth; // parse login and password from headers
|
|
276
|
+
|
|
277
|
+
const b64auth = (req.headers.authorization || '').split(' ')[1] || '';
|
|
278
|
+
const [login, password] = Buffer.from(b64auth, 'base64').toString().split(':'); // verify login and password are set and correct
|
|
279
|
+
|
|
280
|
+
if (login && password && login === auth.login && password === auth.password) {
|
|
281
|
+
// -> access granted...
|
|
282
|
+
return next();
|
|
283
|
+
} // -> access denied...
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
res.writeHead(401, {
|
|
287
|
+
'WWW-Authenticate': 'Basic',
|
|
288
|
+
'Content-Type': 'text/plain'
|
|
289
|
+
});
|
|
290
|
+
res.end('Authentication required.');
|
|
291
|
+
} else {
|
|
292
|
+
// route not protected
|
|
293
|
+
return next();
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
268
298
|
return Promise.resolve();
|
|
269
299
|
}
|
|
270
300
|
/**
|
|
@@ -414,7 +444,7 @@ class Server {
|
|
|
414
444
|
}
|
|
415
445
|
}
|
|
416
446
|
|
|
417
|
-
_logger.default.clientConfigAndRouting(routes, this.config
|
|
447
|
+
_logger.default.clientConfigAndRouting(routes, this.config);
|
|
418
448
|
|
|
419
449
|
return Promise.resolve();
|
|
420
450
|
}).then(() => {
|
|
@@ -512,8 +542,16 @@ class Server {
|
|
|
512
542
|
|
|
513
543
|
if (this.config.env.subpath && !data.env.subpath) {
|
|
514
544
|
data.env.subpath = this.config.env.subpath;
|
|
515
|
-
}
|
|
545
|
+
} // cors / coop / coep headers for `crossOriginIsolated pages, enables
|
|
546
|
+
// sharedArrayBuffers and high precision timers
|
|
547
|
+
// cf. https://web.dev/why-coop-coep/
|
|
548
|
+
|
|
516
549
|
|
|
550
|
+
res.writeHead(200, {
|
|
551
|
+
'Cross-Origin-Resource-Policy': 'same-origin',
|
|
552
|
+
'Cross-Origin-Embedder-Policy': 'require-corp',
|
|
553
|
+
'Cross-Origin-Opener-Policy': 'same-origin'
|
|
554
|
+
});
|
|
517
555
|
const appIndex = tmpl(data);
|
|
518
556
|
res.end(appIndex);
|
|
519
557
|
}); // return route infos for logging on server start
|
package/server/utils/logger.js
CHANGED
|
@@ -19,7 +19,10 @@ const logger = {
|
|
|
19
19
|
console.log(_chalk.default.cyan(`+ ${msg}`));
|
|
20
20
|
},
|
|
21
21
|
|
|
22
|
-
clientConfigAndRouting(routes,
|
|
22
|
+
clientConfigAndRouting(routes, config) {
|
|
23
|
+
const clientsConfig = config.app.clients;
|
|
24
|
+
const servIp = config.env.serverIp;
|
|
25
|
+
const auth = config.env.auth;
|
|
23
26
|
const table = [];
|
|
24
27
|
|
|
25
28
|
for (let clientType in clientsConfig) {
|
|
@@ -27,18 +30,20 @@ const logger = {
|
|
|
27
30
|
|
|
28
31
|
if (client.target === 'node') {
|
|
29
32
|
const line = {
|
|
30
|
-
|
|
33
|
+
client_type: `> ${clientType}`,
|
|
31
34
|
target: _chalk.default.red(client.target),
|
|
32
35
|
path: `server ip: ${_chalk.default.green(serverIp)}`,
|
|
33
|
-
default: undefined
|
|
36
|
+
default: undefined,
|
|
37
|
+
auth: undefined
|
|
34
38
|
};
|
|
35
39
|
table.push(line);
|
|
36
40
|
} else if (client.target === 'browser') {
|
|
37
41
|
const line = {
|
|
38
|
-
|
|
42
|
+
client_type: `> ${clientType}`,
|
|
39
43
|
target: _chalk.default.red(client.target),
|
|
40
44
|
path: routes.find(r => r.clientType === clientType) ? _chalk.default.green(routes.find(r => r.clientType === clientType).path) : _chalk.default.red('no route defined'),
|
|
41
|
-
default: client.default ? '
|
|
45
|
+
default: client.default ? 'x' : undefined,
|
|
46
|
+
auth: auth && auth.clients.indexOf(clientType) !== -1 ? 'x' : undefined
|
|
42
47
|
};
|
|
43
48
|
table.push(line);
|
|
44
49
|
} else {
|
|
@@ -46,10 +51,21 @@ const logger = {
|
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
53
|
|
|
54
|
+
console.log(``);
|
|
49
55
|
console.log((0, _columnify.default)(table, {
|
|
50
|
-
showHeaders:
|
|
51
|
-
minWidth:
|
|
52
|
-
|
|
56
|
+
showHeaders: true,
|
|
57
|
+
minWidth: 6,
|
|
58
|
+
columnSplitter: ' | ',
|
|
59
|
+
config: {
|
|
60
|
+
default: {
|
|
61
|
+
align: 'center'
|
|
62
|
+
},
|
|
63
|
+
auth: {
|
|
64
|
+
align: 'center'
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}));
|
|
68
|
+
console.log(``); // check if a route is defined but not in config
|
|
53
69
|
|
|
54
70
|
const configClientTypes = Object.keys(clientsConfig);
|
|
55
71
|
routes.forEach(r => {
|