@soundworks/core 4.0.0-alpha.19 → 4.0.0-alpha.20

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/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## v4.0.0-alpha.20 - 19/02/2024
2
+
3
+ - Refactor heartbeat - #86
4
+ - Refactor state collection - #85
5
+ - Fix issue with self signed certs and config generated by wizard - #84
6
+
1
7
  ## v4.0.0-alpha.19 - 17/01/2024
2
8
 
3
9
  - Hotfix for #85
package/README.md CHANGED
@@ -24,7 +24,7 @@ npx @soundworks/create@latest
24
24
 
25
25
  ![soundworks-create](./misc/assets/soundworks-create-min.gif)
26
26
 
27
- See [https://soundworks.dev/tutorials/getting-started.html](https://soundworks.dev/tutorials/getting-started.html) for more informations on the wizard and how to start using **soundworks**.
27
+ See [https://soundworks.dev/tutorials/getting-started.html](https://soundworks.dev/tutorials/getting-started.html) for more information on the wizard and how to start using **soundworks**.
28
28
 
29
29
  ## Misc
30
30
 
@@ -36,7 +36,7 @@ However, for maintenance reasons, we aim at following the TC39 and W3C specifica
36
36
 
37
37
  ### Manual install
38
38
 
39
- Note that the `@soundworks/core` package is automatically installed when creating an application using the `@soundworks/create` wizard, so most of the time you should not have to install this package manually. See [https://soundworks.dev/guides/getting-started.html](https://soundworks.dev/guides/getting-started.html) for more informations on the **soundworks** wizard.
39
+ Note that the `@soundworks/core` package is automatically installed when creating an application using the `@soundworks/create` wizard, so most of the time you should not have to install this package manually. See [https://soundworks.dev/guides/getting-started.html](https://soundworks.dev/guides/getting-started.html) for more information on the **soundworks** wizard.
40
40
 
41
41
  ```
42
42
  npm install @soundworks/core
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soundworks/core",
3
- "version": "4.0.0-alpha.19",
3
+ "version": "4.0.0-alpha.20",
4
4
  "description": "Open-source creative coding framework for distributed applications based on Web technologies",
5
5
  "authors": [
6
6
  "Benjamin Matuszewski",
@@ -1,13 +1,19 @@
1
1
  import { isBrowser } from '@ircam/sc-utils';
2
2
  import WebSocket from 'isomorphic-ws';
3
3
 
4
+ import {
5
+ PING_INTERVAL,
6
+ PING_LATENCY_TOLERANCE,
7
+ PING_MESSAGE,
8
+ PONG_MESSAGE,
9
+ } from '../common/constants.js';
10
+ import logger from '../common/logger.js';
4
11
  import {
5
12
  packBinaryMessage,
6
13
  unpackBinaryMessage,
7
14
  packStringMessage,
8
15
  unpackStringMessage,
9
16
  } from '../common/sockets-utils.js';
10
- import logger from '../common/logger.js';
11
17
 
12
18
  // WebSocket events:
13
19
  //
@@ -64,6 +70,8 @@ class Socket {
64
70
  this._stringListeners = new Map();
65
71
  /** @private */
66
72
  this._binaryListeners = new Map();
73
+ /** @private */
74
+ this._heartbeatId = null;
67
75
  }
68
76
 
69
77
  /**
@@ -130,20 +138,51 @@ class Socket {
130
138
  ws.addEventListener('open', connectEvent => {
131
139
  // parse incoming messages for pubsub
132
140
  this.ws = ws;
141
+ // ping / pong to check connection
142
+ const heartbeat = () => {
143
+ clearTimeout(this._heartbeatId);
144
+
145
+ this._heartbeatId = setTimeout(() => {
146
+ this.terminate();
147
+ }, PING_INTERVAL + PING_LATENCY_TOLERANCE);
148
+ };
149
+
150
+ heartbeat();
151
+
133
152
  this.ws.addEventListener('message', e => {
153
+ if (e.data === PING_MESSAGE) {
154
+ heartbeat();
155
+ this.ws.send(PONG_MESSAGE);
156
+ // do not propagate ping / pong messages
157
+ return;
158
+ }
159
+
134
160
  const [channel, args] = unpackStringMessage(e.data);
135
161
  this._emit(false, channel, ...args);
136
162
  });
137
163
 
138
164
  // broadcast all `WebSocket` native events
139
- ['close', 'error', 'upgrade', 'message'].forEach(eventName => {
140
- this.ws.addEventListener(eventName, (e) => {
141
- this._emit(false, eventName, e);
142
- });
165
+ this.ws.addEventListener('close', (e) => {
166
+ clearTimeout(this._heartbeatId);
167
+ this._emit(false, 'close', e);
168
+ });
169
+
170
+ this.ws.addEventListener('error', (e) => {
171
+ clearTimeout(this._heartbeatId);
172
+ this._emit(false, 'error', e);
173
+ });
174
+
175
+ this.ws.addEventListener('upgrade', (e) => {
176
+ this._emit(false, 'upgrade', e);
177
+ });
178
+
179
+ this.ws.addEventListener('message', (e) => {
180
+ this._emit(false, 'message', e);
143
181
  });
144
182
 
145
183
  // forward open event
146
184
  this._emit(false, 'open', connectEvent);
185
+
147
186
  // continue with raw socket
148
187
  resolve();
149
188
  });
@@ -173,36 +212,6 @@ class Socket {
173
212
  trySocket();
174
213
  });
175
214
 
176
- // @todo - review/fix
177
- // - the `ws.on` method only exists on node implementation, and the 'ping'
178
- // message is not received on addEventListener
179
- // - there seems to be no way to access the ping event in browsers...
180
- //
181
- // let pingTimeoutId = null;
182
- // const pingInterval = config.env.websockets.pingInterval;
183
- // // detect broken connection
184
- // // cf. https://github.com/websockets/ws#how-to-detect-and-close-broken-connections
185
- // const heartbeat = () => {
186
- // try {
187
- // console.log('ping received');
188
- // clearTimeout(pingTimeoutId);
189
-
190
- // // pingTimeoutId = setTimeout(() => {
191
- // // console.log('terminate');
192
- // // this.terminate();
193
- // // }, pingInterval + 2000);
194
- // } catch (err) {
195
- // console.error(err);
196
- // }
197
- // };
198
- //
199
- // this.ws.on('ping', heartbeat);
200
- // this.ws.addEventListener('close', () => {
201
- // clearTimeout(pingTimeoutId);
202
- // });
203
-
204
- // heartbeat();
205
-
206
215
  // ----------------------------------------------------------
207
216
  // init binary socket
208
217
  // ----------------------------------------------------------
@@ -1,8 +1,3 @@
1
- import {
2
- kCreateCollectionController,
3
- kAttachInCollection,
4
- } from './BaseStateManager.js';
5
-
6
1
  /** @private */
7
2
  class BaseSharedStateCollection {
8
3
  constructor(stateManager, schemaName, options = {}) {
@@ -10,7 +5,7 @@ class BaseSharedStateCollection {
10
5
  this._schemaName = schemaName;
11
6
  this._options = Object.assign({ excludeLocal: false }, options);
12
7
 
13
- this._controller = null;
8
+ this._schema = null;
14
9
  this._states = [];
15
10
 
16
11
  this._onUpdateCallbacks = new Set();
@@ -21,15 +16,10 @@ class BaseSharedStateCollection {
21
16
 
22
17
  /** @private */
23
18
  async _init() {
24
- this._controller = await this._stateManager[kCreateCollectionController](this._schemaName);
25
- this._controller.onUpdate(async (updates, context) => {
26
- for (let state of this._states) {
27
- await state._commit(updates, context, true, false);
28
- }
29
- });
19
+ this._schema = await this._stateManager.getSchema(this._schemaName);
30
20
 
31
21
  this._unobserve = await this._stateManager.observe(this._schemaName, async (schemaName, stateId) => {
32
- const state = await this._stateManager[kAttachInCollection](schemaName, stateId);
22
+ const state = await this._stateManager.attach(schemaName, stateId);
33
23
  this._states.push(state);
34
24
 
35
25
  state.onDetach(() => {
@@ -52,6 +42,7 @@ class BaseSharedStateCollection {
52
42
  /**
53
43
  * Size of the collection, alias `size`
54
44
  * @type {number}
45
+ * @readonly
55
46
  */
56
47
  get length() {
57
48
  return this._states.length;
@@ -60,6 +51,7 @@ class BaseSharedStateCollection {
60
51
  /**
61
52
  * Size of the collection, , alias `length`
62
53
  * @type {number}
54
+ * @readonly
63
55
  */
64
56
  get size() {
65
57
  return this._states.length;
@@ -71,7 +63,7 @@ class BaseSharedStateCollection {
71
63
  * @readonly
72
64
  */
73
65
  get schemaName() {
74
- return this._controller.schemaName;
66
+ return this._schemaName;
75
67
  }
76
68
 
77
69
  /**
@@ -86,7 +78,11 @@ class BaseSharedStateCollection {
86
78
  * const schema = collection.getSchema();
87
79
  */
88
80
  getSchema(name = null) {
89
- return this._controller.getSchema(name);
81
+ if (name) {
82
+ return this._schema[name];
83
+ }
84
+
85
+ return this._schema;
90
86
  }
91
87
 
92
88
  /**
@@ -97,7 +93,11 @@ class BaseSharedStateCollection {
97
93
  * const defaults = state.getDefaults();
98
94
  */
99
95
  getDefaults() {
100
- return this._controller.getDefaults();
96
+ const defaults = {};
97
+ for (let name in this._schema) {
98
+ defaults[name] = this._schema[name].default;
99
+ }
100
+ return defaults;
101
101
  }
102
102
 
103
103
  /**
@@ -130,7 +130,6 @@ class BaseSharedStateCollection {
130
130
  // to be cleaned soon
131
131
  const promises = this._states.map(state => state.set(updates, context));
132
132
  return Promise.all(promises);
133
- // return await this._controller.set(updates, context);
134
133
  }
135
134
 
136
135
  /**
@@ -199,8 +198,6 @@ class BaseSharedStateCollection {
199
198
  this._unobserve();
200
199
  this._onUpdateCallbacks.clear();
201
200
 
202
- await this._controller.delete();
203
-
204
201
  const promises = this._states.map(state => state.detach());
205
202
  await Promise.all(promises);
206
203
 
@@ -2,6 +2,7 @@ import { isString, isFunction, isPlainObject } from '@ircam/sc-utils';
2
2
  import SharedState from './BaseSharedState.js';
3
3
  import SharedStateCollection from './BaseSharedStateCollection.js';
4
4
  import BatchedTransport from './BatchedTransport.js';
5
+ import ParameterBag from './ParameterBag.js';
5
6
  import PromiseStore from './PromiseStore.js';
6
7
  import {
7
8
  CREATE_REQUEST,
@@ -16,11 +17,11 @@ import {
16
17
  OBSERVE_NOTIFICATION,
17
18
  UNOBSERVE_NOTIFICATION,
18
19
  DELETE_SCHEMA,
20
+ GET_SCHEMA_REQUEST,
21
+ GET_SCHEMA_RESPONSE,
22
+ GET_SCHEMA_ERROR,
19
23
  } from './constants.js';
20
24
 
21
- export const kCreateCollectionController = Symbol('BaseStateManager::createCollectionController');
22
- export const kAttachInCollection = Symbol('BaseStateManager::attachInCollection');
23
-
24
25
  /**
25
26
  * @private
26
27
  */
@@ -150,6 +151,22 @@ class BaseStateManager {
150
151
  this.client.transport.addListener(DELETE_SCHEMA, schemaName => {
151
152
  this._cachedSchemas.delete(schemaName);
152
153
  });
154
+
155
+ // ---------------------------------------------
156
+ // GET SCHEMA
157
+ // ---------------------------------------------
158
+ this.client.transport.addListener(GET_SCHEMA_RESPONSE, (reqId, schemaName, schema) => {
159
+ if (!this._cachedSchemas.has(schemaName)) {
160
+ this._cachedSchemas.set(schemaName, schema);
161
+ }
162
+ // return a populated schema
163
+ const parameterBag = new ParameterBag(schema);
164
+ this._promiseStore.resolve(reqId, parameterBag.getSchema());
165
+ });
166
+
167
+ this.client.transport.addListener(GET_SCHEMA_ERROR, (reqId, msg) => {
168
+ this._promiseStore.reject(reqId, msg);
169
+ });
153
170
  }
154
171
 
155
172
  /** @private */
@@ -167,6 +184,28 @@ class BaseStateManager {
167
184
  return filter;
168
185
  }
169
186
 
187
+ /**
188
+ * Return the schema from a given registered schema name
189
+ *
190
+ * @param {String} schemaName - Name of the schema as given on registration
191
+ * (cf. ServerStateManager)
192
+ * @example
193
+ * const schema = await client.stateManager.getSchema('my-schema');
194
+ */
195
+ async getSchema(schemaName) {
196
+ if (this._cachedSchemas.has(schemaName)) {
197
+ const schema = this._cachedSchemas.get(schemaName);
198
+ // return a populated schema
199
+ const parameterBag = new ParameterBag(schema);
200
+ return parameterBag.getSchema();
201
+ }
202
+
203
+ return new Promise((resolve, reject) => {
204
+ const reqId = this._promiseStore.add(resolve, reject, 'get-schema');
205
+ this.client.transport.emit(GET_SCHEMA_REQUEST, reqId, schemaName);
206
+ });
207
+ }
208
+
170
209
  /**
171
210
  * Create a `SharedState` instance from a registered schema.
172
211
  *
@@ -187,18 +226,6 @@ class BaseStateManager {
187
226
  });
188
227
  }
189
228
 
190
-
191
- /**
192
- * @private
193
- */
194
- async [kCreateCollectionController](schemaName) {
195
- return new Promise((resolve, reject) => {
196
- const reqId = this._promiseStore.add(resolve, reject, 'create-collection-controller-request');
197
- const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
198
- this.client.transport.emit(CREATE_REQUEST, reqId, schemaName, requireSchema, {}, true);
199
- });
200
- }
201
-
202
229
  /**
203
230
  * Attach to an existing `SharedState` instance.
204
231
  *
@@ -221,14 +248,6 @@ class BaseStateManager {
221
248
  });
222
249
  }
223
250
 
224
- async [kAttachInCollection](schemaName, stateId) {
225
- return new Promise((resolve, reject) => {
226
- const reqId = this._promiseStore.add(resolve, reject, 'attach-in-collection-request');
227
- const requireSchema = this._cachedSchemas.has(schemaName) ? false : true;
228
- this.client.transport.emit(ATTACH_REQUEST, reqId, schemaName, stateId, requireSchema, true);
229
- });
230
- }
231
-
232
251
  /**
233
252
  * Observe all the `SharedState` instances that are created on the network.
234
253
  * This can be usefull for clients with some controller role that might want to track
@@ -391,7 +410,7 @@ class BaseStateManager {
391
410
  await collection._init();
392
411
  } catch (err) {
393
412
  console.log(err.message);
394
- throw new Error(`Cannot create collection, schema "${schemaName}" does not exists`);
413
+ throw new Error(`[stateManager] Cannot create collection, schema "${schemaName}" does not exists`);
395
414
  }
396
415
 
397
416
  return collection;
@@ -226,7 +226,6 @@ class ParameterBag {
226
226
 
227
227
  this._schema[name].initValue = initValue;
228
228
  this._values[name] = initValue;
229
-
230
229
  }
231
230
  }
232
231
 
@@ -14,13 +14,13 @@ import {
14
14
  /**
15
15
  * The "real" state, this instance is kept private by the server.StateManager.
16
16
  * It can only be accessed through a SharedState proxy.
17
+ *
17
18
  * @private
18
19
  */
19
20
  class SharedStatePrivate {
20
21
  constructor(id, schemaName, schema, manager, initValues = {}) {
21
22
  this.id = id;
22
23
  this.schemaName = schemaName;
23
- this.isCollectionController = false;
24
24
 
25
25
  this._manager = manager;
26
26
  this._parameters = new ParameterBag(schema, initValues);
@@ -30,16 +30,11 @@ class SharedStatePrivate {
30
30
  this._creatorId = null;
31
31
  }
32
32
 
33
- _attachClient(remoteId, client, isOwner = false, inCollection = false) {
34
-
35
- // isOwner && inCollection -> collection controller
36
- // !isOwner && inCollection -> attached in collection
37
-
38
- const clientInfos = { client, isOwner, inCollection };
33
+ _attachClient(remoteId, client, isOwner = false) {
34
+ const clientInfos = { client, isOwner };
39
35
  this._attachedClients.set(remoteId, clientInfos);
40
36
 
41
37
  if (isOwner) {
42
- this.isCollectionController = inCollection;
43
38
  this._creatorRemoteId = remoteId;
44
39
  this._creatorId = client.id;
45
40
  }
@@ -87,96 +82,63 @@ class SharedStatePrivate {
87
82
  }
88
83
 
89
84
  if (hasUpdates) {
90
-
91
- // Collection controller logic
92
- if (isOwner && inCollection) {
93
- // notify the collection controller back
85
+ // Normal case
86
+ //
87
+ // We need to handle cases where:
88
+ // - client state (client.id: 2) sends a request
89
+ // - server attached state (client.id: -1) spot a problem and overrides the value
90
+ // We want the remote client (id: 2) to receive in the right order:
91
+ // * 1. the value it requested,
92
+ // * 2. the value overriden by the server-side attached state (id: -1)
93
+ //
94
+ // such problem is now better solved with the the upateHook system, none
95
+ // nonetheway we don't want to introduce inconsistencies here
96
+ //
97
+ // Then we propagate server-side last, because as the server transport
98
+ // is synchronous it can break ordering if a subscription function makes
99
+ // itself an update in reaction to an update. Propagating to server last
100
+ // alllows to maintain network messages order consistent.
101
+ //
102
+ // @note - remoteId correspond to unique remote state id
103
+
104
+ // propagate RESPONSE to the client that originates the request if not the server
105
+ if (client.id !== -1) {
94
106
  client.transport.emit(
95
107
  `${UPDATE_RESPONSE}-${this.id}-${remoteId}`,
96
108
  reqId, filteredUpdates, context,
97
109
  );
110
+ }
98
111
 
99
- // loop through all private states
100
- for (let [_stateId, state] of this._manager._sharedStatePrivateById) {
101
- // pick all states with same schema name and not this
102
- if (state !== this && this.schemaName === state.schemaName) {
103
- // notify all attached clients except those who belong to a collection
104
- // i.e. !isOwner && inCollection, they will be notified by their own
105
- // collection controller
106
- for (let [remoteId, clientInfos] of state._attachedClients) {
107
- if (!clientInfos.isOwner && clientInfos.inCollection) {
108
- continue;
109
- } else {
110
- const peer = clientInfos.client;
111
-
112
- peer.transport.emit(
113
- `${UPDATE_NOTIFICATION}-${state.id}-${remoteId}`,
114
- filteredUpdates, context,
115
- );
116
- }
117
- }
118
- }
119
- }
120
- } else {
112
+ // propagate NOTIFICATION to all other attached clients except server
113
+ for (let [peerRemoteId, clientInfos] of this._attachedClients) {
114
+ const peer = clientInfos.client;
121
115
 
122
- // Normal case
123
- //
124
- // We need to handle cases where:
125
- // - client state (client.id: 2) sends a request
126
- // - server attached state (client.id: -1) spot a problem and overrides the value
127
- // We want the remote client (id: 2) to receive in the right order:
128
- // * 1. the value it requested,
129
- // * 2. the value overriden by the server-side attached state (id: -1)
130
- //
131
- // such problem is now better solved with the the upateHook system, none
132
- // nonetheway we don't want to introduce inconsistencies here
133
- //
134
- // Then we propagate server-side last, because as the server transport
135
- // is synchronous it can break ordering if a subscription function makes
136
- // itself an update in reaction to an update. Propagating to server last
137
- // alllows to maintain network messages order consistent.
138
- //
139
- // @note - remoteId correspond to unique remote state id
140
-
141
- // propagate RESPONSE to the client that originates the request if not the server
142
- if (client.id !== -1) {
143
- client.transport.emit(
144
- `${UPDATE_RESPONSE}-${this.id}-${remoteId}`,
145
- reqId, filteredUpdates, context,
116
+ if (remoteId !== peerRemoteId && peer.id !== -1) {
117
+ peer.transport.emit(
118
+ `${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`,
119
+ filteredUpdates, context,
146
120
  );
147
121
  }
122
+ }
148
123
 
149
- // propagate NOTIFICATION to all other attached clients except server
150
- for (let [peerRemoteId, clientInfos] of this._attachedClients) {
151
- const peer = clientInfos.client;
124
+ // propagate RESPONSE to server if it is the requester
125
+ if (client.id === -1) {
126
+ client.transport.emit(
127
+ `${UPDATE_RESPONSE}-${this.id}-${remoteId}`,
128
+ reqId, filteredUpdates, context,
129
+ );
130
+ }
152
131
 
153
- if (remoteId !== peerRemoteId && peer.id !== -1) {
154
- peer.transport.emit(
155
- `${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`,
156
- filteredUpdates, context,
157
- );
158
- }
159
- }
132
+ // propagate NOTIFICATION other attached state that belongs to server
133
+ for (let [peerRemoteId, clientInfos] of this._attachedClients) {
134
+ const peer = clientInfos.client;
160
135
 
161
- // propagate RESPONSE to server if it is the requester
162
- if (client.id === -1) {
163
- client.transport.emit(
164
- `${UPDATE_RESPONSE}-${this.id}-${remoteId}`,
165
- reqId, filteredUpdates, context,
136
+ if (remoteId !== peerRemoteId && peer.id === -1) {
137
+ peer.transport.emit(
138
+ `${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`,
139
+ filteredUpdates, context,
166
140
  );
167
141
  }
168
-
169
- // propagate NOTIFICATION other attached state that belongs to server
170
- for (let [peerRemoteId, clientInfos] of this._attachedClients) {
171
- const peer = clientInfos.client;
172
-
173
- if (remoteId !== peerRemoteId && peer.id === -1) {
174
- peer.transport.emit(
175
- `${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`,
176
- filteredUpdates, context,
177
- );
178
- }
179
- }
180
142
  }
181
143
  } else {
182
144
  // propagate back to the requester that the update has been aborted
@@ -1,6 +1,11 @@
1
1
  // id of the server when owner of a state
2
2
  export const SERVER_ID = -1;
3
3
 
4
+ export const PING_INTERVAL = 10 * 1000;
5
+ export const PING_LATENCY_TOLERANCE = 1000;
6
+ export const PING_MESSAGE = 'h:ping';
7
+ export const PONG_MESSAGE = 'h:pong';
8
+
4
9
  // batched transport channel
5
10
  export const BATCHED_TRANSPORT_CHANNEL = 'b:t';
6
11
 
@@ -36,6 +41,10 @@ export const UPDATE_NOTIFICATION = 's:u:not';
36
41
 
37
42
  export const DELETE_SCHEMA = 's:d:s';
38
43
 
44
+ export const GET_SCHEMA_REQUEST = 's:s:req';
45
+ export const GET_SCHEMA_RESPONSE = 's:s:res';
46
+ export const GET_SCHEMA_ERROR = 's:s:err';
47
+
39
48
  // context channels
40
49
  export const CONTEXT_ENTER_REQUEST = 'c:en:req';
41
50
  export const CONTEXT_ENTER_RESPONSE = 'c:en:res';
@@ -9,6 +9,7 @@ import { isPlainObject, idGenerator, getTime } from '@ircam/sc-utils';
9
9
  import chalk from 'chalk';
10
10
  import compression from 'compression';
11
11
  import express from 'express';
12
+ import equal from 'fast-deep-equal';
12
13
  import Keyv from 'keyv';
13
14
  import { KeyvFile } from 'keyv-file';
14
15
  import merge from 'lodash/merge.js';
@@ -214,11 +215,11 @@ class Server {
214
215
  throw new Error(`[soundworks:Server] Invalid "env.httpsInfos" config, should contain both "cert" and "key" entries`);
215
216
  }
216
217
  // @todo - move that to constructor
217
- if (!fs.existsSync(httpsInfos.cert)) {
218
+ if (httpsInfos.cert !== null && !fs.existsSync(httpsInfos.cert)) {
218
219
  throw new Error(`[soundworks:Server] Invalid "env.httpsInfos" config, "cert" file not found`);
219
220
  }
220
221
 
221
- if (!fs.existsSync(httpsInfos.key)) {
222
+ if (httpsInfos.key !== null && !fs.existsSync(httpsInfos.key)) {
222
223
  throw new Error(`[soundworks:Server] Invalid "env.httpsInfos" config, "key" file not found`);
223
224
  }
224
225
  }
@@ -444,19 +445,22 @@ class Server {
444
445
  this.router.use(soundworksAuth);
445
446
  }
446
447
 
447
- // start http server
448
- const useHttps = this.config.env.useHttps || false;
449
-
450
448
  // ------------------------------------------------------------
451
449
  // create HTTP(S) SERVER
452
450
  // ------------------------------------------------------------
451
+ const useHttps = this.config.env.useHttps || false;
452
+
453
453
  if (!useHttps) {
454
454
  this.httpServer = http.createServer(this.router);
455
455
  } else {
456
456
  const httpsInfos = this.config.env.httpsInfos;
457
+ let useSelfSigned = false;
458
+
459
+ if (!httpsInfos || equal(httpsInfos, { cert: null, key: null })) {
460
+ useSelfSigned = true;
461
+ }
457
462
 
458
- // if certs have been given in config
459
- if (httpsInfos !== null) {
463
+ if (!useSelfSigned) {
460
464
  try {
461
465
  // existance of file is checked in contructor
462
466
  let cert = fs.readFileSync(httpsInfos.cert);
@@ -1,4 +1,9 @@
1
1
  import { getTime } from '@ircam/sc-utils';
2
+ import {
3
+ PING_INTERVAL,
4
+ PING_MESSAGE,
5
+ PONG_MESSAGE,
6
+ } from '../common/constants.js';
2
7
  import {
3
8
  packBinaryMessage,
4
9
  unpackBinaryMessage,
@@ -32,17 +37,7 @@ import {
32
37
  * @hideconstructor
33
38
  */
34
39
  class Socket {
35
- constructor(ws, binaryWs, rooms, sockets, options = {}) {
36
- /**
37
- * Configuration object
38
- *
39
- * @type {object}
40
- */
41
- this.config = {
42
- pingInterval: 5 * 1000,
43
- ...options,
44
- };
45
-
40
+ constructor(ws, binaryWs, rooms, sockets) {
46
41
  /**
47
42
  * `ws` socket instance configured with `binaryType=blob` (string)
48
43
  *
@@ -81,16 +76,53 @@ class Socket {
81
76
  this._stringListeners = new Map();
82
77
  /** @private */
83
78
  this._binaryListeners = new Map();
79
+ /** @private */
80
+ this._heartbeatId = null;
81
+
82
+ // heartbeat system (run only on string socket), adapted from:
83
+ // https://github.com/websockets/ws#how-to-detect-and-close-broken-connections
84
+ /** @private */
85
+ this._isAlive = true;
86
+ let msg = {
87
+ type: 'add-measurement',
88
+ value: {
89
+ ping: 0,
90
+ pong: 0,
91
+ },
92
+ };
84
93
 
85
94
  // ----------------------------------------------------------
86
- // init string socket
95
+ // String socket
96
+ // implements ping/pong behavior
87
97
  // ----------------------------------------------------------
88
98
  this.ws.addEventListener('message', e => {
99
+ if (e.data === PONG_MESSAGE) {
100
+ this._isAlive = true;
101
+
102
+ msg.value.pong = getTime();
103
+ this.sockets._latencyStatsWorker.postMessage(msg);
104
+ // do not propagate ping / pong messages
105
+ return;
106
+ }
107
+
89
108
  const [channel, args] = unpackStringMessage(e.data);
90
109
  this._emit(false, channel, ...args);
91
110
  });
92
111
 
93
- // broadcast all `ws` "native" events
112
+ this._heartbeatId = setInterval(() => {
113
+ if (this._isAlive === false) {
114
+ // emit a 'close' event to go trough all the disconnection pipeline
115
+ this._emit(false, 'close');
116
+ return;
117
+ }
118
+
119
+ this._isAlive = false;
120
+ msg.value.ping = getTime();
121
+
122
+ this.ws.send(PING_MESSAGE);
123
+ }, PING_INTERVAL);
124
+
125
+ // broadcast all "native" events
94
126
  [
95
127
  'close',
96
128
  'error',
@@ -107,14 +139,14 @@ class Socket {
107
139
  });
108
140
 
109
141
  // ----------------------------------------------------------
110
- // init binary socket
142
+ // Binary socket
111
143
  // ----------------------------------------------------------
112
144
  this.binaryWs.addEventListener('message', e => {
113
145
  const [channel, data] = unpackBinaryMessage(e.data);
114
146
  this._emit(true, channel, data);
115
147
  });
116
148
 
117
- // broadcast all `ws` "native" events
149
+ // broadcast all "native" events
118
150
  [
119
151
  'close',
120
152
  'error',
@@ -129,38 +161,6 @@ class Socket {
129
161
  this._emit(true, eventName, e.data);
130
162
  });
131
163
  });
132
-
133
- // heartbeat system (run only on string socket), adapted from:
134
- // https://github.com/websockets/ws#how-to-detect-and-close-broken-connections
135
- this._isAlive = true;
136
- let msg = {
137
- type: 'add-measurement',
138
- value: {
139
- ping: 0,
140
- pong: 0,
141
- },
142
- };
143
-
144
- // heartbeat system, only on "regular" socket
145
- this.ws.on('pong', () => {
146
- this._isAlive = true;
147
-
148
- msg.value.pong = getTime();
149
- this.sockets._latencyStatsWorker.postMessage(msg);
150
- });
151
-
152
- this._intervalId = setInterval(() => {
153
- if (this._isAlive === false) {
154
- // emit a 'close' event to go trough all the disconnection pipeline
155
- this._emit(false, 'close');
156
- return;
157
- }
158
-
159
- this._isAlive = false;
160
- msg.value.ping = getTime();
161
-
162
- this.ws.ping();
163
- }, this.config.pingInterval);
164
164
  }
165
165
 
166
166
  /**
@@ -171,7 +171,8 @@ class Socket {
171
171
  */
172
172
  terminate() {
173
173
  // clear ping/pong check
174
- clearInterval(this._intervalId);
174
+ clearInterval(this._heartbeatId);
175
+
175
176
  // clean rooms
176
177
  for (let [_key, room] of this.rooms) {
177
178
  room.delete(this);
@@ -22,6 +22,9 @@ import {
22
22
  OBSERVE_NOTIFICATION,
23
23
  UNOBSERVE_NOTIFICATION,
24
24
  DELETE_SCHEMA,
25
+ GET_SCHEMA_REQUEST,
26
+ GET_SCHEMA_RESPONSE,
27
+ GET_SCHEMA_ERROR,
25
28
  PRIVATE_STATES,
26
29
  } from '../common/constants.js';
27
30
 
@@ -355,7 +358,7 @@ class StateManager extends BaseStateManager {
355
358
  // ---------------------------------------------
356
359
  client.transport.addListener(
357
360
  CREATE_REQUEST,
358
- (reqId, schemaName, requireSchema, initValues = {}, inCollection = false) => {
361
+ (reqId, schemaName, requireSchema, initValues = {}) => {
359
362
  if (this._schemas.has(schemaName)) {
360
363
  try {
361
364
  const schema = this._schemas.get(schemaName);
@@ -365,7 +368,7 @@ class StateManager extends BaseStateManager {
365
368
 
366
369
  // attach client to the state as owner
367
370
  const isOwner = true;
368
- state._attachClient(remoteId, client, isOwner, inCollection);
371
+ state._attachClient(remoteId, client, isOwner);
369
372
 
370
373
  this._sharedStatePrivateById.set(stateId, state);
371
374
 
@@ -385,13 +388,13 @@ class StateManager extends BaseStateManager {
385
388
  });
386
389
  }
387
390
  } catch (err) {
388
- const msg = `Cannot create state "${schemaName}", ${err.message}`;
391
+ const msg = `[stateManager] Cannot create state "${schemaName}", ${err.message}`;
389
392
  console.error(msg);
390
393
 
391
394
  client.transport.emit(CREATE_ERROR, reqId, msg);
392
395
  }
393
396
  } else {
394
- const msg = `Cannot create state "${schemaName}", schema does not exists`;
397
+ const msg = `[stateManager] Cannot create state "${schemaName}", schema does not exists`;
395
398
  console.error(msg);
396
399
 
397
400
  client.transport.emit(CREATE_ERROR, reqId, msg);
@@ -404,7 +407,7 @@ class StateManager extends BaseStateManager {
404
407
  // ---------------------------------------------
405
408
  client.transport.addListener(
406
409
  ATTACH_REQUEST,
407
- (reqId, schemaName, stateId = null, requireSchema = true, inCollection = false) => {
410
+ (reqId, schemaName, stateId = null, requireSchema = true) => {
408
411
  if (this._schemas.has(schemaName)) {
409
412
  let state = null;
410
413
 
@@ -429,7 +432,7 @@ class StateManager extends BaseStateManager {
429
432
  // i.e. same state -> several remote attach on the same node
430
433
  const remoteId = generateRemoteId.next().value;
431
434
  const isOwner = false;
432
- state._attachClient(remoteId, client, isOwner, inCollection);
435
+ state._attachClient(remoteId, client, isOwner);
433
436
 
434
437
  const currentValues = state._parameters.getValues();
435
438
  const schema = this._schemas.get(schemaName);
@@ -441,13 +444,13 @@ class StateManager extends BaseStateManager {
441
444
  );
442
445
 
443
446
  } else {
444
- const msg = `Cannot attach, no existing state for schema "${schemaName}" with stateId: "${stateId}"`;
447
+ const msg = `[stateManager] Cannot attach, no existing state for schema "${schemaName}" with stateId: "${stateId}"`;
445
448
  console.error(msg);
446
449
 
447
450
  client.transport.emit(ATTACH_ERROR, reqId, msg);
448
451
  }
449
452
  } else {
450
- const msg = `Cannot attach, schema "${schemaName}" does not exists`;
453
+ const msg = `[stateManager] Cannot attach, schema "${schemaName}" does not exists`;
451
454
  console.error(msg);
452
455
 
453
456
  client.transport.emit(ATTACH_ERROR, reqId, msg);
@@ -477,7 +480,7 @@ class StateManager extends BaseStateManager {
477
480
 
478
481
  client.transport.emit(OBSERVE_RESPONSE, reqId, ...statesInfos);
479
482
  } else {
480
- const msg = `Cannot observe, schema "${observedSchemaName}" does not exists`;
483
+ const msg = `[stateManager] Cannot observe, schema "${observedSchemaName}" does not exists`;
481
484
  client.transport.emit(OBSERVE_ERROR, reqId, msg);
482
485
  }
483
486
  });
@@ -485,6 +488,19 @@ class StateManager extends BaseStateManager {
485
488
  client.transport.addListener(UNOBSERVE_NOTIFICATION, () => {
486
489
  this._observers.delete(client);
487
490
  });
491
+
492
+ // ---------------------------------------------
493
+ // GET SCHEMA
494
+ // ---------------------------------------------
495
+ client.transport.addListener(GET_SCHEMA_REQUEST, (reqId, schemaName) => {
496
+ if (this._schemas.has(schemaName)) {
497
+ const schema = this._schemas.get(schemaName);
498
+ client.transport.emit(GET_SCHEMA_RESPONSE, reqId, schemaName, schema);
499
+ } else {
500
+ const msg = `[stateManager] Cannot get schema, schema "${schemaName}" does not exists`;
501
+ client.transport.emit(GET_SCHEMA_ERROR, reqId, msg);
502
+ }
503
+ });
488
504
  }
489
505
 
490
506
  /**