@soundworks/core 5.2.1 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soundworks/core",
3
- "version": "5.2.1",
3
+ "version": "5.3.0",
4
4
  "description": "Open-source creative coding framework for distributed applications based on Web technologies",
5
5
  "collaborators": [
6
6
  "Benjamin Matuszewski",
@@ -314,6 +314,10 @@ class Client {
314
314
  * await client.start();
315
315
  */
316
316
  async init() {
317
+ if (this.#status === 'inited') {
318
+ throw new DOMException(`Cannot execute 'init' twice on Client: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError');
319
+ }
320
+
317
321
  if (this.#status !== 'idle') {
318
322
  throw new DOMException(`Cannot execute 'init' on Client: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError');
319
323
  }
@@ -391,6 +395,10 @@ class Client {
391
395
  await this.init();
392
396
  }
393
397
 
398
+ if (this.#status === 'started') {
399
+ throw new DOMException(`Cannot execute 'start' twice on Client: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError');
400
+ }
401
+
394
402
  if (this.#status !== 'inited') {
395
403
  throw new DOMException(`Cannot execute 'start' on Client: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError');
396
404
  }
@@ -416,6 +424,10 @@ class Client {
416
424
  * await client.stop();
417
425
  */
418
426
  async stop() {
427
+ if (this.#status === 'stopped') {
428
+ throw new DOMException(`Cannot execute 'stop' twice on Client: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError');
429
+ }
430
+
419
431
  if (this.#status !== 'started') {
420
432
  throw new DOMException(`Cannot execute 'stop' on Client: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError');
421
433
  }
@@ -12,6 +12,7 @@ export const sharedOptions = {
12
12
  metas: {},
13
13
  filterChange: true,
14
14
  immediate: false,
15
+ acknowledge: true,
15
16
  };
16
17
 
17
18
  export const types = {
@@ -465,31 +465,31 @@ class SharedState {
465
465
 
466
466
  const newValues = {};
467
467
  const oldValues = {};
468
- const localParams = {};
469
- const sharedParams = {};
468
+ const locallyResolvedParam = {};
469
+ const networkPropagatedParams = {};
470
470
  let hasLocalParam = false;
471
- let hasSharedParam = false;
472
- let forwardParams = undefined;
473
- let propagateNow = false;
471
+ let hasRegularSharedParam = false;
472
+ let hasNonAcknowledgedParams = false;
473
+ let syncedOnUpdate = false;
474
474
 
475
475
  for (let name in updates) {
476
- // Try to coerce value early, so that eventual errors are triggered early
477
- // on the node requesting the update, and not only on the server side
478
- // This throws if name does not exists
476
+ // Try to coerce value now, so that eventual errors are triggered early
477
+ // on the node requesting the update and not only on the server side.
479
478
  try {
479
+ // This also throws if name does not exists
480
480
  this.#parameters.coerceValue(name, updates[name]);
481
481
  } catch (err) {
482
482
  throw new TypeError(`Cannot execute 'set' on SharedState (${this.#className}): ${err.message}`);
483
483
  }
484
484
 
485
- // Check that name is in filter list, if any
485
+ // Make sure that given name is in filter white list (if any)
486
486
  if (this.#filter !== null) {
487
487
  if (!this.#filter.includes(name)) {
488
488
  throw new DOMException(`Cannot execute 'set' on SharedState (${this.#className}): Parameter '${name}' is not in white list`, 'NotSupportedError');
489
489
  }
490
490
  }
491
491
 
492
- // `immediate` option behavior
492
+ // ### `immediate` modifier behavior
493
493
  //
494
494
  // If immediate=true
495
495
  // - call listeners if value changed
@@ -501,7 +501,7 @@ class SharedState {
501
501
  // - if the node is initiator of the update (UPDATE_RESPONSE), (re-)check
502
502
  // to prevent execute the listeners twice
503
503
 
504
- // `local` option behavior
504
+ // ### `local` modifier behavior
505
505
  //
506
506
  // - If the `updates` object only contains local variables, we can call the
507
507
  // update listeners and return a fulfilled promise immediately
@@ -510,9 +510,19 @@ class SharedState {
510
510
  // response, call update listeners with server response and resolve promise with
511
511
  // the full payload, i.e. reintegrating the local params in the resolve payload
512
512
 
513
- const { local, immediate, filterChange, event } = this.#parameters.getDescription(name);
513
+ // ### `acknowledge` modifier behavior
514
+ //
515
+ // Same as immediate, but no feedback from server
516
+
517
+ const {
518
+ local,
519
+ immediate,
520
+ filterChange,
521
+ event,
522
+ acknowledge,
523
+ } = this.#parameters.getDescription(name);
514
524
 
515
- if (immediate || local) {
525
+ if (immediate || local || acknowledge === false) {
516
526
  const oldValue = this.#parameters.get(name);
517
527
  const [newValue, changed] = this.#parameters.set(name, updates[name]);
518
528
 
@@ -520,46 +530,50 @@ class SharedState {
520
530
  if (changed || filterChange === false || event) {
521
531
  oldValues[name] = oldValue;
522
532
  newValues[name] = newValue;
523
- propagateNow = true;
533
+ syncedOnUpdate = true;
524
534
  }
525
535
  }
526
536
 
527
- // define params that must go through network or not
528
537
  if (local) {
538
+ // these don't go to the network
529
539
  hasLocalParam = true;
530
- // get sanitize value for fulfilling promise
531
- localParams[name] = this.#parameters.get(name);
540
+ locallyResolvedParam[name] = this.#parameters.get(name);
541
+ } else if (acknowledge === false) {
542
+ // these go to the network but don't receive an ack
543
+ // they are both locally forwarded and network propagated then
544
+ hasNonAcknowledgedParams = true;
545
+ locallyResolvedParam[name] = this.#parameters.get(name);
546
+ networkPropagatedParams[name] = updates[name];
532
547
  } else {
533
- // note that immediate are shared params too
534
- hasSharedParam = true;
535
- sharedParams[name] = updates[name];
548
+ // note that immediate params are shared and acknowledged
549
+ hasRegularSharedParam = true;
550
+ networkPropagatedParams[name] = updates[name];
536
551
  }
537
552
  }
538
553
 
539
- // propagate immediate params if changed
540
- if (propagateNow) {
554
+ // params that trigger a synced onUpdate call: immediate, local, non-acknowledged
555
+ if (syncedOnUpdate) {
541
556
  this.#onUpdateCallbacks.forEach(listener => listener(newValues, oldValues));
542
557
  }
543
558
 
544
- // check if we can resolve immediately or if we need to go through network
545
- if (hasLocalParam) {
546
- if (!hasSharedParam) {
547
- return Promise.resolve(localParams);
548
- } else {
549
- // store local params to fulfill promise with all values, see PromiseStore
550
- forwardParams = localParams;
551
- }
559
+ // if we only have local params, we can resolve immediately
560
+ if (hasLocalParam && !hasNonAcknowledgedParams && !hasRegularSharedParam) {
561
+ return Promise.resolve(locallyResolvedParam);
552
562
  }
553
563
 
554
- // override updates to be shared on network without local params
555
- updates = sharedParams;
556
-
557
564
  // go through server-side normal behavior
558
565
  const { id: reqId, promise } = this[kSharedStatePromiseStore].createPromise();
559
- if (forwardParams) {
560
- this[kSharedStatePromiseStore].associateResolveData(reqId, forwardParams);
566
+ // store local params that should be re-merged w/ regular ones
567
+ if (Object.keys(locallyResolvedParam).length > 0) {
568
+ this[kSharedStatePromiseStore].associateResolveData(reqId, locallyResolvedParam);
569
+ }
570
+
571
+ this.#client.transport.emit(`${UPDATE_REQUEST}-${this.#id}-${this.#instanceId}`, reqId, networkPropagatedParams);
572
+
573
+ // if we don't have regular shared params, we can resolve now
574
+ if (!hasRegularSharedParam) {
575
+ this[kSharedStatePromiseStore].resolve(reqId, {});
561
576
  }
562
- this.#client.transport.emit(`${UPDATE_REQUEST}-${this.#id}-${this.#instanceId}`, reqId, updates);
563
577
 
564
578
  return promise;
565
579
  }
@@ -746,7 +760,7 @@ class SharedState {
746
760
 
747
761
  return this.detach();
748
762
  } else {
749
- throw new DOMException(`Cannot execute 'delete' on SharedState (${this.#className}): SharedState (${this.#id}) is not owned by this node. Use 'SharedState#detach' instead`, 'NotSupportedError');
763
+ throw new DOMException(`Cannot execute 'delete' on SharedState (${this.#className}): This instance is attached and not owned. Use 'SharedState#detach' instead`, 'NotSupportedError');
750
764
  }
751
765
  }
752
766
 
@@ -814,6 +828,10 @@ class SharedState {
814
828
  * @param {Function} callback - Callback to execute when the state is deleted.
815
829
  */
816
830
  onDelete(callback) {
831
+ if (!this.#isOwner) {
832
+ throw new DOMException(`Cannot execute 'onDelete' on SharedState (${this.#className}): This instance is attached and not owned. Use 'SharedState#onDetach' instead`, 'NotSupportedError');
833
+ }
834
+
817
835
  this.#onDeleteCallbacks.add(callback);
818
836
  return () => this.#onDeleteCallbacks.delete(callback);
819
837
  }
@@ -52,25 +52,33 @@
52
52
  * @property {boolean} [event=false] - Define if the parameter is a volatile, i.e.
53
53
  * its value only exists on an update and is set back to `null` after propagation.
54
54
  * When `true`, `nullable` is automatically set to `true` and `default` to `null`.
55
+ * @property {boolean} [required=false] - When set to true, the parameter must be
56
+ * provided at the creation of the shared state.
55
57
  * @property {boolean} [filterChange=true] - When set to `false`, an update will
56
58
  * trigger the propagation of a parameter even when its value didn't change.
57
- * This option provides a sort of middle ground between the default behavior
59
+ * This modifier provides a sort of middle ground between the default behavior
58
60
  * (e.g. where only changed values are propagated) and the behavior of the `event`
59
61
  * option (which has no state per se). Hence, setting this options to `false` if
60
62
  * `event=true` makes no sens.
61
63
  * @property {boolean} [immediate=false] - When set to `true`, an update will
62
64
  * trigger the update listeners immediately on the node that generated the update,
63
- * before propagating the change on the network.
64
- * This option is useful in cases the network would introduce a noticeable
65
- * latency on the client.
65
+ * before propagating the change on the network. This modifier is useful in cases
66
+ * the network would introduce a noticeable latency on the client.
66
67
  * If for some reason the value is overridden server-side (e.g. in an `updateHook`)
67
68
  * the listeners will be called again when the "real" value is received.
68
- * @property {boolean} [required=false] - When set to true, the parameter must be
69
- * provided in the initialization values when the state is created.
69
+ * Setting this modifier to `true` will trigger the `onUpdate` callback synchronously
70
+ * according to the `set` call.
70
71
  * @property {boolean} [local=false] - When set to true, the parameter is never
71
72
  * propagated on the network (hence it is no longer a shared parameter :). This
72
73
  * is useful to declare some common parameter (e.g. some interface state) that
73
74
  * don't need to be shared but to stay in the shared state API paradigm.
75
+ * Setting this modifier to `true` will trigger the `onUpdate` callback synchronously
76
+ * according to the `set` call.
77
+ * @property {boolean} [acknowledge=true] - When set to false, the acknowledgement
78
+ * is never sent back to the shared state that initiated an update. This can be
79
+ * useful to e.g. stream values to the network, but can lead to invalid state.
80
+ * Setting this modifier to `true` will trigger the `onUpdate` callback synchronously
81
+ * according to the `set` call.
74
82
  * @property {number} [min=-Number.MIN_VALUE] - Minimum value of the parameter. Only applies
75
83
  * for `integer` and `float` types.
76
84
  * @property {number} [max=Number.MAX_VALUE] - Maximum value of the parameter. Only applies
@@ -1,2 +1,2 @@
1
1
  // [WARNING] This file is generated, do not edit
2
- export default '5.2.1';
2
+ export default '5.3.0';
@@ -482,6 +482,10 @@ class Server {
482
482
  * await server.start(); // init is called implicitly
483
483
  */
484
484
  async init() {
485
+ if (this.#status === 'inited') {
486
+ throw new DOMException(`Cannot execute 'init' twice on Server: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError');
487
+ }
488
+
485
489
  if (this.#status !== 'idle') {
486
490
  throw new DOMException(`Cannot execute 'init' on Server: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError');
487
491
  }
@@ -547,6 +551,10 @@ class Server {
547
551
  await this.init();
548
552
  }
549
553
 
554
+ if (this.#status === 'started') {
555
+ throw new DOMException(`Cannot execute 'start' twice on Server: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError');
556
+ }
557
+
550
558
  if (this.#status !== 'inited') {
551
559
  throw new DOMException(`Cannot execute 'start' on Server: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError');
552
560
  }
@@ -601,6 +609,10 @@ class Server {
601
609
  * await server.stop();
602
610
  */
603
611
  async stop() {
612
+ if (this.#status === 'stopped') {
613
+ throw new DOMException(`Cannot execute 'stop' twice on Server: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError');
614
+ }
615
+
604
616
  if (this.#status !== 'started') {
605
617
  throw new DOMException(`Cannot execute 'stop' on Server: Lifecycle methods must be called in following order: init, start, stop`, 'InvalidAccessError');
606
618
  }
@@ -16,18 +16,18 @@ import {
16
16
  } from '../server/ServerStateManager.js';
17
17
 
18
18
  /**
19
- * Filter update object according to filter list.
19
+ * Filter update object according to a given white list.
20
20
  * Return identity is filter is null
21
21
  * @param {object} updates
22
- * @param {array|null} filter
22
+ * @param {array|null} whiteList
23
23
  * @private
24
24
  */
25
- function filterUpdates(updates, filter) {
26
- if (filter === null) {
25
+ function whiteListFilterUpdates(updates, whiteList) {
26
+ if (whiteList === null) {
27
27
  return updates;
28
28
  }
29
29
 
30
- return filter.reduce((acc, key) => {
30
+ return whiteList.reduce((acc, key) => {
31
31
  if (key in updates) {
32
32
  acc[key] = updates[key];
33
33
  }
@@ -35,6 +35,29 @@ function filterUpdates(updates, filter) {
35
35
  }, {});
36
36
  }
37
37
 
38
+ /**
39
+ * Filter update object according to a given black list.
40
+ * Return identity is filter is null
41
+ * @param {object} updates
42
+ * @param {array|null} filter
43
+ * @private
44
+ */
45
+ function blackListFilterUpdates(updates, blackList) {
46
+ if (blackList === null) {
47
+ return updates;
48
+ }
49
+ // create a shallow copy so we can have multiple filters applied on same source
50
+ const filtered = Object.assign({}, updates);
51
+
52
+ blackList.forEach(key => {
53
+ if (key in updates) {
54
+ delete filtered[key];
55
+ }
56
+ });
57
+
58
+ return filtered;
59
+ }
60
+
38
61
  export const kSharedStatePrivateAttachClient = Symbol('soundworks:shared-state-private-attach-client');
39
62
  export const kSharedStatePrivateDetachClient = Symbol('soundworks:shared-state-private-detach-client');
40
63
  export const kSharedStatePrivateGetValues = Symbol('soundworks:shared-state-private-get-values');
@@ -122,13 +145,13 @@ class SharedStatePrivate {
122
145
 
123
146
  if (hookAborted === false) {
124
147
  let acknowledgedUpdates = {};
125
- let hasUpdates = false;
148
+ let acknowledgeFilter = [];
126
149
 
127
150
  for (let name in updates) {
128
151
  const [newValue, changed] = this.#parameters.set(name, updates[name]);
129
152
  // if `filterChange` is set to `false` we don't check if the value
130
153
  // has been changed or not, it is always propagated to client states
131
- const { event, filterChange } = this.#parameters.getDescription(name);
154
+ const { event, filterChange, acknowledge } = this.#parameters.getDescription(name);
132
155
 
133
156
  // if event type reset internal value to null
134
157
  if (event) {
@@ -137,11 +160,14 @@ class SharedStatePrivate {
137
160
 
138
161
  if ((filterChange && changed) || !filterChange) {
139
162
  acknowledgedUpdates[name] = newValue;
140
- hasUpdates = true;
163
+
164
+ if (acknowledge === false) {
165
+ acknowledgeFilter.push(name);
166
+ }
141
167
  }
142
168
  }
143
169
 
144
- if (hasUpdates) {
170
+ if (Object.keys(acknowledgedUpdates).length > 0) {
145
171
  // Normal case
146
172
  //
147
173
  // We need to handle cases where:
@@ -161,25 +187,29 @@ class SharedStatePrivate {
161
187
  //
162
188
  // @note - instanceId correspond to unique remote state id
163
189
 
190
+ // - Apply acknowledge filter on `acknowledgedUpdates`
191
+ // - We don't need to apply the regular filter on update request, they are blocked client-side
192
+ const requesterFilteredUpdates = blackListFilterUpdates(acknowledgedUpdates, acknowledgeFilter);
193
+
164
194
  // propagate RESPONSE to the client that originates the request if not the server
165
195
  if (client.id !== -1) {
166
- // no need to filter updates on requested, is blocked on client-side
167
- client.transport.emit(
168
- `${UPDATE_RESPONSE}-${this.id}-${instanceId}`,
169
- reqId,
170
- acknowledgedUpdates,
171
- );
196
+ // propagate only if there something left after applying the acknowledge filter
197
+ if (Object.keys(requesterFilteredUpdates).length > 0) {
198
+ client.transport.emit(
199
+ `${UPDATE_RESPONSE}-${this.id}-${instanceId}`,
200
+ reqId,
201
+ requesterFilteredUpdates,
202
+ );
203
+ };
172
204
  }
173
205
 
174
- // propagate NOTIFICATION to all attached clients except server
206
+ // propagate NOTIFICATION to all peer states except on server-side
175
207
  for (let [peerInstanceId, clientInfos] of this.#attachedClients) {
176
- const peer = clientInfos.client;
208
+ const { client: peer, filter } = clientInfos;
177
209
 
178
210
  if (instanceId !== peerInstanceId && peer.id !== -1) {
179
- const filter = clientInfos.filter;
180
- const filteredUpdates = filterUpdates(acknowledgedUpdates, filter);
181
-
182
- // propagate only if there something left after applying the filter
211
+ const filteredUpdates = whiteListFilterUpdates(acknowledgedUpdates, filter);
212
+ // propagate only if there something left after applying the white list filter
183
213
  if (Object.keys(filteredUpdates).length > 0) {
184
214
  peer.transport.emit(
185
215
  `${UPDATE_NOTIFICATION}-${this.id}-${peerInstanceId}`,
@@ -191,22 +221,23 @@ class SharedStatePrivate {
191
221
 
192
222
  // propagate RESPONSE to server if it is the requester
193
223
  if (client.id === -1) {
194
- // no need to filter updates on requested, is blocked on client-side
195
- client.transport.emit(
196
- `${UPDATE_RESPONSE}-${this.id}-${instanceId}`,
197
- reqId,
198
- acknowledgedUpdates,
199
- );
224
+ // propagate only if there something left after applying the acknowledge filter
225
+ if (Object.keys(requesterFilteredUpdates).length > 0) {
226
+ client.transport.emit(
227
+ `${UPDATE_RESPONSE}-${this.id}-${instanceId}`,
228
+ reqId,
229
+ requesterFilteredUpdates,
230
+ );
231
+ }
200
232
  }
201
233
 
202
- // propagate NOTIFICATION to other state attached on the server
234
+ // propagate NOTIFICATION to all peer states on the server-side
203
235
  for (let [peerInstanceId, clientInfos] of this.#attachedClients) {
204
- const peer = clientInfos.client;
236
+ const { client: peer, filter } = clientInfos;
205
237
 
206
238
  if (instanceId !== peerInstanceId && peer.id === -1) {
207
- const filter = clientInfos.filter;
208
- const filteredUpdates = filterUpdates(acknowledgedUpdates, filter);
209
-
239
+ const filteredUpdates = whiteListFilterUpdates(acknowledgedUpdates, filter);
240
+ // propagate only if there something left after applying the white list filter
210
241
  if (Object.keys(filteredUpdates).length > 0) {
211
242
  peer.transport.emit(
212
243
  `${UPDATE_NOTIFICATION}-${this.id}-${peerInstanceId}`,
@@ -216,8 +247,7 @@ class SharedStatePrivate {
216
247
  }
217
248
  }
218
249
  } else {
219
- // propagate back to the requester that the update has been aborted
220
- // ignore all other attached clients.
250
+ // propagate back to the requester that the update has been aborted, ignore all peers
221
251
  client.transport.emit(`${UPDATE_ABORT}-${this.id}-${instanceId}`, reqId, updates);
222
252
  }
223
253
  } else {
@@ -5,6 +5,7 @@ export namespace sharedOptions {
5
5
  let metas: {};
6
6
  let filterChange: boolean;
7
7
  let immediate: boolean;
8
+ let acknowledge: boolean;
8
9
  }
9
10
  export namespace types {
10
11
  export namespace boolean {
@@ -42,10 +42,15 @@ type SharedStateParameterDescription = {
42
42
  * When `true`, `nullable` is automatically set to `true` and `default` to `null`.
43
43
  */
44
44
  event?: boolean;
45
+ /**
46
+ * - When set to true, the parameter must be
47
+ * provided at the creation of the shared state.
48
+ */
49
+ required?: boolean;
45
50
  /**
46
51
  * - When set to `false`, an update will
47
52
  * trigger the propagation of a parameter even when its value didn't change.
48
- * This option provides a sort of middle ground between the default behavior
53
+ * This modifier provides a sort of middle ground between the default behavior
49
54
  * (e.g. where only changed values are propagated) and the behavior of the `event`
50
55
  * option (which has no state per se). Hence, setting this options to `false` if
51
56
  * `event=true` makes no sens.
@@ -54,25 +59,31 @@ type SharedStateParameterDescription = {
54
59
  /**
55
60
  * - When set to `true`, an update will
56
61
  * trigger the update listeners immediately on the node that generated the update,
57
- * before propagating the change on the network.
58
- * This option is useful in cases the network would introduce a noticeable
59
- * latency on the client.
62
+ * before propagating the change on the network. This modifier is useful in cases
63
+ * the network would introduce a noticeable latency on the client.
60
64
  * If for some reason the value is overridden server-side (e.g. in an `updateHook`)
61
65
  * the listeners will be called again when the "real" value is received.
66
+ * Setting this modifier to `true` will trigger the `onUpdate` callback synchronously
67
+ * according to the `set` call.
62
68
  */
63
69
  immediate?: boolean;
64
- /**
65
- * - When set to true, the parameter must be
66
- * provided in the initialization values when the state is created.
67
- */
68
- required?: boolean;
69
70
  /**
70
71
  * - When set to true, the parameter is never
71
72
  * propagated on the network (hence it is no longer a shared parameter :). This
72
73
  * is useful to declare some common parameter (e.g. some interface state) that
73
74
  * don't need to be shared but to stay in the shared state API paradigm.
75
+ * Setting this modifier to `true` will trigger the `onUpdate` callback synchronously
76
+ * according to the `set` call.
74
77
  */
75
78
  local?: boolean;
79
+ /**
80
+ * - When set to false, the acknowledgement
81
+ * is never sent back to the shared state that initiated an update. This can be
82
+ * useful to e.g. stream values to the network, but can lead to invalid state.
83
+ * Setting this modifier to `true` will trigger the `onUpdate` callback synchronously
84
+ * according to the `set` call.
85
+ */
86
+ acknowledge?: boolean;
76
87
  /**
77
88
  * - Minimum value of the parameter. Only applies
78
89
  * for `integer` and `float` types.
@@ -1,2 +1,2 @@
1
- declare const _default: "5.2.1";
1
+ declare const _default: "5.3.0";
2
2
  export default _default;