@soundworks/core 4.0.0-alpha.9 → 5.0.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.
Files changed (144) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +14 -29
  3. package/package.json +24 -42
  4. package/src/client/Client.js +306 -256
  5. package/src/client/{Context.js → ClientContext.js} +63 -54
  6. package/src/client/ClientContextManager.js +115 -0
  7. package/src/client/{Plugin.js → ClientPlugin.js} +23 -19
  8. package/src/client/{PluginManager.js → ClientPluginManager.js} +43 -40
  9. package/src/client/ClientSocket.js +270 -0
  10. package/src/client/{StateManager.js → ClientStateManager.js} +13 -13
  11. package/src/client/index.js +36 -39
  12. package/src/common/BasePlugin.js +63 -58
  13. package/src/common/BasePluginManager.js +191 -142
  14. package/src/common/BaseStateManager.js +635 -145
  15. package/src/common/BatchedTransport.js +73 -0
  16. package/src/common/ParameterBag.js +90 -93
  17. package/src/common/PromiseStore.js +54 -0
  18. package/src/common/SharedState.js +821 -0
  19. package/src/common/SharedStateCollection.js +492 -0
  20. package/src/common/constants.js +14 -2
  21. package/src/common/logger.js +64 -68
  22. package/src/common/shared-state-types.js +82 -0
  23. package/src/common/sockets-utils.js +0 -64
  24. package/src/common/version.js +2 -0
  25. package/src/server/Server.js +552 -824
  26. package/src/server/ServerClient.js +74 -0
  27. package/src/server/{Context.js → ServerContext.js} +96 -77
  28. package/src/server/{ContextManager.js → ServerContextManager.js} +107 -87
  29. package/src/server/{Plugin.js → ServerPlugin.js} +30 -32
  30. package/src/server/ServerPluginManager.js +177 -0
  31. package/src/server/ServerSocket.js +256 -0
  32. package/src/server/ServerSockets.js +150 -0
  33. package/src/server/ServerStateManager.js +706 -0
  34. package/src/server/SharedStatePrivate.js +277 -0
  35. package/src/server/audit-network-latency.worker.js +19 -4
  36. package/src/server/{audit-schema.js → audit-state-class-description.js} +3 -2
  37. package/src/server/create-http-server.js +127 -0
  38. package/src/server/crypto.js +8 -7
  39. package/src/server/index.js +60 -17
  40. package/types/client/Client.d.ts +95 -70
  41. package/types/client/{Context.d.ts → ClientContext.d.ts} +59 -34
  42. package/types/client/{ContextManager.d.ts → ClientContextManager.d.ts} +19 -22
  43. package/types/client/ClientPlugin.d.ts +51 -0
  44. package/types/client/ClientPluginManager.d.ts +112 -0
  45. package/types/client/ClientSocket.d.ts +76 -0
  46. package/types/client/{StateManager.d.ts → ClientStateManager.d.ts} +18 -21
  47. package/types/client/index.d.ts +33 -6
  48. package/types/common/BasePlugin.d.ts +64 -53
  49. package/types/common/BasePluginManager.d.ts +84 -39
  50. package/types/common/BaseStateManager.d.ts +227 -49
  51. package/types/common/BatchedTransport.d.ts +17 -0
  52. package/types/common/ParameterBag.d.ts +73 -76
  53. package/types/common/PromiseStore.d.ts +11 -0
  54. package/types/common/SharedState.d.ts +291 -0
  55. package/types/common/SharedStateCollection.d.ts +311 -0
  56. package/types/common/constants.d.ts +10 -3
  57. package/types/common/logger.d.ts +8 -6
  58. package/types/common/shared-state-types.d.ts +95 -0
  59. package/types/common/sockets-utils.d.ts +0 -5
  60. package/types/common/version.d.ts +2 -0
  61. package/types/server/Server.d.ts +215 -136
  62. package/types/server/ServerClient.d.ts +45 -0
  63. package/types/server/ServerContext.d.ts +209 -0
  64. package/types/server/ServerContextManager.d.ts +75 -0
  65. package/types/server/ServerPlugin.d.ts +57 -0
  66. package/types/server/ServerPluginManager.d.ts +115 -0
  67. package/types/server/ServerSocket.d.ts +69 -0
  68. package/types/server/ServerSockets.d.ts +42 -0
  69. package/types/server/ServerStateManager.d.ts +284 -0
  70. package/types/server/SharedStatePrivate.d.ts +23 -0
  71. package/types/server/audit-network-latency.worker.d.ts +2 -2
  72. package/types/server/{audit-schema.d.ts → audit-state-class-description.d.ts} +8 -9
  73. package/types/server/create-http-server.d.ts +5 -0
  74. package/types/server/crypto.d.ts +4 -0
  75. package/types/server/index.d.ts +51 -3
  76. package/.eslintrc +0 -15
  77. package/.github/workflows/gh-pages.yml +0 -31
  78. package/.github/workflows/unit-tests.yml +0 -18
  79. package/src/client/ContextManager.js +0 -111
  80. package/src/client/SharedState.js +0 -84
  81. package/src/client/SharedStateCollection.js +0 -35
  82. package/src/client/Socket.js +0 -402
  83. package/src/common/BaseSharedState.js +0 -583
  84. package/src/common/BaseSharedStateCollection.js +0 -179
  85. package/src/common/SharedStatePrivate.js +0 -192
  86. package/src/common/promise-store.js +0 -25
  87. package/src/server/Client.js +0 -55
  88. package/src/server/PluginManager.js +0 -170
  89. package/src/server/SharedState.js +0 -85
  90. package/src/server/SharedStateCollection.js +0 -36
  91. package/src/server/Socket.js +0 -396
  92. package/src/server/Sockets.js +0 -224
  93. package/src/server/StateManager.js +0 -620
  94. package/tsconfig.json +0 -21
  95. package/types/client/Client.d.ts.map +0 -1
  96. package/types/client/Context.d.ts.map +0 -1
  97. package/types/client/ContextManager.d.ts.map +0 -1
  98. package/types/client/Plugin.d.ts +0 -53
  99. package/types/client/Plugin.d.ts.map +0 -1
  100. package/types/client/PluginManager.d.ts +0 -96
  101. package/types/client/PluginManager.d.ts.map +0 -1
  102. package/types/client/SharedState.d.ts +0 -85
  103. package/types/client/SharedState.d.ts.map +0 -1
  104. package/types/client/Socket.d.ts +0 -138
  105. package/types/client/Socket.d.ts.map +0 -1
  106. package/types/client/StateManager.d.ts.map +0 -1
  107. package/types/client/index.d.ts.map +0 -1
  108. package/types/common/BasePlugin.d.ts.map +0 -1
  109. package/types/common/BasePluginManager.d.ts.map +0 -1
  110. package/types/common/BaseSharedState.d.ts +0 -173
  111. package/types/common/BaseSharedState.d.ts.map +0 -1
  112. package/types/common/BaseStateManager.d.ts.map +0 -1
  113. package/types/common/ParameterBag.d.ts.map +0 -1
  114. package/types/common/SharedStatePrivate.d.ts +0 -20
  115. package/types/common/SharedStatePrivate.d.ts.map +0 -1
  116. package/types/common/constants.d.ts.map +0 -1
  117. package/types/common/logger.d.ts.map +0 -1
  118. package/types/common/promise-store.d.ts +0 -4
  119. package/types/common/promise-store.d.ts.map +0 -1
  120. package/types/common/sockets-utils.d.ts.map +0 -1
  121. package/types/common/utils.d.ts +0 -5
  122. package/types/common/utils.d.ts.map +0 -1
  123. package/types/server/Client.d.ts +0 -40
  124. package/types/server/Client.d.ts.map +0 -1
  125. package/types/server/Context.d.ts +0 -59
  126. package/types/server/Context.d.ts.map +0 -1
  127. package/types/server/ContextManager.d.ts +0 -69
  128. package/types/server/ContextManager.d.ts.map +0 -1
  129. package/types/server/Plugin.d.ts +0 -70
  130. package/types/server/Plugin.d.ts.map +0 -1
  131. package/types/server/PluginManager.d.ts +0 -91
  132. package/types/server/PluginManager.d.ts.map +0 -1
  133. package/types/server/Server.d.ts.map +0 -1
  134. package/types/server/SharedState.d.ts +0 -86
  135. package/types/server/SharedState.d.ts.map +0 -1
  136. package/types/server/Socket.d.ts +0 -128
  137. package/types/server/Socket.d.ts.map +0 -1
  138. package/types/server/Sockets.d.ts +0 -73
  139. package/types/server/Sockets.d.ts.map +0 -1
  140. package/types/server/StateManager.d.ts +0 -190
  141. package/types/server/StateManager.d.ts.map +0 -1
  142. package/types/server/audit-network-latency.worker.d.ts.map +0 -1
  143. package/types/server/audit-schema.d.ts.map +0 -1
  144. package/types/server/index.d.ts.map +0 -1
