@soundworks/core 4.0.0-alpha.4 → 4.0.0-alpha.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.
@@ -0,0 +1,15 @@
1
+ name: "Unit Tests"
2
+ on: push
3
+
4
+ jobs:
5
+ test:
6
+ runs-on: ubuntu-latest
7
+ steps:
8
+ - name: Checkout
9
+ uses: actions/checkout@v3.3.0
10
+ - name: 'Install Node'
11
+ uses: actions/setup-node@v3.6.0
12
+ - name: Install dependencies
13
+ run: npm install
14
+ - name: "Run Test"
15
+ run: npm test -- tests/**.spec.js
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # `soundworks`
1
+ # soundworks
2
2
 
3
3
  [![npm version](https://badge.fury.io/js/@soundworks%2Fcore.svg)](https://badge.fury.io/js/@soundworks%2Fcore)
4
4
  [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
@@ -21,7 +21,7 @@ npx @soundworks/create@latest
21
21
 
22
22
  ![./assets/soundworks-create-min.gif](./assets/soundworks-create-min.gif)
23
23
 
24
- See [https://soundworks.dev/guides/getting-started.html](https://soundworks.dev/guides/getting-started.html) for more informations on the wizard and how to start with `soundworks`.
24
+ 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 with `soundworks`.
25
25
 
26
26
  <!--
27
27
  ## Documentation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soundworks/core",
3
- "version": "4.0.0-alpha.4",
3
+ "version": "4.0.0-alpha.5",
4
4
  "description": "Open-source creative coding framework for distributed applications based on Web technologies",
5
5
  "authors": [
6
6
  "Benjamin Matuszewski",
@@ -53,13 +53,13 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "@ircam/sc-gettime": "^1.0.0",
56
+ "@ircam/sc-utils": "^1.0.1",
56
57
  "chalk": "^5.2.0",
57
58
  "columnify": "^1.6.0",
58
59
  "compression": "^1.7.1",
59
60
  "express": "^4.18.1",
60
61
  "fast-deep-equal": "^3.1.3",
61
62
  "fast-text-encoding": "^1.0.0",
62
- "is-plain-obj": "^4.1.0",
63
63
  "isomorphic-ws": "^5.0.0",
64
64
  "keyv": "^4.2.2",
65
65
  "keyv-file": "^0.2.0",
@@ -81,7 +81,7 @@
81
81
  "docdash": "^2.0.1",
82
82
  "dotenv": "^16.0.3",
83
83
  "eslint": "^8.32.0",
84
- "eslint-plugin-jsdoc": "^39.6.4",
84
+ "eslint-plugin-jsdoc": "^40.0.0",
85
85
  "generate-changelog": "^1.8.0",
86
86
  "jsdoc": "^4.0.0",
87
87
  "markdown-toc": "^1.2.0",
@@ -1,4 +1,4 @@
1
- import isPlainObject from 'is-plain-obj';
1
+ import { isBrowser, isPlainObject } from '@ircam/sc-utils';
2
2
 
3
3
  import ContextManager from './ContextManager.js';
4
4
  import PluginManager from './PluginManager.js';
@@ -10,7 +10,6 @@ import {
10
10
  CLIENT_HANDSHAKE_ERROR,
11
11
  AUDIT_STATE_NAME,
12
12
  } from '../common/constants.js';
13
- import { isBrowser } from '../common/utils.js';
14
13
  import logger from '../common/logger.js';
15
14
 
16
15
  /**
@@ -251,7 +250,7 @@ class Client {
251
250
  this.socket.addListener(CLIENT_HANDSHAKE_ERROR, (err) => {
252
251
  let msg = ``;
253
252
 
254
- switch(err.type) {
253
+ switch (err.type) {
255
254
  case 'invalid-client-type':
256
255
  msg = `[soundworks:Client] ${err.message}`;
257
256
  break;
@@ -275,7 +274,7 @@ class Client {
275
274
 
276
275
  this.socket.send(CLIENT_HANDSHAKE_REQUEST, payload);
277
276
  });
278
- } catch(err) {
277
+ } catch (err) {
279
278
  throw new Error(err);
280
279
  }
281
280
 
@@ -209,7 +209,7 @@ class Context {
209
209
  const reqId = storeRequestPromise(resolve, reject);
210
210
  this.client.socket.send(CONTEXT_ENTER_REQUEST, reqId, this.name);
211
211
  });
212
- } catch(err) {
212
+ } catch (err) {
213
213
  throw new Error(err);
214
214
  }
215
215
  }
@@ -243,7 +243,7 @@ class Context {
243
243
  const reqId = storeRequestPromise(resolve, reject);
244
244
  this.client.socket.send(CONTEXT_EXIT_REQUEST, reqId, this.name);
245
245
  });
246
- } catch(err) {
246
+ } catch (err) {
247
247
  throw new Error(err);
248
248
  }
249
249
  }
@@ -78,7 +78,7 @@ class ContextManager {
78
78
  this._contextStartPromises.set(contextName, startPromise);
79
79
  await startPromise;
80
80
  context.status = 'started';
81
- } catch(err) {
81
+ } catch (err) {
82
82
  context.status = 'errored';
83
83
  throw new Error(err);
84
84
  }
@@ -1,3 +1,4 @@
1
+ import { isBrowser } from '@ircam/sc-utils';
1
2
  import WebSocket from 'isomorphic-ws';
2
3
 
3
4
  import {
@@ -7,7 +8,6 @@ import {
7
8
  unpackStringMessage,
8
9
  } from '../common/sockets-utils.js';
9
10
  import logger from '../common/logger.js';
10
- import { isBrowser } from '../common/utils.js';
11
11
 
12
12
  // WebSocket events:
13
13
  //
@@ -1,7 +1,6 @@
1
- import isPlainObject from 'is-plain-obj';
1
+ import { isPlainObject, isString } from '@ircam/sc-utils';
2
2
 
3
3
  import logger from './logger.js';
4
- import { isString } from './utils.js';
5
4
 
6
5
  /**
7
6
  * Shared functionnality between server-side and client-size plugin manager
@@ -111,9 +110,9 @@ class BasePluginManager {
111
110
  try {
112
111
  await Promise.all(promises);
113
112
  this.status = 'started';
114
- } catch(err) {
113
+ } catch (err) {
115
114
  this.status = 'errored';
116
- throw new Error(err);
115
+ throw err; // throw initial error
117
116
  }
118
117
  }
119
118
 
@@ -170,7 +169,7 @@ class BasePluginManager {
170
169
  this._instanceStartPromises.set(id, startPromise);
171
170
 
172
171
  await startPromise;
173
- } catch(err) {
172
+ } catch (err) {
174
173
  errored = true;
175
174
  this._propagateStateChange(instance, 'errored');
176
175
  throw err;
@@ -1,3 +1,4 @@
1
+ import { isPlainObject } from '@ircam/sc-utils';
1
2
  import ParameterBag from './ParameterBag.js';
2
3
  import {
3
4
  DELETE_REQUEST,
@@ -34,7 +35,7 @@ class BaseSharedState {
34
35
  try {
35
36
  /** @private */
