@soundworks/core 4.0.0-alpha.3 → 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.
- package/.github/workflows/main.yml +15 -0
- package/README.md +2 -2
- package/package.json +13 -13
- package/src/client/Client.js +3 -4
- package/src/client/Context.js +2 -2
- package/src/client/ContextManager.js +1 -1
- package/src/client/Socket.js +1 -1
- package/src/common/BasePluginManager.js +4 -5
- package/src/common/BaseSharedState.js +12 -3
- package/src/common/promise-store.js +1 -1
- package/src/server/Client.js +1 -2
- package/src/server/ContextManager.js +4 -4
- package/src/server/Server.js +17 -13
- package/src/server/SharedState.js +0 -1
- package/src/server/StateManager.js +3 -3
- package/src/common/utils.js +0 -28
|
@@ -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
|
-
#
|
|
1
|
+
# soundworks
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/js/@soundworks%2Fcore)
|
|
4
4
|
[](http://commitizen.github.io/cz-cli/)
|
|
@@ -21,7 +21,7 @@ npx @soundworks/create@latest
|
|
|
21
21
|
|
|
22
22
|

|
|
23
23
|
|
|
24
|
-
See [https://soundworks.dev/
|
|
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.
|
|
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,14 +53,14 @@
|
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@ircam/sc-gettime": "^1.0.0",
|
|
56
|
-
"
|
|
56
|
+
"@ircam/sc-utils": "^1.0.1",
|
|
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
|
-
"
|
|
63
|
-
"isomorphic-ws": "^4.0.1",
|
|
63
|
+
"isomorphic-ws": "^5.0.0",
|
|
64
64
|
"keyv": "^4.2.2",
|
|
65
65
|
"keyv-file": "^0.2.0",
|
|
66
66
|
"lodash.clonedeep": "^4.5.0",
|
|
@@ -68,25 +68,25 @@
|
|
|
68
68
|
"pem": "^1.14.6",
|
|
69
69
|
"serve-static": "^1.15.0",
|
|
70
70
|
"template-literal": "^1.0.4",
|
|
71
|
-
"uuid": "^
|
|
71
|
+
"uuid": "^9.0.0",
|
|
72
72
|
"window-or-global": "^1.0.1",
|
|
73
|
-
"ws": "^8.
|
|
73
|
+
"ws": "^8.12.0"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@ircam/eslint-config": "^1.
|
|
76
|
+
"@ircam/eslint-config": "^1.3.0",
|
|
77
77
|
"chai": "^4.3.6",
|
|
78
78
|
"chai-shallow-deep-equal": "^1.4.6",
|
|
79
79
|
"commitizen": "^4.2.5",
|
|
80
80
|
"cz-conventional-changelog": "^3.3.0",
|
|
81
|
-
"docdash": "^
|
|
81
|
+
"docdash": "^2.0.1",
|
|
82
82
|
"dotenv": "^16.0.3",
|
|
83
|
-
"eslint": "^8.
|
|
84
|
-
"eslint-plugin-jsdoc": "^
|
|
83
|
+
"eslint": "^8.32.0",
|
|
84
|
+
"eslint-plugin-jsdoc": "^40.0.0",
|
|
85
85
|
"generate-changelog": "^1.8.0",
|
|
86
|
-
"jsdoc": "^
|
|
86
|
+
"jsdoc": "^4.0.0",
|
|
87
87
|
"markdown-toc": "^1.2.0",
|
|
88
|
-
"mocha": "^
|
|
89
|
-
"puppeteer": "^
|
|
88
|
+
"mocha": "^10.2.0",
|
|
89
|
+
"puppeteer": "^19.5.2",
|
|
90
90
|
"tcp-ping": "^0.1.1",
|
|
91
91
|
"typescript": "^4.9.3"
|
|
92
92
|
},
|
package/src/client/Client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import isPlainObject from '
|
|
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
|
|
package/src/client/Context.js
CHANGED
|
@@ -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
|
}
|
package/src/client/Socket.js
CHANGED
|
@@ -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 '
|
|
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
|
|
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(`[
|
|
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(`[
|
|
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
|
|
package/src/server/Client.js
CHANGED
|
@@ -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 {
|
package/src/server/Server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
743
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/common/utils.js
DELETED
|
@@ -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
|
-
|