vrpc 3.14.0 → 3.15.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.
@@ -14,7 +14,7 @@ procedure calls (RPC).
14
14
  Author: Dr. Burkhard C. Heisen (https://github.com/heisenware/vrpc)
15
15
 
16
16
  Licensed under the MIT License <http://opensource.org/licenses/MIT>.
17
- Copyright (c) 2018 - 2022 Dr. Burkhard C. Heisen <burkhard.heisen@heisenware.com>.
17
+ Copyright (c) 2018 Dr. Burkhard C. Heisen <burkhard.heisen@heisenware.com>.
18
18
 
19
19
  Permission is hereby granted, free of charge, to any person obtaining a copy
20
20
  of this software and associated documentation files (the "Software"), to deal
@@ -38,6 +38,8 @@ SOFTWARE.
38
38
  const Ajv = require('ajv')
39
39
  const EventEmitter = require('events')
40
40
  const { nanoid } = require('nanoid')
41
+ const Registrations = require('./Registrations')
42
+ const NamePattern = require('./namePattern')
41
43
 
42
44
  // A runtime without dispose symbols (older browsers, Node before 18.18 /
43
45
  // 20.4) gets Node's own stand-ins from the symbol registry, so a class file
@@ -82,6 +84,7 @@ class VrpcAdapter {
82
84
  staticFunctions.push('__createIsolated__')
83
85
  staticFunctions.push('__createShared__')
84
86
  staticFunctions.push('__callAll__')
87
+ staticFunctions.push('__callMatching__')
85
88
  staticFunctions.push('__delete__')
86
89
  let memberFunctions = VrpcAdapter._extractMemberFunctions(Klass)
87
90
  if (onlyPublic) {
@@ -303,8 +306,35 @@ class VrpcAdapter {
303
306
 
304
307
  // private:
305
308
 
309
+ /**
310
+ * Handles one request in place: the answer lands on `json` as `r` or `e`.
311
+ * Every request is answered - a malformed one, an unknown context or
312
+ * function with an error - so no caller waits for its timeout.
313
+ *
314
+ * @returns {Boolean} Whether the call took subscriptions of its sender,
315
+ * whose presence the agent then watches
316
+ */
306
317
  static _call (json) {
307
318
  VrpcAdapter._mustTrackClient = false // reset client tracking flag
319
+ // the answer is what the call yields, never what the request brought
320
+ delete json.r
321
+ delete json.e
322
+ try {
323
+ // a request without arguments has none
324
+ if (json.a === undefined) json.a = []
325
+ if (!Array.isArray(json.a)) {
326
+ throw new Error('The arguments of a request must be an array')
327
+ }
328
+ VrpcAdapter._dispatch(json)
329
+ } catch (err) {
330
+ json.e = VrpcAdapter._error(err)
331
+ }
332
+ // if we took a listener on board we must tell our caller such that
333
+ // things can be cleaned up later.
334
+ return VrpcAdapter._mustTrackClient
335
+ }
336
+
337
+ static _dispatch (json) {
308
338
  switch (json.f) {
309
339
  case '__createIsolated__':
310
340
  VrpcAdapter._handleCreateIsolated(json)
@@ -315,16 +345,49 @@ class VrpcAdapter {
315
345
  case '__callAll__':
316
346
  VrpcAdapter._handleCallAll(json)
317
347
  break
348
+ case '__callMatching__':
349
+ VrpcAdapter._handleCallMatching(json)
350
+ break
318
351
  case '__delete__':
319
352
  VrpcAdapter._handleDelete(json)
320
353
  break
354
+ case '__unsubscribe__':
355
+ VrpcAdapter._handleUnsubscribe(json)
356
+ break
357
+ case '__registrations__':
358
+ VrpcAdapter._handleRegistrations(json)
359
+ break
360
+ case '__end__':
361
+ VrpcAdapter._handleEnd(json)
362
+ break
321
363
  default:
322
364
  // this call may have the side-effect of taking a listener on board
323
365
  VrpcAdapter._handleCall(json)
324
366
  }
325
- // if we took a listener on board we must tell our caller such that
326
- // things can be cleaned up later.
327
- return VrpcAdapter._mustTrackClient
367
+ }
368
+
369
+ // an error as it travels: `{ message, cause? }`, whatever was thrown
370
+ static _error (err) {
371
+ if (err && typeof err === 'object' && 'message' in err) {
372
+ const e = { message: String(err.message) }
373
+ if (err.cause !== undefined) e.cause = err.cause
374
+ return e
375
+ }
376
+ return { message: String(err) }
377
+ }
378
+
379
+ /**
380
+ * Whether a request may call a function: only what the class info lists
381
+ * is reachable - a member function of the instance's class, a static
382
+ * function of the class, or a registered global function. Anything else
383
+ * an object happens to have (an EventEmitter's `emit`, a private helper)
384
+ * stays inside the agent.
385
+ */
386
+ static _isPublished (className, func, isStatic) {
387
+ const entry = VrpcAdapter._functionRegistry.get(className)
388
+ if (!entry) return false
389
+ const published = isStatic ? entry.staticFunctions : entry.memberFunctions
390
+ return published.includes(func)
328
391
  }
329
392
 
330
393
  /**
@@ -343,11 +406,28 @@ class VrpcAdapter {
343
406
  )
344
407
  }
345
408
 
409
+ /**
410
+ * An instance name is one topic level of its own: a non-empty string
411
+ * without `/`, `+`, `#` or `*` (which would read as a pattern), and never
412
+ * `__static__`, which stands for the class in a request topic.
413
+ */
414
+ static _assertInstanceName (name) {
415
+ if (
416
+ typeof name !== 'string' ||
417
+ name.length === 0 ||
418
+ /[/+#*]/.test(name) ||
419
+ name === '__static__'
420
+ ) {
421
+ throw new Error(`Invalid instance name: ${JSON.stringify(name)}`)
422
+ }
423
+ }
424
+
346
425
  static _handleCreateIsolated (json) {
347
426
  let instance
348
427
  try {
349
428
  const className = json.c
350
429
  const [instanceId, ...args] = json.a
430
+ VrpcAdapter._assertInstanceName(instanceId)
351
431
  const existing = VrpcAdapter._instances.get(instanceId)
352
432
  if (existing) {
353
433
  // an id is never re-homed: a shared instance stays shared, an
@@ -375,7 +455,7 @@ class VrpcAdapter {
375
455
  })
376
456
  json.r = instanceId
377
457
  } catch (err) {
378
- json.e = err.message
458
+ json.e = VrpcAdapter._error(err)
379
459
  }
380
460
  return instance
381
461
  }
@@ -385,6 +465,7 @@ class VrpcAdapter {
385
465
  try {
386
466
  const className = json.c
387
467
  const [instanceId, ...args] = json.a
468
+ VrpcAdapter._assertInstanceName(instanceId)
388
469
  const existing = VrpcAdapter._instances.get(instanceId)
389
470
  if (existing && existing.isIsolated) {
390
471
  throw new Error(`Instance '${instanceId}' exists as an isolated instance`)
@@ -403,66 +484,203 @@ class VrpcAdapter {
403
484
  instance: instanceId
404
485
  })
405
486
  } catch (err) {
406
- json.e = err.message
487
+ json.e = VrpcAdapter._error(err)
407
488
  }
408
489
  return instance
409
490
  }
410
491
 
492
+ // `a` = [function, ...arguments]: every shared instance of the class (ALL-1)
411
493
  static _handleCallAll (json) {
494
+ const [func, ...args] = json.a
495
+ VrpcAdapter._callAll(json, func, args, () => true)
496
+ }
497
+
498
+ // `a` = [selection, function, ...arguments]: the shared instances whose
499
+ // name the selection matches, a pattern or a list of them (ALL-4, ALL-5)
500
+ static _handleCallMatching (json) {
501
+ const [selection, func, ...args] = json.a
502
+ if (!NamePattern.isSelection(selection)) {
503
+ json.e = {
504
+ message: `Invalid selection: ${JSON.stringify(selection)} (a pattern or a list of them)`
505
+ }
506
+ return
507
+ }
508
+ VrpcAdapter._callAll(json, func, args, id =>
509
+ NamePattern.selects(selection, id)
510
+ )
511
+ }
512
+
513
+ // calls a function on the shared instances of a class `chosen` takes, and
514
+ // answers one entry per instance (ALL-2)
515
+ static _callAll (json, func, args, chosen) {
412
516
  try {
413
517
  const calls = []
518
+ const subscribes = Registrations.carriesEventIds(args)
519
+ // a remover carrying ids never reaches the class (see Registrations)
520
+ const withdraws = subscribes && Registrations.isRemover(func, json.v)
521
+ if (!withdraws && !VrpcAdapter._isPublished(json.c, func, false)) {
522
+ throw new Error(`Could not find function: ${func}`)
523
+ }
524
+ // what the instance's callbacks unwrap from: the function and its
525
+ // arguments, as a __callAll__ request carries them
526
+ const asCallAll = { ...json, a: [func, ...args] }
414
527
  for (const [
415
528
  id,
416
529
  { className, instance, isIsolated }
417
530
  ] of VrpcAdapter._instances) {
418
- // TODO Think about a configurable behavior w.r.t. isolated instances...
419
- if (className !== json.c || isIsolated) continue
420
- let v = true
421
- let e = null
531
+ // isolated instances never take part
532
+ if (className !== json.c || isIsolated || !chosen(id)) continue
533
+ // one answer per instance: `{ id, val }`, or `{ id, err }` with the
534
+ // error as it travels
422
535
  try {
423
- const unwrapped = VrpcAdapter._unwrapArguments(json, id)
424
- if (unwrapped) {
425
- const funcName = unwrapped[0]
426
- v = instance[funcName].apply(instance, unwrapped.slice(1))
427
- v = VrpcAdapter.sanitizeEventListenerReturnValues(v)
536
+ let val
537
+ if (subscribes) {
538
+ val = VrpcAdapter._subscribe(json, {
539
+ contextKey: id,
540
+ contextName: id,
541
+ target: instance,
542
+ func,
543
+ args,
544
+ isCallAll: true
545
+ }).value
546
+ } else {
547
+ const unwrapped = VrpcAdapter._unwrapArguments(asCallAll, id)
548
+ val = instance[func].apply(instance, unwrapped.slice(1))
549
+ val = VrpcAdapter.sanitizeEventListenerReturnValues(val)
428
550
  }
429
- } catch (err) {
430
- e = err.message
431
- }
432
- if (VrpcAdapter._isPromise(v)) {
433
551
  calls.push(
434
- v
435
- .then(val => ({ id, val, err: null }))
436
- .catch(err => ({ id, err, val: null }))
552
+ VrpcAdapter._isPromise(val)
553
+ ? val.then(
554
+ x => ({ id, val: x }),
555
+ err => ({ id, err: VrpcAdapter._error(err) })
556
+ )
557
+ : { id, val }
437
558
  )
438
- } else {
439
- calls.push({ id, val: v, err: e })
559
+ } catch (err) {
560
+ calls.push({ id, err: VrpcAdapter._error(err) })
440
561
  }
441
562
  }
442
- this._handlePromise(
563
+ VrpcAdapter._handlePromise(
443
564
  json,
444
565
  Promise.allSettled(calls).then(result =>
445
566
  result.filter(x => x.status === 'fulfilled').map(x => x.value)
446
567
  )
447
568
  )
448
569
  } catch (err) {
449
- json.e = err.message
570
+ json.e = VrpcAdapter._error(err)
450
571
  }
451
572
  }
452
573
 
453
574
  static sanitizeEventListenerReturnValues (ret) {
454
- return ret &&
455
- typeof ret === 'object' &&
456
- ret._events !== undefined &&
457
- ret._eventsCount !== undefined
458
- ? true
459
- : ret
575
+ return Registrations.sanitize(ret)
576
+ }
577
+
578
+ /**
579
+ * The book key of a request's context: an instance id, or the class for
580
+ * static functions (kept apart from an instance of the same name).
581
+ */
582
+ static _contextKey (context, isStatic) {
583
+ return isStatic ? `\u0000${context}` : context
584
+ }
585
+
586
+ // a class name addresses the class, as in _handleCall
587
+ static _isStaticContext (context) {
588
+ return VrpcAdapter._functionRegistry.has(context)
589
+ }
590
+
591
+ /**
592
+ * A call carrying subscription ids: one registration per context,
593
+ * function and arguments, shared by every subscription (see
594
+ * Registrations).
595
+ */
596
+ static _subscribe (
597
+ json,
598
+ { contextKey, contextName, target, func, args, isCallAll = false }
599
+ ) {
600
+ const result = VrpcAdapter._registrations.call({
601
+ json,
602
+ contextKey,
603
+ contextName,
604
+ target,
605
+ func,
606
+ args,
607
+ isCallAll,
608
+ speaks: VrpcAdapter._speaksFor(contextName),
609
+ makeCallback: eventId =>
610
+ VrpcAdapter._generateCallback({
611
+ clientId: json.s,
612
+ isCallAll,
613
+ instanceId: contextName,
614
+ eventId,
615
+ receiverVersion: json.v
616
+ })
617
+ })
618
+ // a subscriber is tracked: its going withdraws what it holds
619
+ if (result.ids.length > 0) VrpcAdapter._mustTrackClient = true
620
+ return result
621
+ }
622
+
623
+ /**
624
+ * `__unsubscribe__ [id, ...]`: withdraws the sender's subscriptions
625
+ * wherever they are held; an entry `{ id, context }` withdraws that id
626
+ * on that instance only (one instance of a callAll subscription).
627
+ * Answers true.
628
+ */
629
+ static _handleUnsubscribe (json) {
630
+ VrpcAdapter._registrations.unsubscribe(json.a || [], json.s)
631
+ json.r = true
632
+ }
633
+
634
+ /**
635
+ * `__registrations__`: lists the registrations of an instance or (on the
636
+ * static topic) a class.
637
+ */
638
+ static _handleRegistrations (json) {
639
+ try {
640
+ const isStatic = VrpcAdapter._isStaticContext(json.c)
641
+ if (!isStatic) {
642
+ VrpcAdapter._assertOwner(VrpcAdapter._instances.get(json.c), json, json.c)
643
+ }
644
+ json.r = VrpcAdapter._registrations.list(
645
+ VrpcAdapter._contextKey(json.c, isStatic)
646
+ )
647
+ } catch (err) {
648
+ json.e = VrpcAdapter._error(err)
649
+ }
650
+ }
651
+
652
+ /**
653
+ * `__end__ [function, args, by]`: ends one registration for all its
654
+ * subscriptions, which are told who ended it. Answers whether it existed.
655
+ */
656
+ static _handleEnd (json) {
657
+ try {
658
+ const isStatic = VrpcAdapter._isStaticContext(json.c)
659
+ if (!isStatic) {
660
+ VrpcAdapter._assertOwner(VrpcAdapter._instances.get(json.c), json, json.c)
661
+ }
662
+ const [func, args = [], by = null] = json.a || []
663
+ json.r = VrpcAdapter._registrations.end(
664
+ VrpcAdapter._contextKey(json.c, isStatic),
665
+ func,
666
+ args,
667
+ by
668
+ )
669
+ } catch (err) {
670
+ json.e = VrpcAdapter._error(err)
671
+ }
460
672
  }
461
673
 
462
674
  static _handleDelete (json) {
463
675
  try {
464
676
  const instance = json.a[0] // first arg is instance to be deleted
465
- VrpcAdapter._assertOwner(VrpcAdapter._instances.get(instance), json, instance)
677
+ const entry = VrpcAdapter._instances.get(instance)
678
+ // a delete names the instance's own class: the class info of that
679
+ // class is what announces it gone
680
+ if (entry && entry.className !== json.c && json.s !== VrpcAdapter.LOCAL_SENDER) {
681
+ throw new Error(`Instance '${instance}' is not of class '${json.c}'`)
682
+ }
683
+ VrpcAdapter._assertOwner(entry, json, instance)
466
684
  VrpcAdapter._emitter.emit('beforeDelete', {
467
685
  instance,
468
686
  className: json.c
@@ -479,7 +697,7 @@ class VrpcAdapter {
479
697
  VrpcAdapter._handlePromise(json, disposed.then(() => ended))
480
698
  } else json.r = ended
481
699
  } catch (err) {
482
- json.e = err.message
700
+ json.e = VrpcAdapter._error(err)
483
701
  }
484
702
  }
485
703
 
@@ -488,13 +706,19 @@ class VrpcAdapter {
488
706
  try {
489
707
  VrpcAdapter._assertOwner(VrpcAdapter._instances.get(json.c), json, json.c)
490
708
  } catch (err) {
491
- const { message, cause } = err
492
- json.e = { message, cause }
709
+ json.e = VrpcAdapter._error(err)
493
710
  return
494
711
  }
495
- // special case: removeAllListeners
712
+ // special case: removeAllListeners withdraws the caller's subscriptions
713
+ // of that event - the emitter itself is never touched, it holds other
714
+ // clients' and its own listeners
496
715
  if (json.f === 'removeAllListeners') {
497
- VrpcAdapter._removeAllListeners(json.a[0], json.s, json.c)
716
+ const isStatic = VrpcAdapter._isStaticContext(json.c)
717
+ VrpcAdapter._registrations.withdrawEvent(
718
+ VrpcAdapter._contextKey(json.c, isStatic),
719
+ json.a[0],
720
+ json.s
721
+ )
498
722
  json.r = true
499
723
  return
500
724
  }
@@ -507,66 +731,93 @@ class VrpcAdapter {
507
731
  ) {
508
732
  throw new Error(`Could not find context: ${json.c}`)
509
733
  }
510
- const unwrapped = VrpcAdapter._unwrapArguments(json)
511
- if (!unwrapped) {
512
- // this call must be skipped (event emitter stuff)
513
- json.r = true
514
- return
515
- }
516
-
517
734
  // Check whether context is global
518
735
  if (json.c === VRPC_GLOBAL) {
736
+ if (!VrpcAdapter._isPublished(VRPC_GLOBAL, json.f, true)) {
737
+ throw new Error(`Could not find function: ${json.f}`)
738
+ }
519
739
  try {
740
+ const unwrapped = VrpcAdapter._unwrapArguments(json)
520
741
  const ret = window[VRPC_WINDOW_NS][json.f].apply(null, unwrapped)
521
742
  // check if function returns promise
522
743
  if (VrpcAdapter._isPromise(ret)) {
523
744
  VrpcAdapter._handlePromise(json, ret)
524
745
  } else json.r = ret
525
746
  } catch (err) {
526
- json.e = err.message
747
+ json.e = VrpcAdapter._error(err)
527
748
  }
528
749
  return
529
750
  }
530
-
531
- // Check whether context is a registered class
532
- const entry = VrpcAdapter._functionRegistry.get(json.c)
533
- if (entry !== undefined) {
534
- // entry is class -> function is static
535
- const { Klass } = entry
536
- // TODO Think about whether to do live checking (like here) or
537
- // rather sticking to those functions registered before...
538
- // TODO This is even more important now as the agent does a wildcard
539
- // subscription against all functions
540
- if (VrpcAdapter._isFunction(Klass[json.f])) {
541
- try {
542
- const ret = Klass[json.f].apply(null, unwrapped)
543
- // check if function returns promise
544
- if (VrpcAdapter._isPromise(ret)) {
545
- VrpcAdapter._handlePromise(json, ret)
546
- } else json.r = ret
547
- } catch (err) {
548
- json.e = err.message
549
- }
550
- } else throw new Error(`Could not find static function: ${json.f}`)
751
+ // Check whether context is a registered class (function is static)
752
+ const classEntry = VrpcAdapter._functionRegistry.get(json.c)
753
+ const isStatic = classEntry !== undefined
754
+ let target
755
+ let className = json.c
756
+ if (isStatic) {
757
+ target = classEntry.Klass
551
758
  } else {
552
- // is not static
553
759
  const entry = VrpcAdapter._instances.get(json.c)
554
760
  if (entry === undefined) {
555
761
  throw new Error(`Could not find context: ${json.c}`)
556
762
  }
557
- const { instance } = entry
558
- if (VrpcAdapter._isFunction(instance[json.f])) {
559
- try {
560
- let ret = instance[json.f].apply(instance, unwrapped)
561
- ret = VrpcAdapter.sanitizeEventListenerReturnValues(ret)
562
- // check if function returns promise
563
- if (VrpcAdapter._isPromise(ret)) {
564
- VrpcAdapter._handlePromise(json, ret)
565
- } else json.r = ret
566
- } catch (err) {
567
- json.e = err.message
568
- }
569
- } else throw new Error(`Could not find member function: ${json.f}`)
763
+ target = entry.instance
764
+ className = entry.className
765
+ }
766
+ const carriesIds = Registrations.carriesEventIds(json.a)
767
+ // only what the class info lists is called; a remover carrying ids never
768
+ // reaches the class (see Registrations)
769
+ if (
770
+ !(carriesIds && Registrations.isRemover(json.f, json.v)) &&
771
+ !VrpcAdapter._isPublished(className, json.f, isStatic)
772
+ ) {
773
+ throw new Error(`Could not find function: ${json.f}`)
774
+ }
775
+ // a call carrying subscription ids subscribes (or withdraws)
776
+ if (carriesIds) {
777
+ try {
778
+ const { value } = VrpcAdapter._subscribe(json, {
779
+ contextKey: VrpcAdapter._contextKey(json.c, isStatic),
780
+ contextName: json.c,
781
+ target,
782
+ func: json.f,
783
+ args: json.a
784
+ })
785
+ if (VrpcAdapter._isPromise(value)) {
786
+ VrpcAdapter._handlePromise(json, value)
787
+ } else json.r = value
788
+ } catch (err) {
789
+ json.e = VrpcAdapter._error(err)
790
+ }
791
+ return
792
+ }
793
+ const unwrapped = VrpcAdapter._unwrapArguments(json)
794
+ // special case: removing a listener the adapter does not hold (the
795
+ // client's subscription outlived a restart of this agent, or it sent
796
+ // no listener at all) - nothing to remove, and the instance's
797
+ // EventEmitter would throw on a listener that is not a function
798
+ if (
799
+ (json.f === 'off' || json.f === 'removeListener') &&
800
+ typeof unwrapped[1] !== 'function'
801
+ ) {
802
+ json.r = true
803
+ return
804
+ }
805
+ // TODO Think about whether to do live checking (like here) or
806
+ // rather sticking to those functions registered before...
807
+ if (!VrpcAdapter._isFunction(target[json.f])) {
808
+ throw new Error(`Could not find function: ${json.f}`)
809
+ }
810
+ try {
811
+ let ret = isStatic
812
+ ? target[json.f].apply(null, unwrapped)
813
+ : target[json.f].apply(target, unwrapped)
814
+ if (!isStatic) ret = VrpcAdapter.sanitizeEventListenerReturnValues(ret)
815
+ // check if function returns promise
816
+ if (VrpcAdapter._isPromise(ret)) {
817
+ VrpcAdapter._handlePromise(json, ret)
818
+ } else json.r = ret
819
+ } catch (err) {
820
+ json.e = VrpcAdapter._error(err)
570
821
  }
571
822
  }
572
823
 
@@ -588,9 +839,10 @@ class VrpcAdapter {
588
839
  /**
589
840
  * Ends an instance - the one teardown behind every door (a remote delete,
590
841
  * the auto-delete of an isolated instance whose client went, an
591
- * in-process delete): its name is free at once, the adapter takes back
592
- * every listener it attached (silent from now on anyway, see
593
- * _generateListener), and the object releases what it runs through the
842
+ * in-process delete): its name is free at once, the adapter forgets its
843
+ * registrations (silent from now on, see Registrations; released by the
844
+ * adapter only for an object without a dispose method, and Node's own
845
+ * `on` always), and the object releases what it runs through the
594
846
  * ECMAScript dispose protocol, `[Symbol.asyncDispose]` or else
595
847
  * `[Symbol.dispose]`.
596
848
  *
@@ -601,33 +853,16 @@ class VrpcAdapter {
601
853
  const entry = VrpcAdapter._instances.get(instanceId)
602
854
  if (!entry) return { ended: false, disposed: null }
603
855
  VrpcAdapter._instances.delete(instanceId)
604
- VrpcAdapter._detachListeners(instanceId, entry.instance)
856
+ const { instance } = entry
857
+ VrpcAdapter._registrations.forgetContext(instanceId, {
858
+ release: !(
859
+ VrpcAdapter._isFunction(instance[Symbol.asyncDispose]) ||
860
+ VrpcAdapter._isFunction(instance[Symbol.dispose])
861
+ )
862
+ })
605
863
  return { ended: true, disposed: VrpcAdapter._dispose(instanceId, entry) }
606
864
  }
607
865
 
608
- static _detachListeners (instanceId, instance) {
609
- const registrations = VrpcAdapter._listeners[instanceId]
610
- delete VrpcAdapter._listeners[instanceId]
611
- if (!registrations || !VrpcAdapter._isFunction(instance.removeListener)) {
612
- return
613
- }
614
- for (const { event, listener, clients } of Object.values(registrations)) {
615
- try {
616
- VrpcAdapter._removeTimes(instance, event, listener, clients.length)
617
- } catch (err) {
618
- // a class whose removeListener fails keeps a silent listener
619
- }
620
- }
621
- }
622
-
623
- // a subscription repeated n times may hold n attachments of one listener
624
- // (EventEmitter semantics): each removeListener takes one of them
625
- static _removeTimes (instance, event, listener, times) {
626
- for (let n = 0; n < Math.max(1, times); n++) {
627
- instance.removeListener(event, listener)
628
- }
629
- }
630
-
631
866
  /**
632
867
  * Runs the object's dispose method. A dispose that fails or outlasts
633
868
  * VrpcAdapter.DISPOSE_TIMEOUT is reported (event 'disposeFailed') and
@@ -673,24 +908,7 @@ class VrpcAdapter {
673
908
  }
674
909
 
675
910
  static _unregisterClient (clientId) {
676
- Object.entries(VrpcAdapter._listeners).forEach(([ik, iv]) => {
677
- Object.entries(iv).forEach(([ek, ev]) => {
678
- const { listener, event } = ev
679
- // a client is listed once per subscription: its going ends them all
680
- const remaining = ev.clients.filter(x => x !== clientId)
681
- const gone = ev.clients.length - remaining.length
682
- if (gone === 0) return
683
- ev.clients = remaining
684
- if (remaining.length === 0) {
685
- const entry = VrpcAdapter._instances.get(ik)
686
- const instance = entry && entry.instance
687
- if (instance && instance.removeListener) {
688
- VrpcAdapter._removeTimes(instance, event, listener, gone)
689
- }
690
- delete VrpcAdapter._listeners[ik][ek]
691
- }
692
- })
693
- })
911
+ VrpcAdapter._registrations.withdrawClient(clientId)
694
912
  }
695
913
 
696
914
  static _validate (schema, params) {
@@ -701,6 +919,7 @@ class VrpcAdapter {
701
919
  if (!valid) throw new Error(VrpcAdapter._ajv.errorsText())
702
920
  }
703
921
 
922
+ // the arguments of a plain call: `__f__` ids become one-shot callbacks
704
923
  static _unwrapArguments (json, instanceId) {
705
924
  const clientId = json.s
706
925
  // if we came from a "callAll" we will have an instanceId defined
@@ -711,59 +930,17 @@ class VrpcAdapter {
711
930
  const func = isCallAll ? json.a[0] : json.f
712
931
  const unwrapped = isCallAll ? [func] : []
713
932
  for (const arg of args) {
714
- if (typeof arg === 'string') {
715
- if (arg.startsWith('__f__')) {
716
- // function callback
717
- unwrapped.push(
718
- VrpcAdapter._generateCallback({
719
- clientId,
720
- isCallAll,
721
- instanceId: context,
722
- eventId: arg
723
- })
724
- )
725
- } else if (arg.startsWith('__e__')) {
726
- // event registration
727
- if (func === 'on' || func === 'addListener') {
728
- VrpcAdapter._mustTrackClient = true
729
- const held = VrpcAdapter._holdsListener(context, arg)
730
- const listener = VrpcAdapter._registerListener({
731
- clientId,
732
- isCallAll,
733
- instanceId: context,
734
- eventId: arg,
735
- event: args[0]
736
- })
737
- // attached already: skip the call, one listener hears once
738
- if (held) return null
739
- unwrapped.push(listener)
740
- // event un-registration
741
- } else if (func === 'off' || func === 'removeListener') {
742
- const listener = VrpcAdapter._unregisterListener({
743
- clientId,
744
- instanceId: context,
745
- eventId: arg
746
- })
747
- if (!listener) {
748
- return null // skip call as others are still interested
749
- }
750
- unwrapped.push(listener)
751
- } else {
752
- VrpcAdapter._mustTrackClient = true
753
- const listener = VrpcAdapter._registerListener({
754
- clientId,
755
- isCallAll,
756
- instanceId: context,
757
- eventId: arg,
758
- event: null
759
- })
760
- unwrapped.push(listener)
761
- }
762
- } else {
763
- unwrapped.push(arg)
764
- }
933
+ if (typeof arg === 'string' && arg.startsWith('__f__')) {
934
+ unwrapped.push(
935
+ VrpcAdapter._generateCallback({
936
+ clientId,
937
+ isCallAll,
938
+ instanceId: context,
939
+ eventId: arg,
940
+ receiverVersion: json.v
941
+ })
942
+ )
765
943
  } else {
766
- // regular argument
767
944
  unwrapped.push(arg)
768
945
  }
769
946
  }
@@ -787,113 +964,38 @@ class VrpcAdapter {
787
964
  }
788
965
  }
789
966
 
790
- static _holdsListener (instanceId, eventId) {
791
- const registrations = VrpcAdapter._listeners[instanceId]
792
- return Boolean(registrations && registrations[eventId])
793
- }
794
-
795
- static _generateCallback ({ eventId, clientId, instanceId, isCallAll }) {
796
- const speaks = VrpcAdapter._speaksFor(instanceId)
797
- return (...innerArgs) => {
798
- if (!speaks()) return
799
- // "callAll" event notifications expect the instanceId as first argument
800
- const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
801
- VrpcAdapter._callback({ a, s: clientId, i: eventId })
802
- }
803
- }
804
-
805
- static _registerListener ({
967
+ static _generateCallback ({
806
968
  eventId,
807
969
  clientId,
808
970
  instanceId,
809
- event,
810
- isCallAll
971
+ isCallAll,
972
+ receiverVersion
811
973
  }) {
812
- if (!VrpcAdapter._listeners[instanceId]) {
813
- VrpcAdapter._listeners[instanceId] = {
814
- [eventId]: {
815
- event,
816
- clients: [clientId],
817
- listener: VrpcAdapter._generateListener({
818
- eventId,
819
- instanceId,
820
- isCallAll
821
- })
822
- }
823
- }
824
- return VrpcAdapter._listeners[instanceId][eventId].listener
825
- }
826
- if (!VrpcAdapter._listeners[instanceId][eventId]) {
827
- VrpcAdapter._listeners[instanceId][eventId] = {
828
- event,
829
- clients: [clientId],
830
- listener: VrpcAdapter._generateListener({
831
- eventId,
832
- instanceId,
833
- isCallAll
834
- })
835
- }
836
- return VrpcAdapter._listeners[instanceId][eventId].listener
837
- }
838
- VrpcAdapter._listeners[instanceId][eventId].clients.push(clientId)
839
- return VrpcAdapter._listeners[instanceId][eventId].listener
840
- }
841
-
842
- static _generateListener ({ eventId, instanceId, isCallAll }) {
843
974
  const speaks = VrpcAdapter._speaksFor(instanceId)
844
975
  return (...innerArgs) => {
845
976
  if (!speaks()) return
977
+ // "callAll" event notifications expect the instanceId as first argument
846
978
  const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
847
- VrpcAdapter._callback({ a, i: eventId })
979
+ // the receiver's protocol version: whether it can be told of a
980
+ // callback that cannot travel (CALL-10)
981
+ VrpcAdapter._callback({ a, s: clientId, i: eventId, receiverVersion })
848
982
  }
849
983
  }
850
984
 
851
- static _unregisterListener ({ eventId, clientId, instanceId }) {
852
- if (
853
- VrpcAdapter._listeners[instanceId] &&
854
- VrpcAdapter._listeners[instanceId][eventId]
855
- ) {
856
- const { listener, clients } = VrpcAdapter._listeners[instanceId][eventId]
857
- const index = clients.indexOf(clientId)
858
- if (index !== -1) clients.splice(index, 1)
859
- if (clients.length === 0) {
860
- delete VrpcAdapter._listeners[instanceId][eventId]
861
- if (Object.keys(VrpcAdapter._listeners[instanceId]).length === 0) {
862
- delete VrpcAdapter._listeners[instanceId]
863
- }
864
- return listener
865
- }
866
- }
867
- }
868
-
869
- static _removeAllListeners (event, clientId, instanceId) {
870
- const eventIds = VrpcAdapter._listeners[instanceId]
871
- if (!eventIds) return
872
- Object.entries(eventIds).forEach(([ek, ev]) => {
873
- if (ev.event === event && ev.clients.includes(clientId)) {
874
- ev.clients = ev.clients.filter(x => x !== clientId)
875
- if (ev.clients.length === 0) {
876
- VrpcAdapter.getInstance(instanceId).removeListener(
877
- ev.event,
878
- ev.listener
879
- )
880
- delete VrpcAdapter._listeners[instanceId][ek]
881
- }
882
- }
883
- })
884
- }
885
-
985
+ /**
986
+ * Answers a promise twice: at once with `r: '__p__<f>-<n>'`, and once it
987
+ * settled with a second answer under that id, `{ s, i, r }` or
988
+ * `{ s, i, e }`.
989
+ */
886
990
  static _handlePromise (json, promise) {
887
- try {
888
- const cid = VrpcAdapter._correlationId++ % Number.MAX_SAFE_INTEGER
889
- const id = `__p__${json.f}-${cid}`
890
- json.r = id
891
- promise
892
- .then(r => VrpcAdapter._callback({ ...json, r, i: id }))
893
- .catch(err => VrpcAdapter._callback({ ...json, e: err.message, i: id }))
894
- } catch (err) {
895
- json.data.e = err.message
896
- }
991
+ const cid = VrpcAdapter._correlationId++ % Number.MAX_SAFE_INTEGER
992
+ const id = `__p__${json.f}-${cid}`
993
+ const { s } = json
994
+ json.r = id
995
+ promise.then(
996
+ r => VrpcAdapter._callback({ s, i: id, r }),
997
+ err => VrpcAdapter._callback({ s, i: id, e: VrpcAdapter._error(err) })
998
+ )
897
999
  }
898
1000
 
899
1001
  static _getClassEntry (className) {
@@ -929,6 +1031,18 @@ class VrpcAdapter {
929
1031
  static _extractMemberFunctions (klass) {
930
1032
  let klass_ = klass
931
1033
  let fs = []
1034
+ // Node's EventEmitter offers subscribing, withdrawing and once to
1035
+ // clients; emit (anyone could fake an event to every subscriber, or emit
1036
+ // 'error' and end the agent) and its internals stay inside the class
1037
+ const isEmitter =
1038
+ klass instanceof EventEmitter ||
1039
+ (!!klass.prototype && klass.prototype instanceof EventEmitter)
1040
+ const published = (names, level) => {
1041
+ if (level === EventEmitter.prototype) {
1042
+ return names.filter(x => VrpcAdapter._emitterPublic.has(x))
1043
+ }
1044
+ return isEmitter ? names.filter(x => x !== 'emit') : names
1045
+ }
932
1046
  do {
933
1047
  if (klass_.prototype) {
934
1048
  const funcs = Object.getOwnPropertyNames(klass_.prototype).filter(
@@ -936,13 +1050,12 @@ class VrpcAdapter {
936
1050
  !VrpcAdapter._blackList.has(x) &&
937
1051
  VrpcAdapter._isFunction(klass_.prototype[x])
938
1052
  )
939
- fs = [...fs, ...funcs]
1053
+ fs = [...fs, ...published(funcs, klass_.prototype)]
940
1054
  } else {
941
1055
  const funcs = Object.getOwnPropertyNames(klass_).filter(
942
- x =>
943
- !VrpcAdapter._blackList.has(x) && VrpcAdapter._isFunction(klass_[x])
1056
+ x => !VrpcAdapter._blackList.has(x) && VrpcAdapter._isFunction(klass_[x])
944
1057
  )
945
- fs = [...fs, ...funcs]
1058
+ fs = [...fs, ...published(funcs, klass_)]
946
1059
  }
947
1060
  klass_ = Object.getPrototypeOf(klass_)
948
1061
  } while (klass_)
@@ -954,13 +1067,16 @@ class VrpcAdapter {
954
1067
  let klass_ = klass
955
1068
  let fs = []
956
1069
  do {
957
- fs = [
958
- ...fs,
959
- ...Object.getOwnPropertyNames(klass_).filter(prop => {
960
- const desc = Object.getOwnPropertyDescriptor(klass_, prop)
961
- return !!desc && typeof desc.value === 'function'
962
- })
963
- ]
1070
+ // EventEmitter's own statics are Node helpers, not the class's API
1071
+ if (klass_ !== EventEmitter) {
1072
+ fs = [
1073
+ ...fs,
1074
+ ...Object.getOwnPropertyNames(klass_).filter(prop => {
1075
+ const desc = Object.getOwnPropertyDescriptor(klass_, prop)
1076
+ return !!desc && typeof desc.value === 'function'
1077
+ })
1078
+ ]
1079
+ }
964
1080
  klass_ = Object.getPrototypeOf(klass_)
965
1081
  } while (klass_ && klass_.name)
966
1082
  // Filter out all duplicates
@@ -1041,10 +1157,26 @@ VrpcAdapter._functionRegistry = new Map()
1041
1157
  VrpcAdapter._instances = new Map()
1042
1158
  VrpcAdapter._emitter = new EventEmitter()
1043
1159
  VrpcAdapter._correlationId = 0
1044
- VrpcAdapter._listeners = {}
1160
+ // the event registrations of every context and their subscriptions
1161
+ VrpcAdapter._registrations = new Registrations({
1162
+ publish: message => VrpcAdapter._callback(message),
1163
+ emit: (event, payload) => VrpcAdapter._emitter.emit(event, payload)
1164
+ })
1045
1165
  // the longest a delete waits for the dispose method of its instance
1046
1166
  VrpcAdapter.DISPOSE_TIMEOUT = 10000
1167
+ // the EventEmitter functions published to clients
1168
+ VrpcAdapter._emitterPublic = new Set([
1169
+ 'on',
1170
+ 'addListener',
1171
+ 'off',
1172
+ 'removeListener',
1173
+ 'once',
1174
+ 'removeAllListeners'
1175
+ ])
1047
1176
  VrpcAdapter._blackList = new Set([
1177
+ // a class's constructor is reached through __createShared__ and
1178
+ // __createIsolated__, never as a member function
1179
+ 'constructor',
1048
1180
  'caller',
1049
1181
  'callee',
1050
1182
  'arguments',