@@ -1,179 +0,0 @@
1
- /**
2
- * @private
3
- */
4
- class BaseSharedStateCollection {
5
- constructor(stateManager, schemaName) {
6
- this._stateManager = stateManager;
7
- this._schemaName = schemaName;
8
- this._states = [];
9
- this._onUpdateCallbacks = new Set();
10
- this._onAttachCallbacks = new Set();
11
- this._onDetachCallbacks = new Set();
12
- this._unobserve = null;
13
- }
14
-
15
- async _init() {
16
- this._unobserve = await this._stateManager.observe(this._schemaName, async (schemaName, stateId, nodeId) => {
17
- const state = await this._stateManager.attach(schemaName, stateId);
18
-
19
- this._states.push(state);
20
-
21
- state.onDetach(() => {
22
- const index = this._states.indexOf(state);
23
- this._states.splice(index, 1);
24
-
25
- this._onDetachCallbacks.forEach(callback => callback(state));
26
- });
27
-
28
- state.onUpdate((newValues, oldValues, context) => {
29
- Array.from(this._onUpdateCallbacks).forEach(callback => {
30
- callback(state, newValues, oldValues, context);
31
- });
32
- });
33
-
34
- this._onAttachCallbacks.forEach(callback => callback(state));
35
- });
36
- }
37
-
38
- /**
39
- * Size of the collection
40
- * @type {number}
41
- */
42
- get length() {
43
- return this._states.length;
44
- }
45
-
46
- /**
47
- * Detach from the collection, i.e. detach all underlying shared states.
48
- * @type {number}
49
- */
50
- async detach() {
51
- this._unobserve();
52
-
53
- const promises = Array.from(this._states).map(state => state.detach());
54
- await Promise.all(promises);
55
-
56
- this._onUpdateCallbacks.clear();
57
- this._onDetachCallbacks.clear();
58
- }
59
-
60
- /**
61
- * Return the current values of all the states in the collection.
62
- * @return {Object[]}
63
- */
64
- getValues() {
65
- return this._states.map(state => state.getValues());
66
- }
67
-
68
- /**
69
- * Return the current param value of all the states in the collection.
70
- *
71
- * @param {String} name - Name of the parameter
72
- * @return {any[]}
73
- */
74
- get(name) {
75
- return this._states.map(state => state.get(name));
76
- }
77
-
78
- /**
79
- * Subscribe to any state update of the collection.
80
- *
81
- * @param {server.SharedStateCollection~onUpdateCallback|client.SharedStateCollection~onUpdateCallback}
82
- * callback - Callback to execute when an update is applied on a state.
83
- * @param {Boolean} [executeListener=false] - Execute the callback immediately
84
- * for all underlying states with current state values. (`oldValues` will be
85
- * set to `{}`, and `context` to `null`)
86
- * @returns {Function} - Function that delete the registered listener.
87
- */
88
- onUpdate(callback, executeListener = false) {
89
- this._onUpdateCallbacks.add(callback);
90
-
91
- if (executeListener === true) {
92
- this._states.forEach(state => {
93
- const currentValues = state.getValues();
94
- const oldValues = {};
95
- const context = null;
96
-
97
- callback(state, currentValues, oldValues, context);
98
- });
99
- }
100
-
101
- return () => this._onUpdateCallbacks.delete(callback);
102
- }
103
-
104
- /**
105
- * Register a function to execute when a state is added to the collection.
106
- *
107
- * @param {Function} callback - callback to execute when a state is added to
108
- * the collection.
109
- */
110
- onAttach(callback) {
111
- this._onAttachCallbacks.add(callback);
112
- }
113
-
114
- /**
115
- * Register a function to execute when a state is removed from the collection.
116
- *
117
- * @param {Function} callback - callback to execute when a state is removed
118
- * from the collection.
119
- */
120
- onDetach(callback) {
121
- this._onDetachCallbacks.add(callback);
122
- }
123
-
124
- /**
125
- * Execute the given function once for each states of the collection (see `Array.forEach`).
126
- *
127
- * @param {Function} func - A function to execute for each element in the array.
128
- * Its return value is discarded.
129
- */
130
- forEach(func) {
131
- this._states.forEach(func);
132
- }
133
-
134
- /**
135
- * Creates a new array populated with the results of calling a provided function
136
- * on every state of the collection (see `Array.map`).
137
- *
138
- * @param {Function} func - A function to execute for each element in the array.
139
- * Its return value is added as a single element in the new array.
140
- */
141
- map(func) {
142
- return this._states.map(func);
143
- }
144
-
145
- /**
146
- * Creates a shallow copy of a portion of the collection, filtered down to just
147
- * the estates that pass the test implemented by the provided function (see `Array.filter`).
148
- *
149
- * @param {Function} func - A function to execute for each element in the array.
150
- * It should return a truthy to keep the element in the resulting array, and a f
151
- * alsy value otherwise.
152
- */
153
- filter(func) {
154
- return this._states.filter(func);
155
- }
156
-
157
- /**
158
- * Sort the elements of the collection in place (see `Array.sort`).
159
- *
160
- * @param {Function} func - Function that defines the sort order.
161
- */
162
- sort(func) {
163
- this._states.sort(func);
164
- }
165
-
166
- /**
167
- * Returns the first element of the collection that satisfies the provided testing
168
- * function. If no values satisfy the testing function, undefined is returned.
169
- *
170
- * @param {Function} func - Function to execute for each element in the array.
171
- * It should return a truthy value to indicate a matching element has been found.
172
- * @return {}
173
- */
174
- find(func) {
175
- return this._states.find(func);
176
- }
177
- }
178
-
179
- export default BaseSharedStateCollection;
@@ -1,192 +0,0 @@
1
- import ParameterBag from '../common/ParameterBag.js';
2
- import {
3
- DELETE_REQUEST,
4
- DELETE_RESPONSE,
5
- DELETE_NOTIFICATION,
6
- DETACH_REQUEST,
7
- DETACH_RESPONSE,
8
- UPDATE_REQUEST,
9
- UPDATE_RESPONSE,
10
- UPDATE_ABORT,
11
- UPDATE_NOTIFICATION,
12
- } from '../common/constants.js';
13
-
14
- /**
15
- * The "real" state, this instance is kept private by the server.StateManager.
16
- * It cannot be accessed without a SharedState proxy
17
- * @private
18
- */
19
- class SharedStatePrivate {
20
- constructor(id, schemaName, schema, manager, initValues = {}) {
21
- this.id = id;
22
- this.schemaName = schemaName;
23
-
24
- this._manager = manager;
25
- this._parameters = new ParameterBag(schema, initValues);
26
- this._attachedClients = new Map(); // other peers interested in watching / controlling the state
27
-
28
- this._creatorRemoteId = null;
29
- this._creatorId = null;
30
- }
31
-
32
- _attachClient(remoteId, client, isOwner = false) {
33
- this._attachedClients.set(remoteId, client);
34
-
35
- if (isOwner) {
36
- this._creatorRemoteId = remoteId;
37
- this._creatorId = client.id;
38
- }
39
-
40
- // attach client listeners
41
- client.transport.addListener(`${UPDATE_REQUEST}-${this.id}-${remoteId}`, async (reqId, updates, context) => {
42
- // apply registered hooks
43
- const hooks = this._manager._hooksBySchemaName.get(this.schemaName);
44
- const values = this._parameters.getValues();
45
- let hookAborted = false;
46
-
47
- // cf. https://github.com/collective-soundworks/soundworks/issues/45
48
- for (let hook of hooks.values()) {
49
- const result = await hook(updates, values, context);
50
-
51
- if (result === null) { // explicit abort if hook returns null
52
- hookAborted = true;
53
- break;
54
- } else if (result === undefined) { // implicit continue if hook returns undefined
55
- continue;
56
- } else { // the hook returned an updates object
57
- updates = result;
58
- }
59
- }
60
-
61
- if (hookAborted === false) {
62
- const filteredUpdates = {};
63
- let hasUpdates = false;
64
-
65
- for (let name in updates) {
66
- // from v3.1.0 - the `filteredUpdates` check is made using 'fast-deep-equal'
67
- // cf. https://github.com/epoberezkin/fast-deep-equal
68
- // therefore unchanged objects are not considered changed
69
- // nor propagated anymore.
70
- // until v3.0.4 - we checked the `schema[name].type === 'any'`, to always consider
71
- // objects as dirty, because if the state is attached locally, we
72
- // compare the Object instances instead of their values.
73
- // @note - this should be made more robust but how?
74
- const [newValue, changed] = this._parameters.set(name, updates[name]);
75
-
76
- // if `filterChange` is set to `false` we don't check if the value
77
- // has been changed or not, it is always propagated to client states
78
- const { event, filterChange } = this._parameters.getSchema(name);
79
-
80
- // if event type reset internal value to null
81
- if (event) {
82
- this._parameters.set(name, null);
83
- }
84
-
85
- if ((filterChange && changed) || !filterChange) {
86
- filteredUpdates[name] = newValue;
87
- hasUpdates = true;
88
- }
89
- }
90
-
91
- if (hasUpdates) {
92
- // send response to requester
93
- // client.transport.emit(`${UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, filteredUpdates);
94
-
95
- // @note: we propagate server-side last, because as the server transport
96
- // is synchronous it can break ordering if a subscription function makes
97
- // itself an update in reaction to an update, therefore network messages
98
- // order would be broken,
99
-
100
- // we need to handle cases where:
101
- // client state (client.id: 2) sends a request
102
- // server attached state (client.id: -1) spot a problem and overrides the value
103
- // we want the remote client (id: 2) to receive in the right order:
104
- // * 1. the value it requested,
105
- // * 2. the value overriden by the server-side attached state (id: -1)
106
-
107
- // this problem could be solved properly with a reducer system:
108
- // if (dirty) {
109
- // -> call (async) reducer
110
- // -> get values from reducer
111
- //. -> dispatch to everybody
112
- // }
113
-
114
- for (let [peerRemoteId, peer] of this._attachedClients.entries()) {
115
- // propagate notification to all other attached clients except server
116
- if (remoteId !== peerRemoteId && peer.id !== -1) {
117
- peer.transport.emit(`${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`, filteredUpdates, context);
118
- }
119
- }
120
-
121
- if (client.id !== -1) {
122
- client.transport.emit(`${UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, filteredUpdates, context);
123
- }
124
-
125
- for (let [peerRemoteId, peer] of this._attachedClients.entries()) {
126
- // propagate notification to server
127
- if (remoteId !== peerRemoteId && peer.id === -1) {
128
- peer.transport.emit(`${UPDATE_NOTIFICATION}-${this.id}-${peerRemoteId}`, filteredUpdates, context);
129
- }
130
- }
131
-
132
- if (client.id === -1) {
133
- client.transport.emit(`${UPDATE_RESPONSE}-${this.id}-${remoteId}`, reqId, filteredUpdates, context);
134
- }
135
- } else {
136
- // propagate back to the requester that the update has been aborted
137
- // ignore all other attached clients.
138
- client.transport.emit(`${UPDATE_ABORT}-${this.id}-${remoteId}`, reqId, updates, context);
139
- }
140
- } else {
141
- // retrieve values from inner state (also handle immediate approriately)
142
- const oldValues = {};
143
-
144
- for (let name in updates) {
145
- oldValues[name] = this._parameters.get(name);
146
- }
147
- // aborted by hook (updates have been overriden to {})
148
- client.transport.emit(`${UPDATE_ABORT}-${this.id}-${remoteId}`, reqId, oldValues, context);
149
- }
150
- });
151
-
152
- if (isOwner) {
153
- // delete only if creator
154
- client.transport.addListener(`${DELETE_REQUEST}-${this.id}-${remoteId}`, (reqId) => {
155
- this._manager._serverStatesById.delete(this.id);
156
- // --------------------------------------------------------------------
157
- // WARNING - MAKE SURE WE DON'T HAVE PROBLEM W/ THAT
158
- // --------------------------------------------------------------------
159
- // @todo - propagate server-side last, because if a subscription function sends a
160
- // message to a client, network messages order are kept coherent
161
- // this._subscriptions.forEach(func => func(updated));
162
- for (let [remoteId, attached] of this._attachedClients.entries()) {
163
- this._detachClient(remoteId, attached);
164
-
165
- if (remoteId === this._creatorRemoteId) {
166
- attached.transport.emit(`${DELETE_RESPONSE}-${this.id}-${remoteId}`, reqId);
167
- } else {
168
- attached.transport.emit(`${DELETE_NOTIFICATION}-${this.id}-${remoteId}`);
169
- }
170
- }
171
- });
172
- } else {
173
- // detach only if not creator
174
- client.transport.addListener(`${DETACH_REQUEST}-${this.id}-${remoteId}`, (reqId) => {
175
- this._detachClient(remoteId, client);
176
- client.transport.emit(`${DETACH_RESPONSE}-${this.id}-${remoteId}`, reqId);
177
- });
178
- }
179
- }
180
-
181
- _detachClient(remoteId, client) {
182
- this._attachedClients.delete(remoteId);
183
- // delete listeners
184
- client.transport.removeAllListeners(`${UPDATE_REQUEST}-${this.id}-${remoteId}`);
185
- client.transport.removeAllListeners(`${DELETE_REQUEST}-${this.id}-${remoteId}`);
186
- client.transport.removeAllListeners(`${DETACH_REQUEST}-${this.id}-${remoteId}`);
187
- }
188
- }
189
-
190
- export default SharedStatePrivate;
191
-
192
-
@@ -1,25 +0,0 @@
1
- import { idGenerator } from '@ircam/sc-utils';
2
-
3
- const generateRequestId = idGenerator();
4
- const requestPromises = new Map();
5
-
6
- export function storeRequestPromise(resolve, reject) {
7
- const reqId = generateRequestId.next().value;
8
- requestPromises.set(reqId, { resolve, reject });
9
-
10
- return reqId;
11
- }
12
-
13
- export function resolveRequest(reqId, data) {
14
- const { resolve } = requestPromises.get(reqId);
15
- requestPromises.delete(reqId);
16
-
17
- resolve(data);
18
- }
19
-
20
- export function rejectRequest(reqId, data) {
21
- const { reject } = requestPromises.get(reqId);
22
- requestPromises.delete(reqId);
23
-
24
- reject(data);
25
- }
@@ -1,55 +0,0 @@
1
- import { idGenerator } from '@ircam/sc-utils';
2
- import { v4 as uuid } from 'uuid';
3
-
4
- const generateId = idGenerator();
5
-
6
- /**
7
- * Server-side representation of a `soundworks` client.
8
- *
9
- * @memberof server
10
- * @hideconstructor
11
- * @see {@link client.Client}
12
- */
13
- class Client {
14
- /**
15
- * @param {String} role - Role of the client
16
- * @param {server.Socket} socket - Socket connection with the client
17
- */
18
- constructor(role, socket) {
19
- /**
20
- * Client role, as specified in client side config {@link client.Client}.
21
- *
22
- * @type {String}
23
- */
24
- this.role = role;
25
-
26
- /**
27
- * Session Id (incremented positive number).
28
- *
29
- * @type {Number}
30
- */
31
- this.id = generateId.next().value;
32
-
33
- /**
34
- * Unique session Id (uuidv4).
35
- *
36
- * @type {String}
37
- */
38
- this.uuid = uuid();
39
-
40
- /**
41
- * Socket connection with the remote client {@link client.Client}.
42
- *
43
- * @type {server.Socket}
44
- */
45
- this.socket = socket;
46
-
47
- /**
48
- * Is set in server._onSocketConnection
49
- * @private
50
- */
51
- this.token = null;
52
- }
53
- }
54
-
55
- export default Client;
@@ -1,170 +0,0 @@
1
- import BasePluginManager from '../common/BasePluginManager.js';
2
- import Plugin from './Plugin.js';
3
- import Server from './Server.js';
4
-
5
- /**
6
- * Callback executed when a plugin internal state is updated.
7
- *
8
- * @callback server.PluginManager~onStateChangeCallback
9
- * @param {object<server.Plugin#id, server.Plugin>} fullState - List of all plugins.
10
- * @param {server.Plugin|null} initiator - Plugin that initiated the update or `null`
11
- * if the change was initiated by the state manager (i.e. when the initialization
12
- * of the plugins starts).
13
- */
14
-
15
- /**
16
- * Delete the registered {@link server.PluginManager~onStateChangeCallback}.
17
- *
18
- * @callback server.PluginManager~deleteOnStateChangeCallback
19
- */
20
-
21
- /**
22
- * The `PluginManager` allows to register and retrieve `soundworks` plugins.
23
- *
24
- * Plugins should always be registered both client-side and server-side,
25
- * and before {@link client.Client#start} or {@link server.Server#start}
26
- * to be properly initialized.
27
- *
28
- * In some sitautions, you might want to register the same plugin factory several times
29
- * using different ids (e.g. for watching several parts of the file system, etc.).
30
- *
31
- * Refer to the plugins' documentation for more precise examples, and the specific API
32
- * they expose. See [https://soundworks.dev/guide/ecosystem](https://soundworks.dev/guide/ecosystem)
33
- * for more informations on the available plugins.
34
- *
35
- * ```
36
- * // client-side
37
- * import { Client } from '@soundworks/core/client.js';
38
- * import syncPlugin from '@soundworks/plugin-sync/client.js';
39
- *
40
- * const client = new Client(config);
41
- * // register the plugin before `client.start()`
42
- * client.pluginManager.register('sync', syncPlugin);
43
- *
44
- * await client.start();
45
- *
46
- * const sync = await client.pluginManager.get('sync');
47
- *
48
- * setInterval(() => {
49
- * // log the estimated global synced clock alongside the local clock.
50
- * console.log(sync.getSyncTime(), sync.getLocalTime());
51
- * }, 1000);
52
- * ```
53
- *
54
- * ```
55
- * // server-side
56
- * import { Server } from '@soundworks/core/server.js';
57
- * import syncPlugin from '@soundworks/plugin-sync/server.js';
58
- *
59
- * const server = new Server(config);
60
- * // register the plugin before `server.start()`
61
- * server.pluginManager.register('sync', syncPlugin);
62
- *
63
- * await server.start();
64
- *
65
- * const sync = await server.pluginManager.get('sync');
66
- *
67
- * setInterval(() => {
68
- * // log the estimated global synced clock alongside the local clock.
69
- * console.log(sync.getSyncTime(), sync.getLocalTime());
70
- * }, 1000);
71
- * ```
72
- *
73
- * @memberof server
74
- * @extends BasePluginManager
75
- * @inheritdoc
76
- * @hideconstructor
77
- */
78
- class PluginManager extends BasePluginManager {
79
- constructor(server) {
80
- if (!(server instanceof Server)) {
81
- throw new Error(`[soundworks.PluginManager] Invalid argument, "new PluginManager(server)" should receive an instance of "soundworks.Server" as argument`);
82
- }
83
-
84
- super(server);
85
- }
86
-
87
- register(id, factory = null, options = {}, deps = []) {
88
- const ctor = factory(Plugin);
89
-
90
- if (!(ctor.prototype instanceof Plugin)) {
91
- throw new Error(`[soundworks.PluginManager] Invalid argument, "pluginManager.register" second argument should be a factory function returning a class extending the "Plugin" base class`);
92
- }
93
-
94
- super.register(id, ctor, options, deps);
95
- }
96
-
97
- /**
98
- * Retrieve an fully started instance of a registered plugin.
99
- *
100
- * Be aware that the `get` method resolves only when the plugin is fully 'started',
101
- * which is what we want 99.99% of the time. As such, and to prevent the application
102
- * to be stuck in some kind of Promise dead lock, this method will throw if
103
- * used before `server.init()` (or `server.start()`).
104
- *
105
- * To handle the remaining 0.01% cases and access plugin instances during their
106
- * initialization (e.g. to display initialization screen, etc.), you should rely
107
- * on the `onStateChange` method.
108
- *
109
- * _Note: the async API is designed to enable the dynamic creation of plugins
110
- * (hopefully without brealing changes) in a future release._
111
- *
112
- * @param {server.Plugin#id} id - Id of the plugin as defined when registered.
113
- * @returns {server.Plugin}
114
- * @see {@link server.PluginManager#onStateChange}
115
- *
116
- * @private
117
- */
118
- async get(id) {
119
- if (this.status !== 'started') {
120
- throw new Error(`[soundworks.PluginManager] Cannot get plugin before "server.init()"`);
121
- }
122
-
123
- return super.unsafeGet(id);
124
- }
125
-
126
- // server only methods
127
-
128
- /** private */
129
- checkRegisteredPlugins(registeredPlugins) {
130
- let missingPlugins = [];
131
-
132
- for (let id of registeredPlugins) {
133
- if (!this._instances.has(id)) {
134
- missingPlugins.push(id);
135
- }
136
- }
137
-
138
- if (missingPlugins.length > 0) {
139
- throw new Error(`Invalid plugin list, the following plugins registered client-side: [${missingPlugins.join(', ')}] have not been registered server-side. Registered server-side plugins are: [${Array.from(this._instances.keys()).join(', ')}].`);
140
- }
141
- }
142
-
143
- /** @private */
144
- async addClient(client, registeredPlugins = []) {
145
- let promises = [];
146
-
147
- for (let pluginName of registeredPlugins) {
148
- const plugin = await this.get(pluginName);
149
- const promise = plugin.addClient(client);
150
- promises.push(promise);
151
- }
152
-
153
- await Promise.all(promises);
154
- }
155
-
156
- /** @private */
157
- async removeClient(client) {
158
- let promises = [];
159
-
160
- for (let plugin of this._instances.values()) {
161
- if (plugin.clients.has(client)) {
162
- promises.push(plugin.removeClient(client));
163
- }
164
- }
165
-
166
- await Promise.all(promises);
167
- }
168
- }
169
-
170
- export default PluginManager;