36
37
  this._parameters = new ParameterBag(schema, initValues);
37
- } catch(err) {
38
+ } catch (err) {
38
39
  console.error(err.stack);
39
40
 
40
41
  throw new Error(`Error creating or attaching state "${schemaName}" w/ values:\n
@@ -255,6 +256,14 @@ ${JSON.stringify(initValues, null, 2)}`);
255
256
  * const updates = await state.set({ myParam: Math.random() });
256
257
  */
257
258
  async set(updates, context = null) {
259
+ if (!isPlainObject(updates)) {
260
+ throw new ReferenceError(`[SharedState] State "${this.schemaName}": state.set(updates[, context]) should receive an object as first parameter`);
261
+ }
262
+
263
+ if (context !== null && !isPlainObject(context)) {
264
+ throw new ReferenceError(`[SharedState] State "${this.schemaName}": state.set(updates[, context]) should receive an object as second parameter`);
265
+ }
266
+
258
267
  // handle immediate option
259
268
  const immediateNewValues = {};
260
269
  const immediateOldValues = {};
@@ -263,7 +272,7 @@ ${JSON.stringify(initValues, null, 2)}`);
263
272
  for (let name in updates) {
264
273
  // throw early (client-side and not only server-side) if parameter is undefined
265
274
  if (!this._parameters.has(name)) {
266
- throw new ReferenceError(`[stateManager] Cannot set value of undefined parameter "${name}"`);
275
+ throw new ReferenceError(`[SharedState] State "${this.schemaName}": cannot set value of undefined parameter "${name}"`);
267
276
  }
268
277
 
269
278
  // @note: general idea...
@@ -407,7 +416,7 @@ ${JSON.stringify(initValues, null, 2)}`);
407
416
  if (this._isOwner) {
408
417
  return this.detach();
409
418
  } else {
410
- throw new Error(`[stateManager] can delete state "${this.schemaName}", only owner of the state (i.e. the node that "create[d]" it) can delete it`);
419
+ throw new Error(`[SharedState] Cannot delete state "${this.schemaName}", only the owner of the state (i.e. the node that created it) can delete the state. Use "detach" instead.`);
411
420
  }
412
421
  }
413
422
 
