vrpc 3.1.1 → 3.2.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/browser/vrpc.js +1 -1
- package/browser/vrpc.js.LICENSE.txt +2 -0
- package/package.json +4 -3
- package/vrpc/VrpcAdapter.js +59 -41
- package/vrpc/VrpcAgent.js +64 -18
- package/vrpc/VrpcBrowserAdapter.js +885 -0
- package/vrpc/VrpcBrowserAgent.js +675 -0
- package/vrpc/VrpcClient.js +104 -43
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
/*
|
|
2
|
+
__/\\\________/\\\____/\\\\\\\\\______/\\\\\\\\\\\\\_________/\\\\\\\\\_
|
|
3
|
+
__\/\\\_______\/\\\__/\\\///////\\\___\/\\\/////////\\\____/\\\////////__
|
|
4
|
+
__\//\\\______/\\\__\/\\\_____\/\\\___\/\\\_______\/\\\__/\\\/___________
|
|
5
|
+
___\//\\\____/\\\___\/\\\\\\\\\\\/____\/\\\\\\\\\\\\\/__/\\\_____________
|
|
6
|
+
____\//\\\__/\\\____\/\\\//////\\\____\/\\\/////////___\/\\\_____________
|
|
7
|
+
_____\//\\\/\\\_____\/\\\____\//\\\___\/\\\____________\//\\\____________
|
|
8
|
+
______\//\\\\\______\/\\\_____\//\\\__\/\\\_____________\///\\\__________
|
|
9
|
+
_______\//\\\_______\/\\\______\//\\\_\/\\\_______________\////\\\\\\\\\_
|
|
10
|
+
________\///________\///________\///__\///___________________\/////////__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
Non-intrusively adapts code and provides access in form of asynchronous remote
|
|
14
|
+
procedure calls (RPC).
|
|
15
|
+
Author: Dr. Burkhard C. Heisen (https://github.com/heisenware/vrpc)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
Licensed under the MIT License <http://opensource.org/licenses/MIT>.
|
|
19
|
+
Copyright (c) 2018 - 2022 Dr. Burkhard C. Heisen <burkhard.heisen@heisenware.com>.
|
|
20
|
+
|
|
21
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
22
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
23
|
+
in the Software without restriction, including without limitation the rights
|
|
24
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
25
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
26
|
+
furnished to do so, subject to the following conditions:
|
|
27
|
+
|
|
28
|
+
The above copyright notice and this permission notice shall be included in all
|
|
29
|
+
copies or substantial portions of the Software.
|
|
30
|
+
|
|
31
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
32
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
33
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
34
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
35
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
36
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
37
|
+
SOFTWARE.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const crypto = require('crypto')
|
|
41
|
+
const Ajv = require('ajv')
|
|
42
|
+
const EventEmitter = require('events')
|
|
43
|
+
const { nanoid } = require('nanoid')
|
|
44
|
+
|
|
45
|
+
const VRPC_GLOBAL = '__global__'
|
|
46
|
+
const VRPC_WINDOW_NS = 'vrpc'
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generates an adapter layer for existing code and enables further VRPC-based
|
|
50
|
+
* communication.
|
|
51
|
+
*/
|
|
52
|
+
class VrpcAdapter {
|
|
53
|
+
/**
|
|
54
|
+
* Registers an existing class an makes it callable from remote
|
|
55
|
+
*
|
|
56
|
+
* @param {Object} Klass Existing class to be registered
|
|
57
|
+
* @param {Object} [options]
|
|
58
|
+
* @param {Boolean} [options.onlyPublic=true] If true, only registers
|
|
59
|
+
* functions that do not begin with an underscore
|
|
60
|
+
* @param {Boolean} [options.withNew=true] If true, class will be constructed
|
|
61
|
+
* using the `new` operator
|
|
62
|
+
* @param {Object} [options.schema=null] If provided is used to validate ctor
|
|
63
|
+
* parameters (only works if registered code reflects a single class)
|
|
64
|
+
*/
|
|
65
|
+
static registerClass (
|
|
66
|
+
Klass,
|
|
67
|
+
{ onlyPublic = true, withNew = true, schema = null } = {}
|
|
68
|
+
) {
|
|
69
|
+
// Get all static static functions
|
|
70
|
+
let staticFunctions = VrpcAdapter._extractStaticFunctions(Klass)
|
|
71
|
+
if (onlyPublic) {
|
|
72
|
+
staticFunctions = staticFunctions.filter(f => !f.startsWith('_'))
|
|
73
|
+
}
|
|
74
|
+
// Inject constructor and destructor
|
|
75
|
+
staticFunctions.push('__createIsolated__')
|
|
76
|
+
staticFunctions.push('__createShared__')
|
|
77
|
+
staticFunctions.push('__callAll__')
|
|
78
|
+
staticFunctions.push('__delete__')
|
|
79
|
+
let memberFunctions = VrpcAdapter._extractMemberFunctions(Klass)
|
|
80
|
+
if (onlyPublic) {
|
|
81
|
+
memberFunctions = memberFunctions.filter(f => {
|
|
82
|
+
return !f.startsWith('_')
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
VrpcAdapter._functionRegistry.set(Klass.name, {
|
|
86
|
+
Klass,
|
|
87
|
+
withNew,
|
|
88
|
+
staticFunctions,
|
|
89
|
+
memberFunctions,
|
|
90
|
+
schema,
|
|
91
|
+
meta: {}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Registers an existing function and makes it callable from remote
|
|
97
|
+
*
|
|
98
|
+
* @param {Function} functionObject Existing function to be registered
|
|
99
|
+
* @param {String} functionName Name of the function to be registered
|
|
100
|
+
*/
|
|
101
|
+
static registerFunction (functionObject, functionName) {
|
|
102
|
+
if (typeof functionObject !== 'function') {
|
|
103
|
+
throw new Error('Provided argument must be a function object')
|
|
104
|
+
}
|
|
105
|
+
console.log('registering function', functionName)
|
|
106
|
+
// add function to global window object
|
|
107
|
+
if (!window[VRPC_WINDOW_NS]) {
|
|
108
|
+
window[VRPC_WINDOW_NS] = {}
|
|
109
|
+
}
|
|
110
|
+
window[VRPC_WINDOW_NS][functionName] = functionObject
|
|
111
|
+
const registry = VrpcAdapter._functionRegistry.get(VRPC_GLOBAL)
|
|
112
|
+
const staticFunctions = registry
|
|
113
|
+
? [...registry.staticFunctions, functionName]
|
|
114
|
+
: [functionName]
|
|
115
|
+
VrpcAdapter._functionRegistry.set(VRPC_GLOBAL, {
|
|
116
|
+
staticFunctions,
|
|
117
|
+
memberFunctions: [],
|
|
118
|
+
meta: {},
|
|
119
|
+
Klass: {},
|
|
120
|
+
withNew: false,
|
|
121
|
+
schema: null
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Registers an existing instance and make it (remotely) callable
|
|
127
|
+
*
|
|
128
|
+
* @param {Object} obj The instance to be registered
|
|
129
|
+
* @param {Object} options
|
|
130
|
+
* @param {String} options.className Class name of the instance
|
|
131
|
+
* @param {String} options.instance Name of the instance
|
|
132
|
+
* @param {Boolean} [options.onlyPublic=true] If true, only registers
|
|
133
|
+
* functions that do not begin with an underscore
|
|
134
|
+
*/
|
|
135
|
+
static registerInstance (
|
|
136
|
+
obj,
|
|
137
|
+
{ className, instance, onlyPublic = true } = {}
|
|
138
|
+
) {
|
|
139
|
+
let memberFunctions = VrpcAdapter._extractMemberFunctions(obj)
|
|
140
|
+
if (onlyPublic) {
|
|
141
|
+
memberFunctions = memberFunctions.filter(f => {
|
|
142
|
+
return !f.startsWith('_')
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
if (!VrpcAdapter._functionRegistry.has(className)) {
|
|
146
|
+
VrpcAdapter._functionRegistry.set(className, {
|
|
147
|
+
memberFunctions,
|
|
148
|
+
meta: {},
|
|
149
|
+
Klass: {},
|
|
150
|
+
withNew: false,
|
|
151
|
+
staticFunctions: [],
|
|
152
|
+
schema: null
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
VrpcAdapter._instances.set(instance, {
|
|
156
|
+
className,
|
|
157
|
+
instance: obj,
|
|
158
|
+
isIsolated: false
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/*****************************************************
|
|
163
|
+
* convenience interface if used as plug-able factory *
|
|
164
|
+
******************************************************/
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Creates a new instance
|
|
168
|
+
*
|
|
169
|
+
* @param {Object} options
|
|
170
|
+
* @param {String} options.className Name of the class which should be
|
|
171
|
+
* instantiated
|
|
172
|
+
* @param {String} [options.instance] Name of the created instance. If not
|
|
173
|
+
* provided an id will be generated
|
|
174
|
+
* @param {Array} [options.args] Positional arguments for the constructor call
|
|
175
|
+
* @param {bool} [options.isIsolated=false] If true the created instance will
|
|
176
|
+
* be visible only to the client who created it
|
|
177
|
+
* @returns {Object} The real instance (not a proxy!)
|
|
178
|
+
*/
|
|
179
|
+
static create ({
|
|
180
|
+
className,
|
|
181
|
+
instance = nanoid(8),
|
|
182
|
+
args = [],
|
|
183
|
+
isIsolated = false
|
|
184
|
+
} = {}) {
|
|
185
|
+
const json = { c: className, a: [instance, ...args] }
|
|
186
|
+
const obj = isIsolated
|
|
187
|
+
? VrpcAdapter._handleCreateIsolated(json)
|
|
188
|
+
: VrpcAdapter._handleCreateShared(json)
|
|
189
|
+
if (json.e) throw new Error(json.e)
|
|
190
|
+
return obj
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Deletes an instance
|
|
195
|
+
*
|
|
196
|
+
* @param {String|Object} instance Instance (name or object itself) to be deleted
|
|
197
|
+
* @returns {Boolean} True in case of success, false otherwise
|
|
198
|
+
*/
|
|
199
|
+
static delete (instance) {
|
|
200
|
+
if (typeof instance === 'string') {
|
|
201
|
+
return VrpcAdapter._delete(instance)
|
|
202
|
+
}
|
|
203
|
+
if (typeof instance === 'object') {
|
|
204
|
+
for (const [k, v] of VrpcAdapter._instances) {
|
|
205
|
+
if (v.instance === instance) {
|
|
206
|
+
VrpcAdapter._instances.delete(k)
|
|
207
|
+
return true
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Retrieves an existing instance by name
|
|
216
|
+
*
|
|
217
|
+
* @param {String} instance Name of the instance to be acquired
|
|
218
|
+
* @returns {Object} The real instance (not a proxy!)
|
|
219
|
+
*/
|
|
220
|
+
static getInstance (instance) {
|
|
221
|
+
const entry = VrpcAdapter._instances.get(instance)
|
|
222
|
+
if (entry) return entry.instance
|
|
223
|
+
throw new Error(`Could not find instance: ${instance}`)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Retrieves an array of all available classes (names only)
|
|
228
|
+
*
|
|
229
|
+
* @returns {Array.<String>} Array of class names
|
|
230
|
+
*/
|
|
231
|
+
static getAvailableClasses () {
|
|
232
|
+
return VrpcAdapter._getClassesArray()
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Provides the names of all currently running instances.
|
|
237
|
+
*
|
|
238
|
+
* @param {String} className Name of class to retrieve the instances for
|
|
239
|
+
* @returns {Array.<String>} Array of instance names
|
|
240
|
+
*/
|
|
241
|
+
static getAvailableInstances (className) {
|
|
242
|
+
return VrpcAdapter._getInstancesArray(className)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
static call (jsonString) {
|
|
246
|
+
const json = JSON.parse(jsonString)
|
|
247
|
+
VrpcAdapter._call(json)
|
|
248
|
+
return JSON.stringify(json)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
static onCallback (callback) {
|
|
252
|
+
VrpcAdapter._callback = callback
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* @typedef {Object.<String, Func>} MetaData Associates meta data to any function
|
|
257
|
+
*/
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @typedef Func
|
|
261
|
+
* @param {String} description Function description
|
|
262
|
+
* @param {Array.<Param>} params Array of parameter details in order of signature
|
|
263
|
+
* @param {Ret} ret Object associating further information to return value
|
|
264
|
+
*/
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* @typedef {Object} Param
|
|
268
|
+
* @param {String} name Parameter name
|
|
269
|
+
* @param {Boolean} optional Whether parameter is optional
|
|
270
|
+
* @param {String} description Parameter description
|
|
271
|
+
* @param {String} [type] Parameter type
|
|
272
|
+
* @param {Any} [default] The default to be injected when not provided
|
|
273
|
+
*/
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* @typedef {Object} Ret
|
|
277
|
+
* @param {String} description Return value description
|
|
278
|
+
* @param {String} [type] Return value type
|
|
279
|
+
*/
|
|
280
|
+
|
|
281
|
+
static on (eventName, listener) {
|
|
282
|
+
VrpcAdapter._emitter.on(eventName, listener)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
static once (eventName, listener) {
|
|
286
|
+
VrpcAdapter._emitter.once(eventName, listener)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
static off (eventName, listener) {
|
|
290
|
+
VrpcAdapter._emitter.off(eventName, listener)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
static removeAllListeners (eventName) {
|
|
294
|
+
VrpcAdapter._emitter.removeAllListeners(eventName)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// private:
|
|
298
|
+
|
|
299
|
+
static _call (json) {
|
|
300
|
+
VrpcAdapter._mustTrackClient = false // reset client tracking flag
|
|
301
|
+
switch (json.f) {
|
|
302
|
+
case '__createIsolated__':
|
|
303
|
+
VrpcAdapter._handleCreateIsolated(json)
|
|
304
|
+
break
|
|
305
|
+
case '__createShared__':
|
|
306
|
+
VrpcAdapter._handleCreateShared(json)
|
|
307
|
+
break
|
|
308
|
+
case '__callAll__':
|
|
309
|
+
VrpcAdapter._handleCallAll(json)
|
|
310
|
+
break
|
|
311
|
+
case '__delete__':
|
|
312
|
+
VrpcAdapter._handleDelete(json)
|
|
313
|
+
break
|
|
314
|
+
default:
|
|
315
|
+
// this call may have the side-effect of taking a listener on board
|
|
316
|
+
VrpcAdapter._handleCall(json)
|
|
317
|
+
}
|
|
318
|
+
// if we took a listener on board we must tell our caller such that
|
|
319
|
+
// things can be cleaned up later.
|
|
320
|
+
return VrpcAdapter._mustTrackClient
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
static _handleCreateIsolated (json) {
|
|
324
|
+
let instance
|
|
325
|
+
try {
|
|
326
|
+
const className = json.c
|
|
327
|
+
const [instanceId, ...args] = json.a
|
|
328
|
+
instance = VrpcAdapter._create(className, instanceId, ...args)
|
|
329
|
+
VrpcAdapter._instances.set(instanceId, {
|
|
330
|
+
instance,
|
|
331
|
+
className,
|
|
332
|
+
isIsolated: true
|
|
333
|
+
})
|
|
334
|
+
VrpcAdapter._emitter.emit('create', {
|
|
335
|
+
args,
|
|
336
|
+
className,
|
|
337
|
+
isIsolated: true,
|
|
338
|
+
instance: instanceId
|
|
339
|
+
})
|
|
340
|
+
json.r = instanceId
|
|
341
|
+
} catch (err) {
|
|
342
|
+
json.e = err.message
|
|
343
|
+
}
|
|
344
|
+
return instance
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
static _handleCreateShared (json) {
|
|
348
|
+
let instance
|
|
349
|
+
try {
|
|
350
|
+
const className = json.c
|
|
351
|
+
const [instanceId, ...args] = json.a
|
|
352
|
+
instance = VrpcAdapter._create(className, instanceId, ...args)
|
|
353
|
+
VrpcAdapter._instances.set(instanceId, {
|
|
354
|
+
instance,
|
|
355
|
+
className,
|
|
356
|
+
isIsolated: false
|
|
357
|
+
})
|
|
358
|
+
json.r = instanceId
|
|
359
|
+
VrpcAdapter._emitter.emit('create', {
|
|
360
|
+
args,
|
|
361
|
+
className,
|
|
362
|
+
isIsolated: false,
|
|
363
|
+
instance: instanceId
|
|
364
|
+
})
|
|
365
|
+
} catch (err) {
|
|
366
|
+
json.e = err.message
|
|
367
|
+
}
|
|
368
|
+
return instance
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
static _handleCallAll (json) {
|
|
372
|
+
try {
|
|
373
|
+
const calls = []
|
|
374
|
+
for (const [
|
|
375
|
+
id,
|
|
376
|
+
{ className, instance, isIsolated }
|
|
377
|
+
] of VrpcAdapter._instances) {
|
|
378
|
+
// TODO Think about a configurable behavior w.r.t. isolated instances...
|
|
379
|
+
if (className !== json.c || isIsolated) continue
|
|
380
|
+
let v = true
|
|
381
|
+
let e = null
|
|
382
|
+
try {
|
|
383
|
+
const unwrapped = VrpcAdapter._unwrapArguments(json, id)
|
|
384
|
+
if (unwrapped) {
|
|
385
|
+
const funcName = unwrapped[0]
|
|
386
|
+
v = instance[funcName].apply(instance, unwrapped.slice(1))
|
|
387
|
+
v = VrpcAdapter.sanitizeEventListenerReturnValues(v)
|
|
388
|
+
}
|
|
389
|
+
} catch (err) {
|
|
390
|
+
e = err.message
|
|
391
|
+
}
|
|
392
|
+
if (VrpcAdapter._isPromise(v)) {
|
|
393
|
+
calls.push(
|
|
394
|
+
v
|
|
395
|
+
.then(val => ({ id, val, err: null }))
|
|
396
|
+
.catch(err => ({ id, err, val: null }))
|
|
397
|
+
)
|
|
398
|
+
} else {
|
|
399
|
+
calls.push({ id, val: v, err: e })
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
this._handlePromise(
|
|
403
|
+
json,
|
|
404
|
+
Promise.allSettled(calls).then(result =>
|
|
405
|
+
result.filter(x => x.status === 'fulfilled').map(x => x.value)
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
} catch (err) {
|
|
409
|
+
json.e = err.message
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
static sanitizeEventListenerReturnValues (ret) {
|
|
414
|
+
return ret &&
|
|
415
|
+
typeof ret === 'object' &&
|
|
416
|
+
ret._events !== undefined &&
|
|
417
|
+
ret._eventsCount !== undefined
|
|
418
|
+
? true
|
|
419
|
+
: ret
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
static _handleDelete (json) {
|
|
423
|
+
try {
|
|
424
|
+
const instance = json.a[0] // first arg is instance to be deleted
|
|
425
|
+
VrpcAdapter._emitter.emit('beforeDelete', {
|
|
426
|
+
instance,
|
|
427
|
+
className: json.c
|
|
428
|
+
})
|
|
429
|
+
json.r = VrpcAdapter._delete(instance)
|
|
430
|
+
VrpcAdapter._emitter.emit('delete', {
|
|
431
|
+
instance,
|
|
432
|
+
className: json.c
|
|
433
|
+
})
|
|
434
|
+
} catch (err) {
|
|
435
|
+
json.e = err.message
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
static _handleCall (json) {
|
|
440
|
+
// special case: removeAllListeners
|
|
441
|
+
if (json.f === 'removeAllListeners') {
|
|
442
|
+
VrpcAdapter._removeAllListeners(json.a[0], json.s, json.c)
|
|
443
|
+
json.r = true
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
const unwrapped = VrpcAdapter._unwrapArguments(json)
|
|
447
|
+
if (!unwrapped) {
|
|
448
|
+
// this call must be skipped (event emitter stuff)
|
|
449
|
+
json.r = true
|
|
450
|
+
return
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Check whether context is global
|
|
454
|
+
if (json.c === VRPC_GLOBAL) {
|
|
455
|
+
try {
|
|
456
|
+
const ret = window[VRPC_WINDOW_NS][json.f].apply(null, unwrapped)
|
|
457
|
+
// check if function returns promise
|
|
458
|
+
if (VrpcAdapter._isPromise(ret)) {
|
|
459
|
+
VrpcAdapter._handlePromise(json, ret)
|
|
460
|
+
} else json.r = ret
|
|
461
|
+
} catch (err) {
|
|
462
|
+
json.e = err.message
|
|
463
|
+
}
|
|
464
|
+
return
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Check whether context is a registered class
|
|
468
|
+
const entry = VrpcAdapter._functionRegistry.get(json.c)
|
|
469
|
+
if (entry !== undefined) {
|
|
470
|
+
// entry is class -> function is static
|
|
471
|
+
const { Klass } = entry
|
|
472
|
+
// TODO Think about whether to do live checking (like here) or
|
|
473
|
+
// rather sticking to those functions registered before...
|
|
474
|
+
// TODO This is even more important now as the agent does a wildcard
|
|
475
|
+
// subscription against all functions
|
|
476
|
+
if (VrpcAdapter._isFunction(Klass[json.f])) {
|
|
477
|
+
try {
|
|
478
|
+
const ret = Klass[json.f].apply(null, unwrapped)
|
|
479
|
+
// check if function returns promise
|
|
480
|
+
if (VrpcAdapter._isPromise(ret)) {
|
|
481
|
+
VrpcAdapter._handlePromise(json, ret)
|
|
482
|
+
} else json.r = ret
|
|
483
|
+
} catch (err) {
|
|
484
|
+
json.e = err.message
|
|
485
|
+
}
|
|
486
|
+
} else throw new Error(`Could not find static function: ${json.f}`)
|
|
487
|
+
} else {
|
|
488
|
+
// is not static
|
|
489
|
+
const entry = VrpcAdapter._instances.get(json.c)
|
|
490
|
+
if (entry === undefined) {
|
|
491
|
+
throw new Error(`Could not find context: ${json.c}`)
|
|
492
|
+
}
|
|
493
|
+
const { instance } = entry
|
|
494
|
+
if (VrpcAdapter._isFunction(instance[json.f])) {
|
|
495
|
+
try {
|
|
496
|
+
let ret = instance[json.f].apply(instance, unwrapped)
|
|
497
|
+
ret = VrpcAdapter.sanitizeEventListenerReturnValues(ret)
|
|
498
|
+
// check if function returns promise
|
|
499
|
+
if (VrpcAdapter._isPromise(ret)) {
|
|
500
|
+
VrpcAdapter._handlePromise(json, ret)
|
|
501
|
+
} else json.r = ret
|
|
502
|
+
} catch (err) {
|
|
503
|
+
json.e = err.message
|
|
504
|
+
}
|
|
505
|
+
} else throw new Error(`Could not find member function: ${json.f}`)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
static _create (className, instanceId, ...args) {
|
|
510
|
+
const entry = VrpcAdapter._instances.get(instanceId)
|
|
511
|
+
if (entry) return entry.instance
|
|
512
|
+
const { Klass, withNew, schema } = VrpcAdapter._getClassEntry(className)
|
|
513
|
+
if (schema !== null) {
|
|
514
|
+
VrpcAdapter._validate(schema, ...args)
|
|
515
|
+
}
|
|
516
|
+
if (withNew) return new Klass(...args)
|
|
517
|
+
return Klass(...args)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
static _delete (instanceId) {
|
|
521
|
+
const entry = VrpcAdapter._instances.get(instanceId)
|
|
522
|
+
if (entry) {
|
|
523
|
+
// in case the instance was an EventEmitter, free all registered listeners
|
|
524
|
+
if (VrpcAdapter._isFunction(entry.removeAllListeners)) {
|
|
525
|
+
entry.removeAllListeners()
|
|
526
|
+
}
|
|
527
|
+
delete VrpcAdapter._listeners[instanceId]
|
|
528
|
+
VrpcAdapter._instances.delete(instanceId)
|
|
529
|
+
return true
|
|
530
|
+
}
|
|
531
|
+
return false
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
static _unregisterClient (clientId) {
|
|
535
|
+
Object.entries(VrpcAdapter._listeners).forEach(([ik, iv]) => {
|
|
536
|
+
Object.entries(iv).forEach(([ek, ev]) => {
|
|
537
|
+
const { clients, listener, event } = ev
|
|
538
|
+
const index = clients.indexOf(clientId)
|
|
539
|
+
if (index !== -1) {
|
|
540
|
+
clients.splice(index, 1)
|
|
541
|
+
if (clients.length === 0) {
|
|
542
|
+
const instance = VrpcAdapter.getInstance(ik)
|
|
543
|
+
if (instance && instance.removeListener) {
|
|
544
|
+
instance.removeListener(event, listener)
|
|
545
|
+
}
|
|
546
|
+
delete VrpcAdapter._listeners[ik][ek]
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
})
|
|
550
|
+
})
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
static _validate (schema, params) {
|
|
554
|
+
if (!VrpcAdapter._ajv) {
|
|
555
|
+
VrpcAdapter._ajv = new Ajv({ useDefaults: true })
|
|
556
|
+
}
|
|
557
|
+
const valid = VrpcAdapter._ajv.validate(schema, params)
|
|
558
|
+
if (!valid) throw new Error(VrpcAdapter._ajv.errorsText())
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
static _unwrapArguments (json, instanceId) {
|
|
562
|
+
const clientId = json.s
|
|
563
|
+
// if we came from a "callAll" we will have an instanceId defined
|
|
564
|
+
// things are different for callAll as we have to dispatch the instanceIds
|
|
565
|
+
const isCallAll = instanceId !== undefined
|
|
566
|
+
const context = isCallAll ? instanceId : json.c
|
|
567
|
+
const args = isCallAll ? json.a.slice(1) : json.a
|
|
568
|
+
const func = isCallAll ? json.a[0] : json.f
|
|
569
|
+
const unwrapped = isCallAll ? [func] : []
|
|
570
|
+
for (const arg of args) {
|
|
571
|
+
if (typeof arg === 'string') {
|
|
572
|
+
if (arg.startsWith('__f__')) {
|
|
573
|
+
// function callback
|
|
574
|
+
unwrapped.push(
|
|
575
|
+
VrpcAdapter._generateCallback({
|
|
576
|
+
clientId,
|
|
577
|
+
isCallAll,
|
|
578
|
+
instanceId: context,
|
|
579
|
+
eventId: arg
|
|
580
|
+
})
|
|
581
|
+
)
|
|
582
|
+
} else if (arg.startsWith('__e__')) {
|
|
583
|
+
// event registration
|
|
584
|
+
if (func === 'on' || func === 'addListener') {
|
|
585
|
+
VrpcAdapter._mustTrackClient = true
|
|
586
|
+
const listener = VrpcAdapter._registerListener({
|
|
587
|
+
clientId,
|
|
588
|
+
isCallAll,
|
|
589
|
+
instanceId: context,
|
|
590
|
+
eventId: arg,
|
|
591
|
+
event: args[0]
|
|
592
|
+
})
|
|
593
|
+
if (!listener) {
|
|
594
|
+
return null // skip call as listener is already registered
|
|
595
|
+
}
|
|
596
|
+
unwrapped.push(listener)
|
|
597
|
+
// event un-registration
|
|
598
|
+
} else if (func === 'off' || func === 'removeListener') {
|
|
599
|
+
const listener = VrpcAdapter._unregisterListener({
|
|
600
|
+
clientId,
|
|
601
|
+
instanceId: context,
|
|
602
|
+
eventId: arg
|
|
603
|
+
})
|
|
604
|
+
if (!listener) {
|
|
605
|
+
return null // skip call as others are still interested
|
|
606
|
+
}
|
|
607
|
+
unwrapped.push(listener)
|
|
608
|
+
} else {
|
|
609
|
+
VrpcAdapter._mustTrackClient = true
|
|
610
|
+
const listener = VrpcAdapter._registerListener({
|
|
611
|
+
clientId,
|
|
612
|
+
isCallAll,
|
|
613
|
+
instanceId: context,
|
|
614
|
+
eventId: arg,
|
|
615
|
+
event: null
|
|
616
|
+
})
|
|
617
|
+
unwrapped.push(listener)
|
|
618
|
+
}
|
|
619
|
+
} else {
|
|
620
|
+
unwrapped.push(arg)
|
|
621
|
+
}
|
|
622
|
+
} else {
|
|
623
|
+
// regular argument
|
|
624
|
+
unwrapped.push(arg)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return unwrapped
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
static _generateCallback ({ eventId, clientId, instanceId, isCallAll }) {
|
|
631
|
+
return (...innerArgs) => {
|
|
632
|
+
// "callAll" event notifications expect the instanceId as first argument
|
|
633
|
+
const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
|
|
634
|
+
VrpcAdapter._callback({ a, s: clientId, i: eventId })
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
static _registerListener ({
|
|
639
|
+
eventId,
|
|
640
|
+
clientId,
|
|
641
|
+
instanceId,
|
|
642
|
+
event,
|
|
643
|
+
isCallAll
|
|
644
|
+
}) {
|
|
645
|
+
if (!VrpcAdapter._listeners[instanceId]) {
|
|
646
|
+
VrpcAdapter._listeners[instanceId] = {
|
|
647
|
+
[eventId]: {
|
|
648
|
+
event,
|
|
649
|
+
clients: [clientId],
|
|
650
|
+
listener: VrpcAdapter._generateListener({
|
|
651
|
+
eventId,
|
|
652
|
+
instanceId,
|
|
653
|
+
isCallAll
|
|
654
|
+
})
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return VrpcAdapter._listeners[instanceId][eventId].listener
|
|
658
|
+
}
|
|
659
|
+
if (!VrpcAdapter._listeners[instanceId][eventId]) {
|
|
660
|
+
VrpcAdapter._listeners[instanceId][eventId] = {
|
|
661
|
+
event,
|
|
662
|
+
clients: [clientId],
|
|
663
|
+
listener: VrpcAdapter._generateListener({
|
|
664
|
+
eventId,
|
|
665
|
+
instanceId,
|
|
666
|
+
isCallAll
|
|
667
|
+
})
|
|
668
|
+
}
|
|
669
|
+
return VrpcAdapter._listeners[instanceId][eventId].listener
|
|
670
|
+
}
|
|
671
|
+
VrpcAdapter._listeners[instanceId][eventId].clients.push(clientId)
|
|
672
|
+
return null
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
static _generateListener ({ eventId, instanceId, isCallAll }) {
|
|
676
|
+
return (...innerArgs) => {
|
|
677
|
+
const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
|
|
678
|
+
VrpcAdapter._callback({ a, i: eventId })
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
static _unregisterListener ({ eventId, clientId, instanceId }) {
|
|
683
|
+
if (
|
|
684
|
+
VrpcAdapter._listeners[instanceId] &&
|
|
685
|
+
VrpcAdapter._listeners[instanceId][eventId]
|
|
686
|
+
) {
|
|
687
|
+
const { listener, clients } = VrpcAdapter._listeners[instanceId][eventId]
|
|
688
|
+
const index = clients.indexOf(clientId)
|
|
689
|
+
if (index !== -1) clients.splice(index, 1)
|
|
690
|
+
if (clients.length === 0) {
|
|
691
|
+
delete VrpcAdapter._listeners[instanceId][eventId]
|
|
692
|
+
if (Object.keys(VrpcAdapter._listeners[instanceId]).length === 0) {
|
|
693
|
+
delete VrpcAdapter._listeners[instanceId]
|
|
694
|
+
}
|
|
695
|
+
return listener
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
static _removeAllListeners (event, clientId, instanceId) {
|
|
701
|
+
const eventIds = VrpcAdapter._listeners[instanceId]
|
|
702
|
+
if (!eventIds) return
|
|
703
|
+
Object.entries(eventIds).forEach(([ek, ev]) => {
|
|
704
|
+
if (ev.event === event && ev.clients.includes(clientId)) {
|
|
705
|
+
ev.clients = ev.clients.filter(x => x !== clientId)
|
|
706
|
+
if (ev.clients.length === 0) {
|
|
707
|
+
VrpcAdapter.getInstance(instanceId).removeListener(
|
|
708
|
+
ev.event,
|
|
709
|
+
ev.listener
|
|
710
|
+
)
|
|
711
|
+
delete VrpcAdapter._listeners[instanceId][ek]
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
})
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
static _handlePromise (json, promise) {
|
|
718
|
+
try {
|
|
719
|
+
const cid = VrpcAdapter._correlationId++ % Number.MAX_SAFE_INTEGER
|
|
720
|
+
const id = `__p__${json.f}-${cid}`
|
|
721
|
+
json.r = id
|
|
722
|
+
promise
|
|
723
|
+
.then(r => VrpcAdapter._callback({ ...json, r, i: id }))
|
|
724
|
+
.catch(err => VrpcAdapter._callback({ ...json, e: err.message, i: id }))
|
|
725
|
+
} catch (err) {
|
|
726
|
+
json.data.e = err.message
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
static _generateId (object) {
|
|
731
|
+
return crypto.createHash('md5').update(JSON.stringify(object)).digest('hex')
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
static _getClassEntry (className) {
|
|
735
|
+
const entry = VrpcAdapter._functionRegistry.get(className)
|
|
736
|
+
if (!entry) {
|
|
737
|
+
throw new Error(`"${className}" is not a registered class`)
|
|
738
|
+
}
|
|
739
|
+
return entry
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
static _isFunction (obj) {
|
|
743
|
+
if (obj) {
|
|
744
|
+
const ident = {}.toString.call(obj)
|
|
745
|
+
return ident === '[object Function]' || ident === '[object AsyncFunction]'
|
|
746
|
+
}
|
|
747
|
+
return false
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
static _isClass (obj) {
|
|
751
|
+
return (
|
|
752
|
+
!!obj && typeof obj === 'function' && /^\s*class\s+/.test(obj.toString())
|
|
753
|
+
)
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
static _isPromise (obj) {
|
|
757
|
+
return (
|
|
758
|
+
!!obj &&
|
|
759
|
+
(typeof obj === 'object' || typeof obj === 'function') &&
|
|
760
|
+
typeof obj.then === 'function'
|
|
761
|
+
)
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
static _extractMemberFunctions (klass) {
|
|
765
|
+
let klass_ = klass
|
|
766
|
+
let fs = []
|
|
767
|
+
do {
|
|
768
|
+
if (klass_.prototype) {
|
|
769
|
+
const funcs = Object.getOwnPropertyNames(klass_.prototype).filter(
|
|
770
|
+
x =>
|
|
771
|
+
!VrpcAdapter._blackList.has(x) &&
|
|
772
|
+
VrpcAdapter._isFunction(klass_.prototype[x])
|
|
773
|
+
)
|
|
774
|
+
fs = [...fs, ...funcs]
|
|
775
|
+
} else {
|
|
776
|
+
const funcs = Object.getOwnPropertyNames(klass_).filter(
|
|
777
|
+
x =>
|
|
778
|
+
!VrpcAdapter._blackList.has(x) && VrpcAdapter._isFunction(klass_[x])
|
|
779
|
+
)
|
|
780
|
+
fs = [...fs, ...funcs]
|
|
781
|
+
}
|
|
782
|
+
klass_ = Object.getPrototypeOf(klass_)
|
|
783
|
+
} while (klass_)
|
|
784
|
+
// Filter out all duplicates
|
|
785
|
+
return Array.from(new Set(fs))
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
static _extractStaticFunctions (klass) {
|
|
789
|
+
let klass_ = klass
|
|
790
|
+
let fs = []
|
|
791
|
+
do {
|
|
792
|
+
fs = [
|
|
793
|
+
...fs,
|
|
794
|
+
...Object.getOwnPropertyNames(klass_).filter(prop => {
|
|
795
|
+
const desc = Object.getOwnPropertyDescriptor(klass_, prop)
|
|
796
|
+
return !!desc && typeof desc.value === 'function'
|
|
797
|
+
})
|
|
798
|
+
]
|
|
799
|
+
klass_ = Object.getPrototypeOf(klass_)
|
|
800
|
+
} while (klass_ && klass_.name)
|
|
801
|
+
// Filter out all duplicates
|
|
802
|
+
return Array.from(new Set(fs))
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
static _getClassesArray () {
|
|
806
|
+
return Array.from(VrpcAdapter._functionRegistry.keys())
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
static _getInstancesArray (className) {
|
|
810
|
+
const instances = []
|
|
811
|
+
VrpcAdapter._instances.forEach((v, k) => {
|
|
812
|
+
if (v.className === className && !v.isIsolated) instances.push(k)
|
|
813
|
+
})
|
|
814
|
+
return instances
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
static _getMemberFunctionsArray (className) {
|
|
818
|
+
const entry = VrpcAdapter._functionRegistry.get(className)
|
|
819
|
+
if (entry) return entry.memberFunctions
|
|
820
|
+
return []
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
static _getStaticFunctionsArray (className) {
|
|
824
|
+
const entry = VrpcAdapter._functionRegistry.get(className)
|
|
825
|
+
if (entry) return entry.staticFunctions
|
|
826
|
+
return []
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
static _getMetaData (className) {
|
|
830
|
+
const entry = VrpcAdapter._functionRegistry.get(className)
|
|
831
|
+
if (entry && entry.meta) return entry.meta
|
|
832
|
+
return {}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Event 'create'
|
|
838
|
+
*
|
|
839
|
+
* Emitted on creation of shared instance
|
|
840
|
+
*
|
|
841
|
+
* @event VrpcAdapter#create
|
|
842
|
+
* @type {Object}
|
|
843
|
+
* @property {String} className The class name of the create instance
|
|
844
|
+
* @property {String} instance The instance name
|
|
845
|
+
* @property {Array.<Any>} args The constructor arguments
|
|
846
|
+
*/
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Event 'delete'
|
|
850
|
+
*
|
|
851
|
+
* Emitted on deletion of shared instance
|
|
852
|
+
*
|
|
853
|
+
* @event VrpcAdapter#beforeDelete
|
|
854
|
+
* @event VrpcAdapter#delete
|
|
855
|
+
* @type {Object}
|
|
856
|
+
* @property {String} className The class name of the deleted instance
|
|
857
|
+
* @property {String} instance The instance name
|
|
858
|
+
*/
|
|
859
|
+
|
|
860
|
+
// Initialize static members
|
|
861
|
+
VrpcAdapter._functionRegistry = new Map()
|
|
862
|
+
VrpcAdapter._instances = new Map()
|
|
863
|
+
VrpcAdapter._emitter = new EventEmitter()
|
|
864
|
+
VrpcAdapter._correlationId = 0
|
|
865
|
+
VrpcAdapter._listeners = {}
|
|
866
|
+
VrpcAdapter._blackList = new Set([
|
|
867
|
+
'caller',
|
|
868
|
+
'callee',
|
|
869
|
+
'arguments',
|
|
870
|
+
'apply',
|
|
871
|
+
'bind',
|
|
872
|
+
'call',
|
|
873
|
+
'toString',
|
|
874
|
+
'hasOwnProperty',
|
|
875
|
+
'isPrototypeOf',
|
|
876
|
+
'propertyIsEnumerable',
|
|
877
|
+
'valueOf',
|
|
878
|
+
'toLocaleString',
|
|
879
|
+
'__defineGetter__',
|
|
880
|
+
'__defineSetter__',
|
|
881
|
+
'__lookupGetter__',
|
|
882
|
+
'__lookupSetter__'
|
|
883
|
+
])
|
|
884
|
+
|
|
885
|
+
module.exports = VrpcAdapter
|