@soundworks/core 4.0.0-alpha.14 → 4.0.0-alpha.16
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/.jsdoc +57 -0
- package/CHANGELOG.md +8 -0
- package/README.md +13 -28
- package/package.json +17 -23
- package/src/client/Context.js +10 -11
- package/src/client/SharedStateCollection.js +5 -2
- package/src/client/Socket.js +0 -1
- package/src/common/BasePlugin.js +1 -1
- package/src/common/BasePluginManager.js +1 -1
- package/src/common/BaseSharedState.js +43 -31
- package/src/common/BaseSharedStateCollection.js +75 -16
- package/src/common/BaseStateManager.js +197 -54
- package/src/common/ParameterBag.js +11 -7
- package/src/common/PromiseStore.js +41 -0
- package/src/common/SharedStatePrivate.js +100 -42
- package/src/common/constants.js +2 -1
- package/src/common/sockets-utils.js +3 -4
- package/src/server/Server.js +2 -3
- package/src/server/SharedStateCollection.js +5 -2
- package/src/server/Socket.js +1 -1
- package/src/server/StateManager.js +130 -88
- package/src/server/audit-network-latency.worker.js +1 -1
- package/src/server/crypto.js +6 -7
- package/src/common/promise-store.js +0 -25
- package/tsconfig.json +0 -21
package/src/common/constants.js
CHANGED
|
@@ -21,6 +21,7 @@ export const DETACH_ERROR = 's:dt:err';
|
|
|
21
21
|
|
|
22
22
|
export const OBSERVE_REQUEST = 's:o:req';
|
|
23
23
|
export const OBSERVE_RESPONSE = 's:o:res';
|
|
24
|
+
export const OBSERVE_ERROR = 's:o:err';
|
|
24
25
|
export const OBSERVE_NOTIFICATION = 's:o:not';
|
|
25
26
|
|
|
26
27
|
export const UNOBSERVE_NOTIFICATION = 's:uo:not';
|
|
@@ -47,5 +48,5 @@ export const CLIENT_HANDSHAKE_RESPONSE = 'cl:h:res';
|
|
|
47
48
|
export const CLIENT_HANDSHAKE_ERROR = 'cl:h:err';
|
|
48
49
|
|
|
49
50
|
// audit state schema name
|
|
50
|
-
export const AUDIT_STATE_NAME = 's:
|
|
51
|
+
export const AUDIT_STATE_NAME = 'p:s:audit';
|
|
51
52
|
export const PRIVATE_STATES = [AUDIT_STATE_NAME];
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import 'fast-text-encoding';
|
|
2
|
-
import root from 'window-or-global';
|
|
3
2
|
|
|
4
|
-
const encoder = new
|
|
5
|
-
const decoder = new
|
|
3
|
+
const encoder = new TextEncoder('utf-8');
|
|
4
|
+
const decoder = new TextDecoder('utf-8');
|
|
6
5
|
|
|
7
6
|
const types = [
|
|
8
7
|
'Int8Array',
|
|
@@ -57,7 +56,7 @@ export function unpackBinaryMessage(buffer /* arraybuffer */) {
|
|
|
57
56
|
const channel = decoder.decode(channelBuffer);
|
|
58
57
|
const type = types[typeIndex];
|
|
59
58
|
// slice (copy) the underlying ArrayBuffer to create a clean TypedArray
|
|
60
|
-
const data = new
|
|
59
|
+
const data = new globalThis[type](buffer.slice(startOffset));
|
|
61
60
|
|
|
62
61
|
return [channel, data];
|
|
63
62
|
}
|
package/src/server/Server.js
CHANGED
|
@@ -5,14 +5,13 @@ import path from 'node:path';
|
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import { X509Certificate, createPrivateKey } from 'node:crypto';
|
|
7
7
|
|
|
8
|
-
import { getTime } from '@ircam/sc-
|
|
9
|
-
import { isPlainObject, idGenerator } from '@ircam/sc-utils';
|
|
8
|
+
import { isPlainObject, idGenerator, getTime } from '@ircam/sc-utils';
|
|
10
9
|
import chalk from 'chalk';
|
|
11
10
|
import compression from 'compression';
|
|
12
11
|
import express from 'express';
|
|
13
12
|
import Keyv from 'keyv';
|
|
14
13
|
import { KeyvFile } from 'keyv-file';
|
|
15
|
-
import merge from 'lodash.
|
|
14
|
+
import merge from 'lodash/merge.js';
|
|
16
15
|
import pem from 'pem';
|
|
17
16
|
import compile from 'template-literal';
|
|
18
17
|
|
|
@@ -15,8 +15,11 @@ import BaseSharedStateCollection from '../common/BaseSharedStateCollection.js';
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* The `SharedStateCollection` interface represent a collection of all states
|
|
18
|
-
* created from a given schema name on the network
|
|
19
|
-
*
|
|
18
|
+
* created from a given schema name on the network.
|
|
19
|
+
*
|
|
20
|
+
* It can optionnaly exclude the states created by the current node.
|
|
21
|
+
*
|
|
22
|
+
* See {@link server.StateManager#getCollection} for factory method API
|
|
20
23
|
*
|
|
21
24
|
* ```
|
|
22
25
|
* const collection = await server.stateManager.getCollection('my-schema');
|
package/src/server/Socket.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import EventEmitter from 'node:events';
|
|
2
2
|
|
|
3
3
|
import { idGenerator, isString, isPlainObject } from '@ircam/sc-utils';
|
|
4
|
-
import clonedeep from 'lodash.
|
|
4
|
+
import clonedeep from 'lodash/cloneDeep.js';
|
|
5
5
|
|
|
6
6
|
import BaseStateManager from '../common/BaseStateManager.js';
|
|
7
7
|
import ParameterBag from '../common/ParameterBag.js';
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
ATTACH_ERROR,
|
|
18
18
|
OBSERVE_REQUEST,
|
|
19
19
|
OBSERVE_RESPONSE,
|
|
20
|
+
OBSERVE_ERROR,
|
|
20
21
|
OBSERVE_NOTIFICATION,
|
|
21
22
|
UNOBSERVE_NOTIFICATION,
|
|
22
23
|
DELETE_SCHEMA,
|
|
@@ -27,6 +28,8 @@ import {
|
|
|
27
28
|
const generateStateId = idGenerator();
|
|
28
29
|
const generateRemoteId = idGenerator();
|
|
29
30
|
|
|
31
|
+
const kIsObservableState = Symbol('StateManager::isObservableState');
|
|
32
|
+
|
|
30
33
|
/**
|
|
31
34
|
* @typedef {object} server.StateManager~schema
|
|
32
35
|
*
|
|
@@ -314,7 +317,7 @@ class StateManager extends BaseStateManager {
|
|
|
314
317
|
super(localClientId, localTransport);
|
|
315
318
|
|
|
316
319
|
this._clientByNodeId = new Map();
|
|
317
|
-
this.
|
|
320
|
+
this._sharedStatePrivateById = new Map();
|
|
318
321
|
this._schemas = new Map();
|
|
319
322
|
this._observers = new Set();
|
|
320
323
|
this._hooksBySchemaName = new Map(); // protected
|
|
@@ -322,6 +325,12 @@ class StateManager extends BaseStateManager {
|
|
|
322
325
|
this.addClient(localClientId, localTransport);
|
|
323
326
|
}
|
|
324
327
|
|
|
328
|
+
[kIsObservableState](state) {
|
|
329
|
+
const { schemaName, isCollectionController } = state;
|
|
330
|
+
// is observable only if not in private state and not a controller state
|
|
331
|
+
return !PRIVATE_STATES.includes(schemaName) && !isCollectionController;
|
|
332
|
+
}
|
|
333
|
+
|
|
325
334
|
/**
|
|
326
335
|
* Add a client to the manager.
|
|
327
336
|
*
|
|
@@ -341,105 +350,133 @@ class StateManager extends BaseStateManager {
|
|
|
341
350
|
// ---------------------------------------------
|
|
342
351
|
// CREATE
|
|
343
352
|
// ---------------------------------------------
|
|
344
|
-
client.transport.addListener(
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
353
|
+
client.transport.addListener(
|
|
354
|
+
CREATE_REQUEST,
|
|
355
|
+
(reqId, schemaName, requireSchema, initValues = {}, inCollection = false) => {
|
|
356
|
+
if (this._schemas.has(schemaName)) {
|
|
357
|
+
try {
|
|
358
|
+
const schema = this._schemas.get(schemaName);
|
|
359
|
+
const stateId = generateStateId.next().value;
|
|
360
|
+
const remoteId = generateRemoteId.next().value;
|
|
361
|
+
const state = new SharedStatePrivate(stateId, schemaName, schema, this, initValues);
|
|
362
|
+
|
|
363
|
+
// attach client to the state as owner
|
|
364
|
+
const isOwner = true;
|
|
365
|
+
state._attachClient(remoteId, client, isOwner, inCollection);
|
|
366
|
+
|
|
367
|
+
this._sharedStatePrivateById.set(stateId, state);
|
|
368
|
+
|
|
369
|
+
const currentValues = state._parameters.getValues();
|
|
370
|
+
const schemaOption = requireSchema ? schema : null;
|
|
371
|
+
|
|
372
|
+
client.transport.emit(
|
|
373
|
+
CREATE_RESPONSE,
|
|
374
|
+
reqId, stateId, remoteId, schemaName, schemaOption, currentValues
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
const isObservable = this[kIsObservableState](state);
|
|
378
|
+
|
|
379
|
+
if (isObservable) {
|
|
380
|
+
this._observers.forEach(observer => {
|
|
381
|
+
observer.transport.emit(OBSERVE_NOTIFICATION, schemaName, stateId, nodeId);
|
|
382
|
+
});
|
|
364
383
|
}
|
|
365
|
-
})
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
384
|
+
} catch (err) {
|
|
385
|
+
const msg = `Cannot create state "${schemaName}", ${err.message}`;
|
|
386
|
+
console.error(msg);
|
|
387
|
+
|
|
388
|
+
client.transport.emit(CREATE_ERROR, reqId, msg);
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
const msg = `Cannot create state "${schemaName}", schema does not exists`;
|
|
392
|
+
console.error(msg);
|
|
393
|
+
|
|
394
|
+
client.transport.emit(CREATE_ERROR, reqId, msg);
|
|
369
395
|
}
|
|
370
|
-
} else {
|
|
371
|
-
const msg = `Cannot create state "${schemaName}", schema does not exists`;
|
|
372
|
-
console.error(msg);
|
|
373
|
-
client.transport.emit(CREATE_ERROR, reqId, msg);
|
|
374
396
|
}
|
|
375
|
-
|
|
397
|
+
);
|
|
376
398
|
|
|
377
399
|
// ---------------------------------------------
|
|
378
400
|
// ATTACH (when creator, is attached by default)
|
|
379
401
|
// ---------------------------------------------
|
|
380
|
-
client.transport.addListener(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
402
|
+
client.transport.addListener(
|
|
403
|
+
ATTACH_REQUEST,
|
|
404
|
+
(reqId, schemaName, stateId = null, requireSchema = true, inCollection = false) => {
|
|
405
|
+
if (this._schemas.has(schemaName)) {
|
|
406
|
+
let state = null;
|
|
407
|
+
|
|
408
|
+
if (stateId !== null && this._sharedStatePrivateById.has(stateId)) {
|
|
409
|
+
state = this._sharedStatePrivateById.get(stateId);
|
|
410
|
+
} else if (stateId === null) {
|
|
411
|
+
// if no `stateId` given, we try to find the first state with the given
|
|
412
|
+
// `schemaName` in the list, this allow a client to attach to a global
|
|
413
|
+
// state created by the server (or some persistant client) without
|
|
414
|
+
// having to know the `stateId` (e.g. some global state...)
|
|
415
|
+
for (let existingState of this._sharedStatePrivateById.values()) {
|
|
416
|
+
if (existingState.schemaName === schemaName) {
|
|
417
|
+
state = existingState;
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
395
420
|
}
|
|
396
421
|
}
|
|
397
|
-
}
|
|
398
422
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
423
|
+
if (state !== null) {
|
|
424
|
+
// @note - we use a unique remote id to allow a client to attach
|
|
425
|
+
// several times to the same state.
|
|
426
|
+
// i.e. same state -> several remote attach on the same node
|
|
427
|
+
const remoteId = generateRemoteId.next().value;
|
|
428
|
+
const isOwner = false;
|
|
429
|
+
state._attachClient(remoteId, client, isOwner, inCollection);
|
|
430
|
+
|
|
431
|
+
const currentValues = state._parameters.getValues();
|
|
432
|
+
const schema = this._schemas.get(schemaName);
|
|
433
|
+
const schemaOption = requireSchema ? schema : null;
|
|
434
|
+
|
|
435
|
+
client.transport.emit(
|
|
436
|
+
ATTACH_RESPONSE,
|
|
437
|
+
reqId, state.id, remoteId, schemaName, schemaOption, currentValues
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
} else {
|
|
441
|
+
const msg = `Cannot attach, no existing state for schema "${schemaName}" with stateId: "${stateId}"`;
|
|
442
|
+
console.error(msg);
|
|
443
|
+
|
|
444
|
+
client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
445
|
+
}
|
|
411
446
|
} else {
|
|
412
|
-
const msg = `Cannot attach,
|
|
413
|
-
client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
447
|
+
const msg = `Cannot attach, schema "${schemaName}" does not exists`;
|
|
414
448
|
console.error(msg);
|
|
449
|
+
|
|
450
|
+
client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
415
451
|
}
|
|
416
|
-
} else {
|
|
417
|
-
const msg = `Cannot attach, schema "${schemaName}" does not exists`;
|
|
418
|
-
client.transport.emit(ATTACH_ERROR, reqId, msg);
|
|
419
|
-
console.error(msg);
|
|
420
452
|
}
|
|
421
|
-
|
|
453
|
+
);
|
|
422
454
|
|
|
423
455
|
// ---------------------------------------------
|
|
424
456
|
// OBSERVE PEERS (be notified when a state is created, lazy)
|
|
425
457
|
// ---------------------------------------------
|
|
426
|
-
client.transport.addListener(OBSERVE_REQUEST, reqId => {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
this._serverStatesById.forEach(state => {
|
|
430
|
-
const { schemaName, id, _creatorId } = state;
|
|
431
|
-
// only track application states
|
|
432
|
-
// (e.g. do not propagate infos about audit state)
|
|
433
|
-
if (!PRIVATE_STATES.includes(schemaName) && _creatorId !== client.id) {
|
|
434
|
-
statesInfos.push([schemaName, id, _creatorId]);
|
|
435
|
-
}
|
|
436
|
-
});
|
|
458
|
+
client.transport.addListener(OBSERVE_REQUEST, (reqId, observedSchemaName) => {
|
|
459
|
+
if (observedSchemaName === null || this._schemas.has(observedSchemaName)) {
|
|
460
|
+
const statesInfos = [];
|
|
437
461
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
this._observers.add(client);
|
|
462
|
+
this._sharedStatePrivateById.forEach(state => {
|
|
463
|
+
const isObservable = this[kIsObservableState](state);
|
|
441
464
|
|
|
442
|
-
|
|
465
|
+
if (isObservable) {
|
|
466
|
+
const { schemaName, id, _creatorId } = state;
|
|
467
|
+
statesInfos.push([schemaName, id, _creatorId]);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// add client to observers first because if some synchronous server side
|
|
472
|
+
// callback throws, the client would never be added to the list
|
|
473
|
+
this._observers.add(client);
|
|
474
|
+
|
|
475
|
+
client.transport.emit(OBSERVE_RESPONSE, reqId, ...statesInfos);
|
|
476
|
+
} else {
|
|
477
|
+
const msg = `Cannot observe, schema "${observedSchemaName}" does not exists`;
|
|
478
|
+
client.transport.emit(OBSERVE_ERROR, reqId, msg);
|
|
479
|
+
}
|
|
443
480
|
});
|
|
444
481
|
|
|
445
482
|
client.transport.addListener(UNOBSERVE_NOTIFICATION, () => {
|
|
@@ -458,18 +495,22 @@ class StateManager extends BaseStateManager {
|
|
|
458
495
|
* @private
|
|
459
496
|
*/
|
|
460
497
|
removeClient(nodeId) {
|
|
461
|
-
for (let [_id, state] of this.
|
|
498
|
+
for (let [_id, state] of this._sharedStatePrivateById.entries()) {
|
|
462
499
|
let deleteState = false;
|
|
463
500
|
|
|
464
501
|
// define if the client is the creator of the state, in which case
|
|
465
502
|
// everybody must delete it
|
|
466
|
-
for (let [remoteId,
|
|
503
|
+
for (let [remoteId, clientInfos] of state._attachedClients) {
|
|
504
|
+
const attachedClient = clientInfos.client;
|
|
505
|
+
|
|
467
506
|
if (nodeId === attachedClient.id && remoteId === state._creatorRemoteId) {
|
|
468
507
|
deleteState = true;
|
|
469
508
|
}
|
|
470
509
|
}
|
|
471
510
|
|
|
472
|
-
for (let [remoteId,
|
|
511
|
+
for (let [remoteId, clientInfos] of state._attachedClients) {
|
|
512
|
+
const attachedClient = clientInfos.client;
|
|
513
|
+
|
|
473
514
|
if (nodeId === attachedClient.id) {
|
|
474
515
|
state._detachClient(remoteId, attachedClient);
|
|
475
516
|
}
|
|
@@ -480,7 +521,7 @@ class StateManager extends BaseStateManager {
|
|
|
480
521
|
attachedClient.transport.emit(`${DELETE_NOTIFICATION}-${state.id}-${remoteId}`);
|
|
481
522
|
}
|
|
482
523
|
|
|
483
|
-
this.
|
|
524
|
+
this._sharedStatePrivateById.delete(state.id);
|
|
484
525
|
}
|
|
485
526
|
}
|
|
486
527
|
}
|
|
@@ -547,14 +588,15 @@ class StateManager extends BaseStateManager {
|
|
|
547
588
|
*/
|
|
548
589
|
deleteSchema(schemaName) {
|
|
549
590
|
// @note: deleting schema
|
|
550
|
-
for (let [_id, state] of this.
|
|
591
|
+
for (let [_id, state] of this._sharedStatePrivateById) {
|
|
551
592
|
if (state.schemaName === schemaName) {
|
|
552
|
-
for (let [remoteId,
|
|
593
|
+
for (let [remoteId, clientInfos] of state._attachedClients) {
|
|
594
|
+
const attached = clientInfos.client;
|
|
553
595
|
state._detachClient(remoteId, attached);
|
|
554
596
|
attached.transport.emit(`${DELETE_NOTIFICATION}-${state.id}-${remoteId}`);
|
|
555
597
|
}
|
|
556
598
|
|
|
557
|
-
this.
|
|
599
|
+
this._sharedStatePrivateById.delete(state.id);
|
|
558
600
|
}
|
|
559
601
|
}
|
|
560
602
|
|
package/src/server/crypto.js
CHANGED
|
@@ -23,16 +23,15 @@ const encryptionIV = crypto
|
|
|
23
23
|
|
|
24
24
|
export function encryptData(obj) {
|
|
25
25
|
const data = JSON.stringify(obj);
|
|
26
|
-
const cipher = crypto.createCipheriv(encryptionMethod, key, encryptionIV)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
).toString('base64')
|
|
26
|
+
const cipher = crypto.createCipheriv(encryptionMethod, key, encryptionIV);
|
|
27
|
+
|
|
28
|
+
return Buffer.from(cipher.update(data, 'utf8', 'hex') + cipher.final('hex')).toString('base64');
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
export function decryptData(encryptedData) {
|
|
33
|
-
const buff = Buffer.from(encryptedData, 'base64')
|
|
34
|
-
const decipher = crypto.createDecipheriv(encryptionMethod, key, encryptionIV)
|
|
32
|
+
const buff = Buffer.from(encryptedData, 'base64');
|
|
33
|
+
const decipher = crypto.createDecipheriv(encryptionMethod, key, encryptionIV);
|
|
35
34
|
|
|
36
|
-
const decrypted = `${decipher.update(buff.toString('utf8'), 'hex', 'utf8')}${decipher.final('utf8')}
|
|
35
|
+
const decrypted = `${decipher.update(buff.toString('utf8'), 'hex', 'utf8')}${decipher.final('utf8')}`;
|
|
37
36
|
return JSON.parse(decrypted);
|
|
38
37
|
}
|
|
@@ -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
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"include": ["src/**/*.js"],
|
|
3
|
-
|
|
4
|
-
"compilerOptions": {
|
|
5
|
-
// Tells TypeScript to read JS files, as
|
|
6
|
-
// normally they are ignored as source files
|
|
7
|
-
"allowJs": true,
|
|
8
|
-
// Generate d.ts files
|
|
9
|
-
"declaration": true,
|
|
10
|
-
// This compiler run should
|
|
11
|
-
// only output d.ts files
|
|
12
|
-
"emitDeclarationOnly": true,
|
|
13
|
-
// Types should go into this directory.
|
|
14
|
-
// Removing this would place the .d.ts files
|
|
15
|
-
// next to the .js files
|
|
16
|
-
"outDir": "types",
|
|
17
|
-
// go to js file when using IDE functions like
|
|
18
|
-
// "Go to Definition" in VSCode
|
|
19
|
-
"declarationMap": true
|
|
20
|
-
}
|
|
21
|
-
}
|