@@ -1,4 +1,4 @@
1
- import { idGenerator } from './utils.js';
1
+ import { idGenerator } from '@ircam/sc-utils';
2
2
 
3
3
  const generateRequestId = idGenerator();
4
4
  const requestPromises = new Map();
@@ -1,7 +1,6 @@
1
+ import { idGenerator } from '@ircam/sc-utils';
1
2
  import { v4 as uuid } from 'uuid';
2
3
 
3
- import { idGenerator } from '../common/utils.js';
4
-
5
4
  const generateId = idGenerator();
6
5
 
7
6
  /**
@@ -114,7 +114,7 @@ class ContextManager {
114
114
  this._contextStartPromises.set(contextName, startPromise);
115
115
  await startPromise;
116
116
  context.status = 'started';
117
- } catch(err) {
117
+ } catch (err) {
118
118
  context.status = 'errored';
119
119
  throw new Error(err);
120
120
  }
@@ -144,7 +144,7 @@ class ContextManager {
144
144
  let context;
145
145
  try {
146
146
  context = await this.get(contextName);
147
- } catch(err) {
147
+ } catch (err) {
148
148
  client.socket.send(
149
149
  CONTEXT_ENTER_ERROR,
150
150
  reqId,
@@ -185,7 +185,7 @@ class ContextManager {
185
185
  await context.enter(client);
186
186
 
187
187
  client.socket.send(CONTEXT_ENTER_RESPONSE, reqId, contextName);
188
- } catch(err) {
188
+ } catch (err) {
189
189
  client.socket.send(CONTEXT_ENTER_ERROR, reqId, contextName, err.message);
190
190
  }
191
191
  });
@@ -207,7 +207,7 @@ class ContextManager {
207
207
  try {
208
208
  await context.exit(client);
209
209
  client.socket.send(CONTEXT_EXIT_RESPONSE, reqId, contextName);
210
- } catch(err) {
210
+ } catch (err) {
211
211
  client.socket.send(CONTEXT_EXIT_ERROR, reqId, contextName, err.message);
212
212
  }
213
213
  } else {
@@ -5,10 +5,10 @@ import path from 'node:path';
5
5
  import os from 'node:os';
6
6
  import { X509Certificate, createPrivateKey } from 'node:crypto';
7
7
 
8
+ import { isPlainObject } from '@ircam/sc-utils';
8
9
  import chalk from 'chalk';
9
10
  import compression from 'compression';
10
11
  import express from 'express';
11
- import isPlainObject from 'is-plain-obj';
12
12
  import Keyv from 'keyv';
13
13
  import { KeyvFile } from 'keyv-file';
14
14
  import merge from 'lodash.merge';
@@ -236,7 +236,7 @@ class Server {
236
236
  * // expose assets located in the `soundfiles` directory on the network
237
237
  * server.router.use('/soundfiles', express.static('soundfiles')));
238
238
  */
239
- this.router = express();
239
+ this.router = express.Router();
240
240
  // compression (must be set before serve-static)
241
241
  this.router.use(compression());
242
242
 
@@ -357,7 +357,7 @@ class Server {
357
357
 
358
358
  // basic http authentication
359
359
  if (this.config.env.auth) {
360
- this.router.use((req, res, next) => {
360
+ const soundworksAuth = (req, res, next) => {
361
361
 
362
362
  const isProtected = this.config.env.auth.clients
363
363
  .map(type => req.path.endsWith(`/${type}`))
@@ -386,7 +386,9 @@ class Server {
386
386
  // route not protected
387
387
  return next();
388
388
  }
389
- });
389
+ };
390
+
391
+ this.router.use(soundworksAuth);
390
392
  }
391
393
 
392
394
  // start http server
@@ -411,7 +413,7 @@ class Server {
411
413
  // this fails with self-signed certificates for whatever reason...
412
414
  try {
413
415
  x509 = new X509Certificate(cert);
414
- } catch(err) {
416
+ } catch (err) {
415
417
  this._dispatchStatus('errored');
416
418
  throw new Error(`[soundworks:Server] Invalid https cert file`);
417
419
  }
@@ -423,7 +425,7 @@ class Server {
423
425
  this._dispatchStatus('errored');
424
426
  throw new Error(`[soundworks:Server] Invalid https key file`);
425
427
  }
426
- } catch(err) {
428
+ } catch (err) {
427
429
  this._dispatchStatus('errored');
428
430
  throw new Error(`[soundworks:Server] Invalid https key file`);
429
431
  }
@@ -447,7 +449,7 @@ class Server {
447
449
  };
448
450
 
449
451
  this.httpServer = https.createServer({ key, cert }, this.router);
450
- } catch(err) {
452
+ } catch (err) {
451
453
  logger.error(`
452
454
  Invalid certificate files, please check your:
453
455
  - key file: ${httpsInfos.key}
@@ -726,7 +728,7 @@ Invalid certificate files, please check your:
726
728
  try {
727
729
  const stats = fs.statSync(clientTmpl);
728
730
  template = stats.isFile() ? clientTmpl : defaultTmpl;
729
- } catch(err) {
731
+ } catch (err) {
730
732
  template = defaultTmpl;
731
733
  }
732
734
 
@@ -734,13 +736,13 @@ Invalid certificate files, please check your:
734
736
 
735
737
  try {
736
738
  tmplString = fs.readFileSync(template, 'utf8');
737
- } catch(err) {
739
+ } catch (err) {
738
740
  throw new Error(`[soundworks:Server] html template file "${template}" not found`);
739
741
  }
740
742
 
741
743
  const tmpl = templateEngine.compile(tmplString);
742
- // http request
743
- router.get(route, (req, res) => {
744
+
745
+ const soundworksClientHandler = (req, res) => {
744
746
  const data = clientConfigFunction(role, this.config, req);
745
747
 
746
748
  // CORS / COOP / COEP headers for `crossOriginIsolated pages,
@@ -756,7 +758,9 @@ Invalid certificate files, please check your:
756
758
 
757
759
  const appIndex = tmpl(data);
758
760
  res.end(appIndex);
759
- });
761
+ };
762
+ // http request
763
+ router.get(route, soundworksClientHandler);
760
764
 
