vrpc 3.14.0 → 3.16.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
@@ -67,7 +69,9 @@ class VrpcAdapter {
67
69
  * @param {Boolean} [options.withNew=true] If true, class will be constructed
68
70
  * using the `new` operator
69
71
  * @param {Object} [options.schema=null] If provided is used to validate ctor
70
- * parameters (only works if registered code reflects a single class)
72
+ * parameters (only works if registered code reflects a single class). The
73
+ * browser bundle (`browser/vrpc.js`) does not validate: it warns once and
74
+ * constructs with the arguments as given
71
75
  */
72
76
  static registerClass (
73
77
  Klass,
@@ -82,6 +86,7 @@ class VrpcAdapter {
82
86
  staticFunctions.push('__createIsolated__')
83
87
  staticFunctions.push('__createShared__')
84
88
  staticFunctions.push('__callAll__')
89
+ staticFunctions.push('__callMatching__')
85
90
  staticFunctions.push('__delete__')
86
91
  let memberFunctions = VrpcAdapter._extractMemberFunctions(Klass)
87
92
  if (onlyPublic) {
@@ -303,8 +308,35 @@ class VrpcAdapter {
303
308
 
304
309
  // private:
305
310
 
311
+ /**
312
+ * Handles one request in place: the answer lands on `json` as `r` or `e`.
313
+ * Every request is answered - a malformed one, an unknown context or
314
+ * function with an error - so no caller waits for its timeout.
315
+ *
316
+ * @returns {Boolean} Whether the call took subscriptions of its sender,
317
+ * whose presence the agent then watches
318
+ */
306
319
  static _call (json) {
307
320
  VrpcAdapter._mustTrackClient = false // reset client tracking flag
321
+ // the answer is what the call yields, never what the request brought
322
+ delete json.r
323
+ delete json.e
324
+ try {
325
+ // a request without arguments has none
326
+ if (json.a === undefined) json.a = []
327
+ if (!Array.isArray(json.a)) {
328
+ throw new Error('The arguments of a request must be an array')
329
+ }
330
+ VrpcAdapter._dispatch(json)
331
+ } catch (err) {
332
+ json.e = VrpcAdapter._error(err)
333
+ }
334
+ // if we took a listener on board we must tell our caller such that
335
+ // things can be cleaned up later.
336
+ return VrpcAdapter._mustTrackClient
337
+ }
338
+
339
+ static _dispatch (json) {
308
340
  switch (json.f) {
309
341
  case '__createIsolated__':
310
342
  VrpcAdapter._handleCreateIsolated(json)
@@ -315,16 +347,49 @@ class VrpcAdapter {
315
347
  case '__callAll__':
316
348
  VrpcAdapter._handleCallAll(json)
317
349
  break
350
+ case '__callMatching__':
351
+ VrpcAdapter._handleCallMatching(json)
352
+ break
318
353
  case '__delete__':
319
354
  VrpcAdapter._handleDelete(json)
320
355
  break
356
+ case '__unsubscribe__':
357
+ VrpcAdapter._handleUnsubscribe(json)
358
+ break
359
+ case '__registrations__':
360
+ VrpcAdapter._handleRegistrations(json)
361
+ break
362
+ case '__end__':
363
+ VrpcAdapter._handleEnd(json)
364
+ break
321
365
  default:
322
366
  // this call may have the side-effect of taking a listener on board
323
367
  VrpcAdapter._handleCall(json)
324
368
  }
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
369
+ }
370
+
371
+ // an error as it travels: `{ message, cause? }`, whatever was thrown
372
+ static _error (err) {
373
+ if (err && typeof err === 'object' && 'message' in err) {
374
+ const e = { message: String(err.message) }
375
+ if (err.cause !== undefined) e.cause = err.cause
376
+ return e
377
+ }
378
+ return { message: String(err) }
379
+ }
380
+
381
+ /**
382
+ * Whether a request may call a function: only what the class info lists
383
+ * is reachable - a member function of the instance's class, a static
384
+ * function of the class, or a registered global function. Anything else
385
+ * an object happens to have (an EventEmitter's `emit`, a private helper)
386
+ * stays inside the agent.
387
+ */
388
+ static _isPublished (className, func, isStatic) {
389
+ const entry = VrpcAdapter._functionRegistry.get(className)
390
+ if (!entry) return false
391
+ const published = isStatic ? entry.staticFunctions : entry.memberFunctions
392
+ return published.includes(func)
328
393
  }
329
394
 
330
395
  /**
@@ -343,11 +408,28 @@ class VrpcAdapter {
343
408
  )
344
409
  }
345
410
 
411
+ /**
412
+ * An instance name is one topic level of its own: a non-empty string
413
+ * without `/`, `+`, `#` or `*` (which would read as a pattern), and never
414
+ * `__static__`, which stands for the class in a request topic.
415
+ */
416
+ static _assertInstanceName (name) {
417
+ if (
418
+ typeof name !== 'string' ||
419
+ name.length === 0 ||
420
+ /[/+#*]/.test(name) ||
421
+ name === '__static__'
422
+ ) {
423
+ throw new Error(`Invalid instance name: ${JSON.stringify(name)}`)
424
+ }
425
+ }
426
+
346
427
  static _handleCreateIsolated (json) {
347
428
  let instance
348
429
  try {
349
430
  const className = json.c
350
431
  const [instanceId, ...args] = json.a
432
+ VrpcAdapter._assertInstanceName(instanceId)
351
433
  const existing = VrpcAdapter._instances.get(instanceId)
352
434
  if (existing) {
353
435
  // an id is never re-homed: a shared instance stays shared, an
@@ -375,7 +457,7 @@ class VrpcAdapter {
375
457
  })
376
458
  json.r = instanceId
377
459
  } catch (err) {
378
- json.e = err.message
460
+ json.e = VrpcAdapter._error(err)
379
461
  }
380
462
  return instance
381
463
  }
@@ -385,6 +467,7 @@ class VrpcAdapter {
385
467
  try {
386
468
  const className = json.c
387
469
  const [instanceId, ...args] = json.a
470
+ VrpcAdapter._assertInstanceName(instanceId)
388
471
  const existing = VrpcAdapter._instances.get(instanceId)
389
472
  if (existing && existing.isIsolated) {
390
473
  throw new Error(`Instance '${instanceId}' exists as an isolated instance`)
@@ -403,66 +486,203 @@ class VrpcAdapter {
403
486
  instance: instanceId
404
487
  })
405
488
  } catch (err) {
406
- json.e = err.message
489
+ json.e = VrpcAdapter._error(err)
407
490
  }
408
491
  return instance
409
492
  }
410
493
 
494
+ // `a` = [function, ...arguments]: every shared instance of the class (ALL-1)
411
495
  static _handleCallAll (json) {
496
+ const [func, ...args] = json.a
497
+ VrpcAdapter._callAll(json, func, args, () => true)
498
+ }
499
+
500
+ // `a` = [selection, function, ...arguments]: the shared instances whose
501
+ // name the selection matches, a pattern or a list of them (ALL-4, ALL-5)
502
+ static _handleCallMatching (json) {
503
+ const [selection, func, ...args] = json.a
504
+ if (!NamePattern.isSelection(selection)) {
505
+ json.e = {
506
+ message: `Invalid selection: ${JSON.stringify(selection)} (a pattern or a list of them)`
507
+ }
508
+ return
509
+ }
510
+ VrpcAdapter._callAll(json, func, args, id =>
511
+ NamePattern.selects(selection, id)
512
+ )
513
+ }
514
+
515
+ // calls a function on the shared instances of a class `chosen` takes, and
516
+ // answers one entry per instance (ALL-2)
517
+ static _callAll (json, func, args, chosen) {
412
518
  try {
413
519
  const calls = []
520
+ const subscribes = Registrations.carriesEventIds(args)
521
+ // a remover carrying ids never reaches the class (see Registrations)
522
+ const withdraws = subscribes && Registrations.isRemover(func, json.v)
523
+ if (!withdraws && !VrpcAdapter._isPublished(json.c, func, false)) {
524
+ throw new Error(`Could not find function: ${func}`)
525
+ }
526
+ // what the instance's callbacks unwrap from: the function and its
527
+ // arguments, as a __callAll__ request carries them
528
+ const asCallAll = { ...json, a: [func, ...args] }
414
529
  for (const [
415
530
  id,
416
531
  { className, instance, isIsolated }
417
532
  ] 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
533
+ // isolated instances never take part
534
+ if (className !== json.c || isIsolated || !chosen(id)) continue
535
+ // one answer per instance: `{ id, val }`, or `{ id, err }` with the
536
+ // error as it travels
422
537
  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)
538
+ let val
539
+ if (subscribes) {
540
+ val = VrpcAdapter._subscribe(json, {
541
+ contextKey: id,
542
+ contextName: id,
543
+ target: instance,
544
+ func,
545
+ args,
546
+ isCallAll: true
547
+ }).value
548
+ } else {
549
+ const unwrapped = VrpcAdapter._unwrapArguments(asCallAll, id)
550
+ val = instance[func].apply(instance, unwrapped.slice(1))
551
+ val = VrpcAdapter.sanitizeEventListenerReturnValues(val)
428
552
  }
429
- } catch (err) {
430
- e = err.message
431
- }
432
- if (VrpcAdapter._isPromise(v)) {
433
553
  calls.push(
434
- v
435
- .then(val => ({ id, val, err: null }))
436
- .catch(err => ({ id, err, val: null }))
554
+ VrpcAdapter._isPromise(val)
555
+ ? val.then(
556
+ x => ({ id, val: x }),
557
+ err => ({ id, err: VrpcAdapter._error(err) })
558
+ )
559
+ : { id, val }
437
560
  )
438
- } else {
439
- calls.push({ id, val: v, err: e })
561
+ } catch (err) {
562
+ calls.push({ id, err: VrpcAdapter._error(err) })
440
563
  }
441
564
  }
442
- this._handlePromise(
565
+ VrpcAdapter._handlePromise(
443
566
  json,
444
567
  Promise.allSettled(calls).then(result =>
445
568
  result.filter(x => x.status === 'fulfilled').map(x => x.value)
446
569
  )
447
570
  )
448
571
  } catch (err) {
449
- json.e = err.message
572
+ json.e = VrpcAdapter._error(err)
450
573
  }
451
574
  }
452
575
 
453
576
  static sanitizeEventListenerReturnValues (ret) {
454
- return ret &&
455
- typeof ret === 'object' &&
456
- ret._events !== undefined &&
457
- ret._eventsCount !== undefined
458
- ? true
459
- : ret
577
+ return Registrations.sanitize(ret)
578
+ }
579
+
580
+ /**
581
+ * The book key of a request's context: an instance id, or the class for
582
+ * static functions (kept apart from an instance of the same name).
583
+ */
584
+ static _contextKey (context, isStatic) {
585
+ return isStatic ? `\u0000${context}` : context
586
+ }
587
+
588
+ // a class name addresses the class, as in _handleCall
589
+ static _isStaticContext (context) {
590
+ return VrpcAdapter._functionRegistry.has(context)
591
+ }
592
+
593
+ /**
594
+ * A call carrying subscription ids: one registration per context,
595
+ * function and arguments, shared by every subscription (see
596
+ * Registrations).
597
+ */
598
+ static _subscribe (
599
+ json,
600
+ { contextKey, contextName, target, func, args, isCallAll = false }
601
+ ) {
602
+ const result = VrpcAdapter._registrations.call({
603
+ json,
604
+ contextKey,
605
+ contextName,
606
+ target,
607
+ func,
608
+ args,
609
+ isCallAll,
610
+ speaks: VrpcAdapter._speaksFor(contextName),
611
+ makeCallback: eventId =>
612
+ VrpcAdapter._generateCallback({
613
+ clientId: json.s,
614
+ isCallAll,
615
+ instanceId: contextName,
616
+ eventId,
617
+ receiverVersion: json.v
618
+ })
619
+ })
620
+ // a subscriber is tracked: its going withdraws what it holds
621
+ if (result.ids.length > 0) VrpcAdapter._mustTrackClient = true
622
+ return result
623
+ }
624
+
625
+ /**
626
+ * `__unsubscribe__ [id, ...]`: withdraws the sender's subscriptions
627
+ * wherever they are held; an entry `{ id, context }` withdraws that id
628
+ * on that instance only (one instance of a callAll subscription).
629
+ * Answers true.
630
+ */
631
+ static _handleUnsubscribe (json) {
632
+ VrpcAdapter._registrations.unsubscribe(json.a || [], json.s)
633
+ json.r = true
634
+ }
635
+
636
+ /**
637
+ * `__registrations__`: lists the registrations of an instance or (on the
638
+ * static topic) a class.
639
+ */
640
+ static _handleRegistrations (json) {
641
+ try {
642
+ const isStatic = VrpcAdapter._isStaticContext(json.c)
643
+ if (!isStatic) {
644
+ VrpcAdapter._assertOwner(VrpcAdapter._instances.get(json.c), json, json.c)
645
+ }
646
+ json.r = VrpcAdapter._registrations.list(
647
+ VrpcAdapter._contextKey(json.c, isStatic)
648
+ )
649
+ } catch (err) {
650
+ json.e = VrpcAdapter._error(err)
651
+ }
652
+ }
653
+
654
+ /**
655
+ * `__end__ [function, args, by]`: ends one registration for all its
656
+ * subscriptions, which are told who ended it. Answers whether it existed.
657
+ */
658
+ static _handleEnd (json) {
659
+ try {
660
+ const isStatic = VrpcAdapter._isStaticContext(json.c)
661
+ if (!isStatic) {
662
+ VrpcAdapter._assertOwner(VrpcAdapter._instances.get(json.c), json, json.c)
663
+ }
664
+ const [func, args = [], by = null] = json.a || []
665
+ json.r = VrpcAdapter._registrations.end(
666
+ VrpcAdapter._contextKey(json.c, isStatic),
667
+ func,
668
+ args,
669
+ by
670
+ )
671
+ } catch (err) {
672
+ json.e = VrpcAdapter._error(err)
673
+ }
460
674
  }
461
675
 
462
676
  static _handleDelete (json) {
463
677
  try {
464
678
  const instance = json.a[0] // first arg is instance to be deleted
465
- VrpcAdapter._assertOwner(VrpcAdapter._instances.get(instance), json, instance)
679
+ const entry = VrpcAdapter._instances.get(instance)
680
+ // a delete names the instance's own class: the class info of that
681
+ // class is what announces it gone
682
+ if (entry && entry.className !== json.c && json.s !== VrpcAdapter.LOCAL_SENDER) {
683
+ throw new Error(`Instance '${instance}' is not of class '${json.c}'`)
684
+ }
685
+ VrpcAdapter._assertOwner(entry, json, instance)
466
686
  VrpcAdapter._emitter.emit('beforeDelete', {
467
687
  instance,
468
688
  className: json.c
@@ -479,7 +699,7 @@ class VrpcAdapter {
479
699
  VrpcAdapter._handlePromise(json, disposed.then(() => ended))
480
700
  } else json.r = ended
481
701
  } catch (err) {
482
- json.e = err.message
702
+ json.e = VrpcAdapter._error(err)
483
703
  }
484
704
  }
485
705
 
@@ -488,13 +708,19 @@ class VrpcAdapter {
488
708
  try {
489
709
  VrpcAdapter._assertOwner(VrpcAdapter._instances.get(json.c), json, json.c)
490
710
  } catch (err) {
491
- const { message, cause } = err
492
- json.e = { message, cause }
711
+ json.e = VrpcAdapter._error(err)
493
712
  return
494
713
  }
495
- // special case: removeAllListeners
714
+ // special case: removeAllListeners withdraws the caller's subscriptions
715
+ // of that event - the emitter itself is never touched, it holds other
716
+ // clients' and its own listeners
496
717
  if (json.f === 'removeAllListeners') {
497
- VrpcAdapter._removeAllListeners(json.a[0], json.s, json.c)
718
+ const isStatic = VrpcAdapter._isStaticContext(json.c)
719
+ VrpcAdapter._registrations.withdrawEvent(
720
+ VrpcAdapter._contextKey(json.c, isStatic),
721
+ json.a[0],
722
+ json.s
723
+ )
498
724
  json.r = true
499
725
  return
500
726
  }
@@ -507,66 +733,93 @@ class VrpcAdapter {
507
733
  ) {
508
734
  throw new Error(`Could not find context: ${json.c}`)
509
735
  }
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
736
  // Check whether context is global
518
737
  if (json.c === VRPC_GLOBAL) {
738
+ if (!VrpcAdapter._isPublished(VRPC_GLOBAL, json.f, true)) {
739
+ throw new Error(`Could not find function: ${json.f}`)
740
+ }
519
741
  try {
742
+ const unwrapped = VrpcAdapter._unwrapArguments(json)
520
743
  const ret = window[VRPC_WINDOW_NS][json.f].apply(null, unwrapped)
521
744
  // check if function returns promise
522
745
  if (VrpcAdapter._isPromise(ret)) {
523
746
  VrpcAdapter._handlePromise(json, ret)
524
747
  } else json.r = ret
525
748
  } catch (err) {
526
- json.e = err.message
749
+ json.e = VrpcAdapter._error(err)
527
750
  }
528
751
  return
529
752
  }
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}`)
753
+ // Check whether context is a registered class (function is static)
754
+ const classEntry = VrpcAdapter._functionRegistry.get(json.c)
755
+ const isStatic = classEntry !== undefined
756
+ let target
757
+ let className = json.c
758
+ if (isStatic) {
759
+ target = classEntry.Klass
551
760
  } else {
552
- // is not static
553
761
  const entry = VrpcAdapter._instances.get(json.c)
554
762
  if (entry === undefined) {
555
763
  throw new Error(`Could not find context: ${json.c}`)
556
764
  }
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}`)
765
+ target = entry.instance
766
+ className = entry.className
767
+ }
768
+ const carriesIds = Registrations.carriesEventIds(json.a)
769
+ // only what the class info lists is called; a remover carrying ids never
770
+ // reaches the class (see Registrations)
771
+ if (
772
+ !(carriesIds && Registrations.isRemover(json.f, json.v)) &&
773
+ !VrpcAdapter._isPublished(className, json.f, isStatic)
774
+ ) {
775
+ throw new Error(`Could not find function: ${json.f}`)
776
+ }
777
+ // a call carrying subscription ids subscribes (or withdraws)
778
+ if (carriesIds) {
779
+ try {
780
+ const { value } = VrpcAdapter._subscribe(json, {
781
+ contextKey: VrpcAdapter._contextKey(json.c, isStatic),
782
+ contextName: json.c,
783
+ target,
784
+ func: json.f,
785
+ args: json.a
786
+ })
787
+ if (VrpcAdapter._isPromise(value)) {
788
+ VrpcAdapter._handlePromise(json, value)
789
+ } else json.r = value
790
+ } catch (err) {
791
+ json.e = VrpcAdapter._error(err)
792
+ }
793
+ return
794
+ }
795
+ const unwrapped = VrpcAdapter._unwrapArguments(json)
796
+ // special case: removing a listener the adapter does not hold (the
797
+ // client's subscription outlived a restart of this agent, or it sent
798
+ // no listener at all) - nothing to remove, and the instance's
799
+ // EventEmitter would throw on a listener that is not a function
800
+ if (
801
+ (json.f === 'off' || json.f === 'removeListener') &&
802
+ typeof unwrapped[1] !== 'function'
803
+ ) {
804
+ json.r = true
805
+ return
806
+ }
807
+ // TODO Think about whether to do live checking (like here) or
808
+ // rather sticking to those functions registered before...
809
+ if (!VrpcAdapter._isFunction(target[json.f])) {
810
+ throw new Error(`Could not find function: ${json.f}`)
811
+ }
812
+ try {
813
+ let ret = isStatic
814
+ ? target[json.f].apply(null, unwrapped)
815
+ : target[json.f].apply(target, unwrapped)
816
+ if (!isStatic) ret = VrpcAdapter.sanitizeEventListenerReturnValues(ret)
817
+ // check if function returns promise
818
+ if (VrpcAdapter._isPromise(ret)) {
819
+ VrpcAdapter._handlePromise(json, ret)
820
+ } else json.r = ret
821
+ } catch (err) {
822
+ json.e = VrpcAdapter._error(err)
570
823
  }
571
824
  }
572
825
 
@@ -588,9 +841,10 @@ class VrpcAdapter {
588
841
  /**
589
842
  * Ends an instance - the one teardown behind every door (a remote delete,
590
843
  * 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
844
+ * in-process delete): its name is free at once, the adapter forgets its
845
+ * registrations (silent from now on, see Registrations; released by the
846
+ * adapter only for an object without a dispose method, and Node's own
847
+ * `on` always), and the object releases what it runs through the
594
848
  * ECMAScript dispose protocol, `[Symbol.asyncDispose]` or else
595
849
  * `[Symbol.dispose]`.
596
850
  *
@@ -601,33 +855,16 @@ class VrpcAdapter {
601
855
  const entry = VrpcAdapter._instances.get(instanceId)
602
856
  if (!entry) return { ended: false, disposed: null }
603
857
  VrpcAdapter._instances.delete(instanceId)
604
- VrpcAdapter._detachListeners(instanceId, entry.instance)
858
+ const { instance } = entry
859
+ VrpcAdapter._registrations.forgetContext(instanceId, {
860
+ release: !(
861
+ VrpcAdapter._isFunction(instance[Symbol.asyncDispose]) ||
862
+ VrpcAdapter._isFunction(instance[Symbol.dispose])
863
+ )
864
+ })
605
865
  return { ended: true, disposed: VrpcAdapter._dispose(instanceId, entry) }
606
866
  }
607
867
 
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
868
  /**
632
869
  * Runs the object's dispose method. A dispose that fails or outlasts
633
870
  * VrpcAdapter.DISPOSE_TIMEOUT is reported (event 'disposeFailed') and
@@ -673,27 +910,21 @@ class VrpcAdapter {
673
910
  }
674
911
 
675
912
  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
- })
913
+ VrpcAdapter._registrations.withdrawClient(clientId)
694
914
  }
695
915
 
696
916
  static _validate (schema, params) {
917
+ // the browser bundle leaves ajv out - a fifth of its size, for agents in
918
+ // a page, which are rare: there a schema is not checked
919
+ if (typeof Ajv !== 'function') {
920
+ if (!VrpcAdapter._warnedNoSchemas) {
921
+ VrpcAdapter._warnedNoSchemas = true
922
+ console.warn(
923
+ 'vrpc: schema validation is not available in the browser build, constructor arguments are not checked'
924
+ )
925
+ }
926
+ return
927
+ }
697
928
  if (!VrpcAdapter._ajv) {
698
929
  VrpcAdapter._ajv = new Ajv({ useDefaults: true })
699
930
  }
@@ -701,6 +932,7 @@ class VrpcAdapter {
701
932
  if (!valid) throw new Error(VrpcAdapter._ajv.errorsText())
702
933
  }
703
934
 
935
+ // the arguments of a plain call: `__f__` ids become one-shot callbacks
704
936
  static _unwrapArguments (json, instanceId) {
705
937
  const clientId = json.s
706
938
  // if we came from a "callAll" we will have an instanceId defined
@@ -711,59 +943,17 @@ class VrpcAdapter {
711
943
  const func = isCallAll ? json.a[0] : json.f
712
944
  const unwrapped = isCallAll ? [func] : []
713
945
  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
- }
946
+ if (typeof arg === 'string' && arg.startsWith('__f__')) {
947
+ unwrapped.push(
948
+ VrpcAdapter._generateCallback({
949
+ clientId,
950
+ isCallAll,
951
+ instanceId: context,
952
+ eventId: arg,
953
+ receiverVersion: json.v
954
+ })
955
+ )
765
956
  } else {
766
- // regular argument
767
957
  unwrapped.push(arg)
768
958
  }
769
959
  }
@@ -787,113 +977,40 @@ class VrpcAdapter {
787
977
  }
788
978
  }
789
979
 
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 ({
980
+ static _generateCallback ({
806
981
  eventId,
807
982
  clientId,
808
983
  instanceId,
809
- event,
810
- isCallAll
984
+ isCallAll,
985
+ receiverVersion
811
986
  }) {
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
987
  const speaks = VrpcAdapter._speaksFor(instanceId)
844
988
  return (...innerArgs) => {
845
989
  if (!speaks()) return
990
+ // "callAll" event notifications expect the instanceId as first argument
846
991
  const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
847
- VrpcAdapter._callback({ a, i: eventId })
848
- }
849
- }
850
-
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
- }
992
+ // the receiver's protocol version: whether it can be told of a
993
+ // callback that cannot travel (CALL-10)
994
+ VrpcAdapter._callback({ a, s: clientId, i: eventId, receiverVersion })
866
995
  }
867
996
  }
868
997
 
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
-
998
+ /**
999
+ * Answers a promise twice: at once with `r: '__p__<f>-<nonce>-<n>'`, and
1000
+ * once it settled with a second answer under that id, `{ s, i, r }` or
1001
+ * `{ s, i, e }`. A client waits for the second answer under the bare id,
1002
+ * whichever agent sends it: the nonce keeps the ids of two agents apart,
1003
+ * whose counters may well stand at the same value.
1004
+ */
886
1005
  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
- }
1006
+ const cid = VrpcAdapter._correlationId++ % Number.MAX_SAFE_INTEGER
1007
+ const id = `__p__${json.f}-${VrpcAdapter._promiseNonce}-${cid}`
1008
+ const { s } = json
1009
+ json.r = id
1010
+ promise.then(
1011
+ r => VrpcAdapter._callback({ s, i: id, r }),
1012
+ err => VrpcAdapter._callback({ s, i: id, e: VrpcAdapter._error(err) })
1013
+ )
897
1014
  }
898
1015
 
899
1016
  static _getClassEntry (className) {
@@ -929,6 +1046,18 @@ class VrpcAdapter {
929
1046
  static _extractMemberFunctions (klass) {
930
1047
  let klass_ = klass
931
1048
  let fs = []
1049
+ // Node's EventEmitter offers subscribing, withdrawing and once to
1050
+ // clients; emit (anyone could fake an event to every subscriber, or emit
1051
+ // 'error' and end the agent) and its internals stay inside the class
1052
+ const isEmitter =
1053
+ klass instanceof EventEmitter ||
1054
+ (!!klass.prototype && klass.prototype instanceof EventEmitter)
1055
+ const published = (names, level) => {
1056
+ if (level === EventEmitter.prototype) {
1057
+ return names.filter(x => VrpcAdapter._emitterPublic.has(x))
1058
+ }
1059
+ return isEmitter ? names.filter(x => x !== 'emit') : names
1060
+ }
932
1061
  do {
933
1062
  if (klass_.prototype) {
934
1063
  const funcs = Object.getOwnPropertyNames(klass_.prototype).filter(
@@ -936,13 +1065,12 @@ class VrpcAdapter {
936
1065
  !VrpcAdapter._blackList.has(x) &&
937
1066
  VrpcAdapter._isFunction(klass_.prototype[x])
938
1067
  )
939
- fs = [...fs, ...funcs]
1068
+ fs = [...fs, ...published(funcs, klass_.prototype)]
940
1069
  } else {
941
1070
  const funcs = Object.getOwnPropertyNames(klass_).filter(
942
- x =>
943
- !VrpcAdapter._blackList.has(x) && VrpcAdapter._isFunction(klass_[x])
1071
+ x => !VrpcAdapter._blackList.has(x) && VrpcAdapter._isFunction(klass_[x])
944
1072
  )
945
- fs = [...fs, ...funcs]
1073
+ fs = [...fs, ...published(funcs, klass_)]
946
1074
  }
947
1075
  klass_ = Object.getPrototypeOf(klass_)
948
1076
  } while (klass_)
@@ -954,13 +1082,16 @@ class VrpcAdapter {
954
1082
  let klass_ = klass
955
1083
  let fs = []
956
1084
  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
- ]
1085
+ // EventEmitter's own statics are Node helpers, not the class's API
1086
+ if (klass_ !== EventEmitter) {
1087
+ fs = [
1088
+ ...fs,
1089
+ ...Object.getOwnPropertyNames(klass_).filter(prop => {
1090
+ const desc = Object.getOwnPropertyDescriptor(klass_, prop)
1091
+ return !!desc && typeof desc.value === 'function'
1092
+ })
1093
+ ]
1094
+ }
964
1095
  klass_ = Object.getPrototypeOf(klass_)
965
1096
  } while (klass_ && klass_.name)
966
1097
  // Filter out all duplicates
@@ -1041,10 +1172,28 @@ VrpcAdapter._functionRegistry = new Map()
1041
1172
  VrpcAdapter._instances = new Map()
1042
1173
  VrpcAdapter._emitter = new EventEmitter()
1043
1174
  VrpcAdapter._correlationId = 0
1044
- VrpcAdapter._listeners = {}
1175
+ // tells this process's promise ids from those of every other agent
1176
+ VrpcAdapter._promiseNonce = nanoid(8)
1177
+ // the event registrations of every context and their subscriptions
1178
+ VrpcAdapter._registrations = new Registrations({
1179
+ publish: message => VrpcAdapter._callback(message),
1180
+ emit: (event, payload) => VrpcAdapter._emitter.emit(event, payload)
1181
+ })
1045
1182
  // the longest a delete waits for the dispose method of its instance
1046
1183
  VrpcAdapter.DISPOSE_TIMEOUT = 10000
1184
+ // the EventEmitter functions published to clients
1185
+ VrpcAdapter._emitterPublic = new Set([
1186
+ 'on',
1187
+ 'addListener',
1188
+ 'off',
1189
+ 'removeListener',
1190
+ 'once',
1191
+ 'removeAllListeners'
1192
+ ])
1047
1193
  VrpcAdapter._blackList = new Set([
1194
+ // a class's constructor is reached through __createShared__ and
1195
+ // __createIsolated__, never as a member function
1196
+ 'constructor',
1048
1197
  'caller',
1049
1198
  'callee',
1050
1199
  'arguments',