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.
@@ -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