761
765
  // return route infos for logging on server start
762
766
  return route;
@@ -805,7 +809,7 @@ Invalid certificate files, please check your:
805
809
 
806
810
  try {
807
811
  this.pluginManager.checkRegisteredPlugins(registeredPlugins);
808
- } catch(err) {
812
+ } catch (err) {
809
813
  socket.send(CLIENT_HANDSHAKE_ERROR, {
810
814
  type: 'invalid-plugin-list',
811
815
  message: err.message,
@@ -18,7 +18,6 @@ import BaseSharedState from '../common/BaseSharedState.js';
18
18
  * @callback server.SharedState~deleteOnUpdateCallback
19
19
  */
20
20
 
21
-
22
21
  /**
23
22
  * The `SharedState` is one of the most important and versatile abstraction provided
24
23
  * by `soundworks`. It represents a set of parameters that are synchronized accross
@@ -1,11 +1,11 @@
1
1
  import EventEmitter from 'node:events';
2
2
 
3
+ import { idGenerator, isString, isPlainObject } from '@ircam/sc-utils';
3
4
  import clonedeep from 'lodash.clonedeep';
4
5
 
5
6
  import BaseStateManager from '../common/BaseStateManager.js';
6
7
  import ParameterBag from '../common/ParameterBag.js';
7
8
  import SharedStatePrivate from '../common/SharedStatePrivate.js';
8
- import { isString, isPlainObject } from '../common/utils.js';
9
9
  import {
10
10
  SERVER_ID,
11
11
  CREATE_REQUEST,
@@ -22,7 +22,7 @@ import {
22
22
  DELETE_SCHEMA,
23
23
  PRIVATE_STATES,
24
24
  } from '../common/constants.js';
25
- import { idGenerator } from '../common/utils.js';
25
+
26
26
 
27
27
  const generateStateId = idGenerator();
28
28
  const generateRemoteId = idGenerator();
@@ -361,7 +361,7 @@ class StateManager extends BaseStateManager {
361
361
  this._observers.forEach(observer => {
362
362
  observer.transport.emit(OBSERVE_NOTIFICATION, [schemaName, stateId, nodeId]);
363
363
  });
364
- } catch(err) {
364
+ } catch (err) {
365
365
  client.transport.emit(CREATE_ERROR, reqId, err.message);
366
366
  console.error(err.message);
367
367
  }
@@ -1,28 +0,0 @@
1
- import isPlainObj from 'is-plain-obj';
2
-
3
- // https://stackoverflow.com/questions/17575790/environment-detection-node-js-or-browser
4
- export const isBrowser = new Function('try {return this===window;}catch(e){ return false;}');
5
-
6
- export function* idGenerator() {
7
- for (let i = 0; true; i++) {
8
- if (i === Number.MAX_SAFE_INTEGER) {
9
- i = 0;
10
- }
11
-
12
- yield i;
13
- }
14
- }
15
-
16
- export function isString(val) {
17
- return (typeof val === 'string' || val instanceof String);
18
- }
19
-
20
- export function isFunction(func) {
21
- return Object.prototype.toString.call(func) == '[object Function]' ||
22
- Object.prototype.toString.call(func) == '[object AsyncFunction]';
23
- }
24
-
25
- export function isPlainObject(obj) {
26
- return isPlainObj(obj);
27
- }
28
-