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
@@ -42,6 +42,8 @@ const caller = require('caller')
42
42
  const commentParser = require('./comment-parser')
43
43
  const EventEmitter = require('events')
44
44
  const { nanoid } = require('nanoid')
45
+ const Registrations = require('./Registrations')
46
+ const NamePattern = require('./namePattern')
45
47
 
46
48
  // Node before 18.18 / 20.4 has no dispose symbols, and a class's
47
49
  // [Symbol.asyncDispose]() would become a method named 'undefined'. Node's
@@ -97,14 +99,16 @@ class VrpcAdapter {
97
99
  * @param {Any} code Existing code to be registered, can be a class
98
100
  * or function object or a relative path to a module
99
101
  * @param {Object} [options]
100
- * @param {Boolean} [options.onlyPublic=true] If true, only registers
101
- * functions that do not begin with an underscore
102
+ * @param {Boolean} [options.onlyPublic=true] If true, only registers public
103
+ * functions: none whose name begins with an underscore, and - when the
104
+ * JSDoc is read - none tagged `@private` or `@access private`
102
105
  * @param {Boolean} [options.withNew=true] If true, class will be constructed
103
106
  * using the `new` operator
104
107
  * @param {Object} [options.schema=null] If provided is used to validate ctor
105
108
  * parameters (only works if registered code reflects a single class)
106
- * @param {String} options.jsdocPath if provided, parses documentation and
107
- * provides it as meta information
109
+ * @param {String} [options.jsdocPath] if provided, parses documentation and
110
+ * provides it as meta information; a code given by path has its JSDoc read
111
+ * without
108
112
  *
109
113
  * NOTE: This function currently only supports registration of classes (either
110
114
  * when provided as object or when exported on the provided module path)
@@ -135,8 +139,9 @@ class VrpcAdapter {
135
139
  * @param {Object} options
136
140
  * @param {String} options.className Class name of the instance
137
141
  * @param {String} options.instance Name of the instance
138
- * @param {Boolean} [options.onlyPublic=true] If true, only registers
139
- * functions that do not begin with an underscore
142
+ * @param {Boolean} [options.onlyPublic=true] If true, only registers public
143
+ * functions: none whose name begins with an underscore, and - when the
144
+ * JSDoc is read - none tagged `@private` or `@access private`
140
145
  * @param {String} [options.jsdocPath] if provided, parses documentation and
141
146
  * provides it as meta information
142
147
  */
@@ -144,18 +149,17 @@ class VrpcAdapter {
144
149
  obj,
145
150
  { className, instance, jsdocPath, onlyPublic = true } = {}
146
151
  ) {
147
- let memberFunctions = VrpcAdapter._extractMemberFunctions(obj)
148
- if (onlyPublic) {
149
- memberFunctions = memberFunctions.filter(f => {
150
- return !f.startsWith('_')
151
- })
152
- }
153
- let meta = null
152
+ let docs = null
154
153
  if (jsdocPath) {
155
154
  const absJsdocPath = path.resolve(caller(), '../', jsdocPath)
156
155
  const content = fs.readFileSync(absJsdocPath).toString('utf8')
157
- meta = this._parseComments(content)
156
+ docs = VrpcAdapter._readDocs(content, obj.constructor && obj.constructor.name)
157
+ }
158
+ let memberFunctions = VrpcAdapter._extractMemberFunctions(obj)
159
+ if (onlyPublic) {
160
+ memberFunctions = VrpcAdapter._onlyPublic(memberFunctions, docs, 'member')
158
161
  }
162
+ const meta = docs ? VrpcAdapter._metaOf(docs.meta, memberFunctions) : null
159
163
  if (!VrpcAdapter._functionRegistry.has(className)) {
160
164
  VrpcAdapter._functionRegistry.set(className, {
161
165
  memberFunctions,
@@ -304,6 +308,13 @@ class VrpcAdapter {
304
308
  * @param {String} [type] Return value type
305
309
  */
306
310
 
311
+ /**
312
+ * Listens to an event of the adapter: `create`, `beforeDelete`,
313
+ * `delete`, `disposeFailed`, `releaseFailed` or `greetFailed`
314
+ *
315
+ * @param {String} eventName The event
316
+ * @param {Function} listener Called with the event's payload
317
+ */
307
318
  static on (eventName, listener) {
308
319
  VrpcAdapter._emitter.on(eventName, listener)
309
320
  }
@@ -328,26 +339,30 @@ class VrpcAdapter {
328
339
  { onlyPublic = true, withNew = true, schema = null, jsdoc = true } = {}
329
340
  ) {
330
341
  // Get all static static functions
342
+ const docs =
343
+ jsdoc && absJsdocPath
344
+ ? VrpcAdapter._readDocs(
345
+ fs.readFileSync(absJsdocPath).toString('utf8'),
346
+ Klass.name
347
+ )
348
+ : null
331
349
  let staticFunctions = VrpcAdapter._extractStaticFunctions(Klass)
332
350
  if (onlyPublic) {
333
- staticFunctions = staticFunctions.filter(f => !f.startsWith('_'))
351
+ staticFunctions = VrpcAdapter._onlyPublic(staticFunctions, docs, 'static')
334
352
  }
335
353
  // Inject constructor and destructor
336
354
  staticFunctions.push('__createIsolated__')
337
355
  staticFunctions.push('__createShared__')
338
356
  staticFunctions.push('__callAll__')
357
+ staticFunctions.push('__callMatching__')
339
358
  staticFunctions.push('__delete__')
340
359
  let memberFunctions = VrpcAdapter._extractMemberFunctions(Klass)
341
360
  if (onlyPublic) {
342
- memberFunctions = memberFunctions.filter(f => {
343
- return !f.startsWith('_')
344
- })
345
- }
346
- let meta = null
347
- if (jsdoc && absJsdocPath) {
348
- const content = fs.readFileSync(absJsdocPath).toString('utf8')
349
- meta = this._parseComments(content)
361
+ memberFunctions = VrpcAdapter._onlyPublic(memberFunctions, docs, 'member')
350
362
  }
363
+ const meta = docs
364
+ ? VrpcAdapter._metaOf(docs.meta, [...staticFunctions, ...memberFunctions])
365
+ : null
351
366
  VrpcAdapter._functionRegistry.set(Klass.name, {
352
367
  Klass,
353
368
  withNew,
@@ -358,8 +373,72 @@ class VrpcAdapter {
358
373
  })
359
374
  }
360
375
 
361
- static _parseComments (content) {
362
- const comments = commentParser(content, { assocFunctions: true })
376
+ /**
377
+ * The public of some functions: none beginning with an underscore, and
378
+ * none the class's JSDoc tags `@private` (when it was read)
379
+ */
380
+ static _onlyPublic (functions, docs, kind) {
381
+ return functions.filter(
382
+ f => !f.startsWith('_') && !(docs && docs.privates[kind].has(f))
383
+ )
384
+ }
385
+
386
+ /**
387
+ * The meta data of the published functions only: a hidden function's
388
+ * documentation stays inside the agent as well
389
+ */
390
+ static _metaOf (meta, published) {
391
+ return Object.fromEntries(
392
+ Object.entries(meta).filter(
393
+ ([name]) => name === '__createShared__' || published.includes(name)
394
+ )
395
+ )
396
+ }
397
+
398
+ /**
399
+ * The meta data of the functions a source documents (see `_readDocs`)
400
+ */
401
+ static _parseComments (content, className) {
402
+ return VrpcAdapter._readDocs(content, className).meta
403
+ }
404
+
405
+ /**
406
+ * Reads the JSDoc of a class's source: the meta data of its functions, and
407
+ * the functions tagged `@private` (or `@access private`), which stay off
408
+ * its class info and so cannot be called. A comment counts for the class
409
+ * whose body it is in; in a source that declares no class, and when no
410
+ * class name is given, every comment counts.
411
+ *
412
+ * @param {String} content the source
413
+ * @param {String} [className] the class to read the documentation of
414
+ * @returns {{ meta: Object, privates: { member: Set, static: Set } }}
415
+ */
416
+ static _readDocs (content, className) {
417
+ const declaresClasses = content
418
+ .split('\n')
419
+ .some(line => commentParser.CLASS_DECLARATION.test(line))
420
+ const comments = commentParser(content, { assocFunctions: true }).filter(
421
+ ({ functionName, className: owner }) =>
422
+ !functionName ||
423
+ className === undefined ||
424
+ owner === className ||
425
+ (!owner && !declaresClasses)
426
+ )
427
+ const privates = { member: new Set(), static: new Set() }
428
+ comments.forEach(({ tags, functionName, isStatic, className: owner }) => {
429
+ if (!functionName || functionName === 'constructor' || !tags) return
430
+ const hidden = tags.some(
431
+ ({ tag, name }) =>
432
+ tag === 'private' || (tag === 'access' && name === 'private')
433
+ )
434
+ if (!hidden) return
435
+ if (owner) {
436
+ privates[isStatic ? 'static' : 'member'].add(functionName)
437
+ } else {
438
+ privates.member.add(functionName)
439
+ privates.static.add(functionName)
440
+ }
441
+ })
363
442
  const meta = {}
364
443
  const paramsOf = tags =>
365
444
  tags
@@ -418,11 +497,38 @@ class VrpcAdapter {
418
497
  }
419
498
  }
420
499
  })
421
- return meta
500
+ return { meta, privates }
422
501
  }
423
502
 
503
+ /**
504
+ * Handles one request in place: the answer lands on `json` as `r` or `e`.
505
+ * Every request is answered - a malformed one, an unknown context or
506
+ * function with an error - so no caller waits for its timeout.
507
+ *
508
+ * @returns {Boolean} Whether the call took subscriptions of its sender,
509
+ * whose presence the agent then watches
510
+ */
424
511
  static _call (json) {
425
512
  this._mustTrackClient = false // reset client tracking flag
513
+ // the answer is what the call yields, never what the request brought
514
+ delete json.r
515
+ delete json.e
516
+ try {
517
+ // a request without arguments has none
518
+ if (json.a === undefined) json.a = []
519
+ if (!Array.isArray(json.a)) {
520
+ throw new Error('The arguments of a request must be an array')
521
+ }
522
+ VrpcAdapter._dispatch(json)
523
+ } catch (err) {
524
+ json.e = VrpcAdapter._error(err)
525
+ }
526
+ // if we took a listener on board we must tell our caller such that
527
+ // things can be cleaned up later.
528
+ return this._mustTrackClient
529
+ }
530
+
531
+ static _dispatch (json) {
426
532
  switch (json.f) {
427
533
  case '__createIsolated__':
428
534
  VrpcAdapter._handleCreateIsolated(json)
@@ -433,16 +539,48 @@ class VrpcAdapter {
433
539
  case '__callAll__':
434
540
  VrpcAdapter._handleCallAll(json)
435
541
  break
542
+ case '__callMatching__':
543
+ VrpcAdapter._handleCallMatching(json)
544
+ break
436
545
  case '__delete__':
437
546
  VrpcAdapter._handleDelete(json)
438
547
  break
548
+ case '__unsubscribe__':
549
+ VrpcAdapter._handleUnsubscribe(json)
550
+ break
551
+ case '__registrations__':
552
+ VrpcAdapter._handleRegistrations(json)
553
+ break
554
+ case '__end__':
555
+ VrpcAdapter._handleEnd(json)
556
+ break
439
557
  default:
440
558
  // this call may have the side-effect of taking a listener on board
441
559
  VrpcAdapter._handleCall(json)
442
560
  }
443
- // if we took a listener on board we must tell our caller such that
444
- // things can be cleaned up later.
445
- return this._mustTrackClient
561
+ }
562
+
563
+ // an error as it travels: `{ message, cause? }`, whatever was thrown
564
+ static _error (err) {
565
+ if (err && typeof err === 'object' && 'message' in err) {
566
+ const e = { message: String(err.message) }
567
+ if (err.cause !== undefined) e.cause = err.cause
568
+ return e
569
+ }
570
+ return { message: String(err) }
571
+ }
572
+
573
+ /**
574
+ * Whether a request may call a function: only what the class info lists
575
+ * is reachable - a member function of the instance's class, or a static
576
+ * function of the class. Anything else an object happens to have (an
577
+ * EventEmitter's `emit`, a private helper) stays inside the agent.
578
+ */
579
+ static _isPublished (className, func, isStatic) {
580
+ const entry = VrpcAdapter._functionRegistry.get(className)
581
+ if (!entry) return false
582
+ const published = isStatic ? entry.staticFunctions : entry.memberFunctions
583
+ return published.includes(func)
446
584
  }
447
585
 
448
586
  /**
@@ -461,11 +599,28 @@ class VrpcAdapter {
461
599
  )
462
600
  }
463
601
 
602
+ /**
603
+ * An instance name is one topic level of its own: a non-empty string
604
+ * without `/`, `+`, `#` or `*` (which would read as a pattern), and never
605
+ * `__static__`, which stands for the class in a request topic.
606
+ */
607
+ static _assertInstanceName (name) {
608
+ if (
609
+ typeof name !== 'string' ||
610
+ name.length === 0 ||
611
+ /[/+#*]/.test(name) ||
612
+ name === '__static__'
613
+ ) {
614
+ throw new Error(`Invalid instance name: ${JSON.stringify(name)}`)
615
+ }
616
+ }
617
+
464
618
  static _handleCreateIsolated (json) {
465
619
  let instance
466
620
  try {
467
621
  const className = json.c
468
622
  const [instanceId, ...args] = json.a
623
+ VrpcAdapter._assertInstanceName(instanceId)
469
624
  const existing = VrpcAdapter._instances.get(instanceId)
470
625
  if (existing) {
471
626
  // an id is never re-homed: a shared instance stays shared, an
@@ -504,6 +659,7 @@ class VrpcAdapter {
504
659
  try {
505
660
  const className = json.c
506
661
  const [instanceId, ...args] = json.a
662
+ VrpcAdapter._assertInstanceName(instanceId)
507
663
  const existing = VrpcAdapter._instances.get(instanceId)
508
664
  if (existing && existing.isIsolated) {
509
665
  throw new Error(`Instance '${instanceId}' exists as an isolated instance`)
@@ -528,35 +684,75 @@ class VrpcAdapter {
528
684
  return instance
529
685
  }
530
686
 
687
+ // `a` = [function, ...arguments]: every shared instance of the class (ALL-1)
531
688
  static _handleCallAll (json) {
689
+ const [func, ...args] = json.a
690
+ VrpcAdapter._callAll(json, func, args, () => true)
691
+ }
692
+
693
+ // `a` = [selection, function, ...arguments]: the shared instances whose
694
+ // name the selection matches, a pattern or a list of them (ALL-4, ALL-5)
695
+ static _handleCallMatching (json) {
696
+ const [selection, func, ...args] = json.a
697
+ if (!NamePattern.isSelection(selection)) {
698
+ json.e = {
699
+ message: `Invalid selection: ${JSON.stringify(selection)} (a pattern or a list of them)`
700
+ }
701
+ return
702
+ }
703
+ VrpcAdapter._callAll(json, func, args, id =>
704
+ NamePattern.selects(selection, id)
705
+ )
706
+ }
707
+
708
+ // calls a function on the shared instances of a class `chosen` takes, and
709
+ // answers one entry per instance (ALL-2)
710
+ static _callAll (json, func, args, chosen) {
532
711
  try {
533
712
  const calls = []
713
+ const subscribes = Registrations.carriesEventIds(args)
714
+ // a remover carrying ids never reaches the class (see Registrations)
715
+ const withdraws = subscribes && Registrations.isRemover(func, json.v)
716
+ if (!withdraws && !VrpcAdapter._isPublished(json.c, func, false)) {
717
+ throw new Error(`Could not find function: ${func}`)
718
+ }
719
+ // what the instance's callbacks unwrap from: the function and its
720
+ // arguments, as a __callAll__ request carries them
721
+ const asCallAll = { ...json, a: [func, ...args] }
534
722
  for (const [
535
723
  id,
536
724
  { className, instance, isIsolated }
537
725
  ] of VrpcAdapter._instances) {
538
- // TODO Think about a configurable behavior w.r.t. isolated instances...
539
- if (className !== json.c || isIsolated) continue
540
- let v = true
541
- let e = null
726
+ // isolated instances never take part
727
+ if (className !== json.c || isIsolated || !chosen(id)) continue
728
+ // one answer per instance: `{ id, val }`, or `{ id, err }` with the
729
+ // error as it travels
542
730
  try {
543
- const unwrapped = VrpcAdapter._unwrapArguments(json, id)
544
- if (unwrapped) {
545
- const funcName = unwrapped[0]
546
- v = instance[funcName].apply(instance, unwrapped.slice(1))
547
- v = VrpcAdapter.sanitizeEventListenerReturnValues(v)
731
+ let val
732
+ if (subscribes) {
733
+ val = VrpcAdapter._subscribe(json, {
734
+ contextKey: id,
735
+ contextName: id,
736
+ target: instance,
737
+ func,
738
+ args,
739
+ isCallAll: true
740
+ }).value
741
+ } else {
742
+ const unwrapped = VrpcAdapter._unwrapArguments(asCallAll, id)
743
+ val = instance[func].apply(instance, unwrapped.slice(1))
744
+ val = VrpcAdapter.sanitizeEventListenerReturnValues(val)
548
745
  }
549
- } catch (err) {
550
- e = err.message
551
- }
552
- if (VrpcAdapter._isPromise(v)) {
553
746
  calls.push(
554
- v
555
- .then(val => ({ id, val, err: null }))
556
- .catch(err => ({ id, err, val: null }))
747
+ VrpcAdapter._isPromise(val)
748
+ ? val.then(
749
+ x => ({ id, val: x }),
750
+ err => ({ id, err: VrpcAdapter._error(err) })
751
+ )
752
+ : { id, val }
557
753
  )
558
- } else {
559
- calls.push({ id, val: v, err: e })
754
+ } catch (err) {
755
+ calls.push({ id, err: VrpcAdapter._error(err) })
560
756
  }
561
757
  }
562
758
  this._handlePromise(
@@ -572,18 +768,117 @@ class VrpcAdapter {
572
768
  }
573
769
 
574
770
  static sanitizeEventListenerReturnValues (ret) {
575
- return ret &&
576
- typeof ret === 'object' &&
577
- ret._events !== undefined &&
578
- ret._eventsCount !== undefined
579
- ? true
580
- : ret
771
+ return Registrations.sanitize(ret)
772
+ }
773
+
774
+ /**
775
+ * The book key of a request's context: an instance id, or the class for
776
+ * static functions (kept apart from an instance of the same name).
777
+ */
778
+ static _contextKey (context, isStatic) {
779
+ return isStatic ? `\u0000${context}` : context
780
+ }
781
+
782
+ // a class name addresses the class, as in _handleCall
783
+ static _isStaticContext (context) {
784
+ return VrpcAdapter._functionRegistry.has(context)
785
+ }
786
+
787
+ /**
788
+ * A call carrying subscription ids: one registration per context,
789
+ * function and arguments, shared by every subscription (see
790
+ * Registrations).
791
+ */
792
+ static _subscribe (
793
+ json,
794
+ { contextKey, contextName, target, func, args, isCallAll = false }
795
+ ) {
796
+ const result = VrpcAdapter._registrations.call({
797
+ json,
798
+ contextKey,
799
+ contextName,
800
+ target,
801
+ func,
802
+ args,
803
+ isCallAll,
804
+ speaks: VrpcAdapter._speaksFor(contextName),
805
+ makeCallback: eventId =>
806
+ VrpcAdapter._generateCallback({
807
+ clientId: json.s,
808
+ isCallAll,
809
+ instanceId: contextName,
810
+ eventId,
811
+ receiverVersion: json.v
812
+ })
813
+ })
814
+ // a subscriber is tracked: its going withdraws what it holds
815
+ if (result.ids.length > 0) VrpcAdapter._mustTrackClient = true
816
+ return result
817
+ }
818
+
819
+ /**
820
+ * `__unsubscribe__ [id, ...]`: withdraws the sender's subscriptions
821
+ * wherever they are held; an entry `{ id, context }` withdraws that id
822
+ * on that instance only (one instance of a callAll subscription).
823
+ * Answers true.
824
+ */
825
+ static _handleUnsubscribe (json) {
826
+ VrpcAdapter._registrations.unsubscribe(json.a || [], json.s)
827
+ json.r = true
828
+ }
829
+
830
+ /**
831
+ * `__registrations__`: lists the registrations of an instance or (on the
832
+ * static topic) a class.
833
+ */
834
+ static _handleRegistrations (json) {
835
+ try {
836
+ const isStatic = VrpcAdapter._isStaticContext(json.c)
837
+ if (!isStatic) {
838
+ VrpcAdapter._assertOwner(VrpcAdapter._instances.get(json.c), json, json.c)
839
+ }
840
+ json.r = VrpcAdapter._registrations.list(
841
+ VrpcAdapter._contextKey(json.c, isStatic)
842
+ )
843
+ } catch (err) {
844
+ const { message, cause } = err
845
+ json.e = { message, cause }
846
+ }
847
+ }
848
+
849
+ /**
850
+ * `__end__ [function, args, by]`: ends one registration for all its
851
+ * subscriptions, which are told who ended it. Answers whether it existed.
852
+ */
853
+ static _handleEnd (json) {
854
+ try {
855
+ const isStatic = VrpcAdapter._isStaticContext(json.c)
856
+ if (!isStatic) {
857
+ VrpcAdapter._assertOwner(VrpcAdapter._instances.get(json.c), json, json.c)
858
+ }
859
+ const [func, args = [], by = null] = json.a || []
860
+ json.r = VrpcAdapter._registrations.end(
861
+ VrpcAdapter._contextKey(json.c, isStatic),
862
+ func,
863
+ args,
864
+ by
865
+ )
866
+ } catch (err) {
867
+ const { message, cause } = err
868
+ json.e = { message, cause }
869
+ }
581
870
  }
582
871
 
583
872
  static _handleDelete (json) {
584
873
  try {
585
874
  const instance = json.a[0] // first arg is instance to be deleted
586
- VrpcAdapter._assertOwner(VrpcAdapter._instances.get(instance), json, instance)
875
+ const entry = VrpcAdapter._instances.get(instance)
876
+ // a delete names the instance's own class: the class info of that
877
+ // class is what announces it gone
878
+ if (entry && entry.className !== json.c && json.s !== VrpcAdapter.LOCAL_SENDER) {
879
+ throw new Error(`Instance '${instance}' is not of class '${json.c}'`)
880
+ }
881
+ VrpcAdapter._assertOwner(entry, json, instance)
587
882
  VrpcAdapter._emitter.emit('beforeDelete', {
588
883
  instance,
589
884
  className: json.c
@@ -614,9 +909,16 @@ class VrpcAdapter {
614
909
  json.e = { message, cause }
615
910
  return
616
911
  }
617
- // special case: removeAllListeners
912
+ // special case: removeAllListeners withdraws the caller's subscriptions
913
+ // of that event - the emitter itself is never touched, it holds other
914
+ // clients' and its own listeners
618
915
  if (json.f === 'removeAllListeners') {
619
- VrpcAdapter._removeAllListeners(json.a[0], json.s, json.c)
916
+ const isStatic = VrpcAdapter._isStaticContext(json.c)
917
+ VrpcAdapter._registrations.withdrawEvent(
918
+ VrpcAdapter._contextKey(json.c, isStatic),
919
+ json.a[0],
920
+ json.s
921
+ )
620
922
  json.r = true
621
923
  return
622
924
  }
@@ -629,6 +931,49 @@ class VrpcAdapter {
629
931
  ) {
630
932
  throw new Error(`Could not find context: ${json.c}`)
631
933
  }
934
+ // Check whether context is a registered class (function is static)
935
+ const classEntry = VrpcAdapter._functionRegistry.get(json.c)
936
+ const isStatic = classEntry !== undefined
937
+ let target
938
+ let className = json.c
939
+ if (isStatic) {
940
+ target = classEntry.Klass
941
+ } else {
942
+ const entry = VrpcAdapter._instances.get(json.c)
943
+ if (entry === undefined) {
944
+ throw new Error(`Could not find context: ${json.c}`)
945
+ }
946
+ target = entry.instance
947
+ className = entry.className
948
+ }
949
+ const carriesIds = Registrations.carriesEventIds(json.a)
950
+ // only what the class info lists is called; a remover carrying ids never
951
+ // reaches the class (see Registrations)
952
+ if (
953
+ !(carriesIds && Registrations.isRemover(json.f, json.v)) &&
954
+ !VrpcAdapter._isPublished(className, json.f, isStatic)
955
+ ) {
956
+ throw new Error(`Could not find function: ${json.f}`)
957
+ }
958
+ // a call carrying subscription ids subscribes (or withdraws)
959
+ if (carriesIds) {
960
+ try {
961
+ const { value } = VrpcAdapter._subscribe(json, {
962
+ contextKey: VrpcAdapter._contextKey(json.c, isStatic),
963
+ contextName: json.c,
964
+ target,
965
+ func: json.f,
966
+ args: json.a
967
+ })
968
+ if (VrpcAdapter._isPromise(value)) {
969
+ this._handlePromise(json, value)
970
+ } else json.r = value
971
+ } catch (err) {
972
+ const { message, cause } = err
973
+ json.e = { message, cause }
974
+ }
975
+ return
976
+ }
632
977
  const unwrapped = VrpcAdapter._unwrapArguments(json)
633
978
  // special case: removing a listener the adapter does not hold (the
634
979
  // client's subscription outlived a restart of this agent, or it sent
@@ -641,47 +986,23 @@ class VrpcAdapter {
641
986
  json.r = true
642
987
  return
643
988
  }
644
- // Check whether context is a registered class
645
- const entry = VrpcAdapter._functionRegistry.get(json.c)
646
- if (entry !== undefined) {
647
- // entry is class -> function is static
648
- const { Klass } = entry
649
- // TODO Think about whether to do live checking (like here) or
650
- // rather sticking to those functions registered before...
651
- // TODO This is even more important now as the agent does a wildcard
652
- // subscription against all functions
653
- if (VrpcAdapter._isFunction(Klass[json.f])) {
654
- try {
655
- const ret = Klass[json.f].apply(null, unwrapped)
656
- // check if function returns promise
657
- if (VrpcAdapter._isPromise(ret)) {
658
- this._handlePromise(json, ret)
659
- } else json.r = ret
660
- } catch (err) {
661
- const { message, cause } = err
662
- json.e = { message, cause }
663
- }
664
- } else throw new Error(`Could not find function: ${json.f}`)
665
- } else {
666
- // is not static
667
- const entry = VrpcAdapter._instances.get(json.c)
668
- if (entry === undefined) {
669
- throw new Error(`Could not find context: ${json.c}`)
670
- }
671
- const { instance } = entry
672
- if (VrpcAdapter._isFunction(instance[json.f])) {
673
- try {
674
- let ret = instance[json.f].apply(instance, unwrapped)
675
- ret = VrpcAdapter.sanitizeEventListenerReturnValues(ret)
676
- // check if function returns promise
677
- if (VrpcAdapter._isPromise(ret)) {
678
- this._handlePromise(json, ret)
679
- } else json.r = ret
680
- } catch (err) {
681
- const { message, cause } = err
682
- json.e = { message, cause }
683
- }
684
- } else throw new Error(`Could not find function: ${json.f}`)
989
+ // TODO Think about whether to do live checking (like here) or
990
+ // rather sticking to those functions registered before...
991
+ if (!VrpcAdapter._isFunction(target[json.f])) {
992
+ throw new Error(`Could not find function: ${json.f}`)
993
+ }
994
+ try {
995
+ let ret = isStatic
996
+ ? target[json.f].apply(null, unwrapped)
997
+ : target[json.f].apply(target, unwrapped)
998
+ if (!isStatic) ret = VrpcAdapter.sanitizeEventListenerReturnValues(ret)
999
+ // check if function returns promise
1000
+ if (VrpcAdapter._isPromise(ret)) {
1001
+ this._handlePromise(json, ret)
1002
+ } else json.r = ret
1003
+ } catch (err) {
1004
+ const { message, cause } = err
1005
+ json.e = { message, cause }
685
1006
  }
686
1007
  }
687
1008
 
@@ -707,10 +1028,13 @@ class VrpcAdapter {
707
1028
  * clock or holds a connection lives on as long as anything reaches it;
708
1029
  * ending it takes three steps:
709
1030
  * 1. its name is free at once: no call reaches the object any more;
710
- * 2. the adapter takes back every listener it attached (its listeners are
711
- * silent from now on anyway, see _generateListener);
1031
+ * 2. the adapter forgets the instance's registrations (their listeners
1032
+ * are silent from now on, see Registrations); it releases them itself
1033
+ * only for an object without a dispose method (legacy), and always
1034
+ * takes back what it attached through Node's own `on`;
712
1035
  * 3. the object releases what it runs through the ECMAScript dispose
713
1036
  * protocol, `[Symbol.asyncDispose]` or else `[Symbol.dispose]`.
1037
+ * Its subscribers learn the loss from the class info (instance gone).
714
1038
  *
715
1039
  * @returns {Object} `{ ended, disposed }`: whether the name was known, and
716
1040
  * a promise when an asynchronous dispose is under way (it never rejects)
@@ -719,33 +1043,16 @@ class VrpcAdapter {
719
1043
  const entry = VrpcAdapter._instances.get(instanceId)
720
1044
  if (!entry) return { ended: false, disposed: null }
721
1045
  VrpcAdapter._instances.delete(instanceId)
722
- VrpcAdapter._detachListeners(instanceId, entry.instance)
1046
+ const { instance } = entry
1047
+ VrpcAdapter._registrations.forgetContext(instanceId, {
1048
+ release: !(
1049
+ VrpcAdapter._isFunction(instance[Symbol.asyncDispose]) ||
1050
+ VrpcAdapter._isFunction(instance[Symbol.dispose])
1051
+ )
1052
+ })
723
1053
  return { ended: true, disposed: VrpcAdapter._dispose(instanceId, entry) }
724
1054
  }
725
1055
 
726
- static _detachListeners (instanceId, instance) {
727
- const registrations = VrpcAdapter._listeners[instanceId]
728
- delete VrpcAdapter._listeners[instanceId]
729
- if (!registrations || !VrpcAdapter._isFunction(instance.removeListener)) {
730
- return
731
- }
732
- for (const { event, listener, clients } of Object.values(registrations)) {
733
- try {
734
- VrpcAdapter._removeTimes(instance, event, listener, clients.length)
735
- } catch (err) {
736
- // a class whose removeListener fails keeps a silent listener
737
- }
738
- }
739
- }
740
-
741
- // a subscription repeated n times may hold n attachments of one listener
742
- // (EventEmitter semantics): each removeListener takes one of them
743
- static _removeTimes (instance, event, listener, times) {
744
- for (let n = 0; n < Math.max(1, times); n++) {
745
- instance.removeListener(event, listener)
746
- }
747
- }
748
-
749
1056
  /**
750
1057
  * Runs the object's dispose method. A dispose that fails or outlasts
751
1058
  * VrpcAdapter.DISPOSE_TIMEOUT is reported (event 'disposeFailed') and
@@ -790,25 +1097,12 @@ class VrpcAdapter {
790
1097
  return Promise.race([settled, bound]).then(() => clearTimeout(timer))
791
1098
  }
792
1099
 
1100
+ /**
1101
+ * A client connection went: every subscription it held is withdrawn, and
1102
+ * a registration whose last subscription that was is released.
1103
+ */
793
1104
  static _unregisterClient (clientId) {
794
- Object.entries(VrpcAdapter._listeners).forEach(([ik, iv]) => {
795
- Object.entries(iv).forEach(([ek, ev]) => {
796
- const { listener, event } = ev
797
- // a client is listed once per subscription: its going ends them all
798
- const remaining = ev.clients.filter(x => x !== clientId)
799
- const gone = ev.clients.length - remaining.length
800
- if (gone === 0) return
801
- ev.clients = remaining
802
- if (remaining.length === 0) {
803
- const entry = VrpcAdapter._instances.get(ik)
804
- const instance = entry && entry.instance
805
- if (instance && instance.removeListener) {
806
- VrpcAdapter._removeTimes(instance, event, listener, gone)
807
- }
808
- delete VrpcAdapter._listeners[ik][ek]
809
- }
810
- })
811
- })
1105
+ VrpcAdapter._registrations.withdrawClient(clientId)
812
1106
  }
813
1107
 
814
1108
  static _validate (schema, params) {
@@ -819,6 +1113,7 @@ class VrpcAdapter {
819
1113
  if (!valid) throw new Error(VrpcAdapter._ajv.errorsText())
820
1114
  }
821
1115
 
1116
+ // the arguments of a plain call: `__f__` ids become one-shot callbacks
822
1117
  static _unwrapArguments (json, instanceId) {
823
1118
  const clientId = json.s
824
1119
  // if we came from a "callAll" we will have an instanceId defined
@@ -829,53 +1124,17 @@ class VrpcAdapter {
829
1124
  const func = isCallAll ? json.a[0] : json.f
830
1125
  const unwrapped = isCallAll ? [func] : []
831
1126
  for (const arg of args) {
832
- if (typeof arg === 'string') {
833
- if (arg.startsWith('__f__')) {
834
- // function callback
835
- unwrapped.push(
836
- this._generateCallback({
837
- clientId,
838
- isCallAll,
839
- instanceId: context,
840
- eventId: arg
841
- })
842
- )
843
- } else if (arg.startsWith('__e__')) {
844
- // event registration
845
- if (func === 'on' || func === 'addListener') {
846
- this._mustTrackClient = true
847
- const listener = VrpcAdapter._registerListener({
848
- clientId,
849
- isCallAll,
850
- instanceId: context,
851
- eventId: arg,
852
- event: args[0]
853
- })
854
- unwrapped.push(listener)
855
- // event un-registration
856
- } else if (func === 'off' || func === 'removeListener') {
857
- const listener = VrpcAdapter._unregisterListener({
858
- clientId,
859
- instanceId: context,
860
- eventId: arg
861
- })
862
- unwrapped.push(listener)
863
- } else {
864
- this._mustTrackClient = true
865
- const listener = VrpcAdapter._registerListener({
866
- clientId,
867
- isCallAll,
868
- instanceId: context,
869
- eventId: arg,
870
- event: null
871
- })
872
- unwrapped.push(listener)
873
- }
874
- } else {
875
- unwrapped.push(arg)
876
- }
1127
+ if (typeof arg === 'string' && arg.startsWith('__f__')) {
1128
+ unwrapped.push(
1129
+ this._generateCallback({
1130
+ clientId,
1131
+ isCallAll,
1132
+ instanceId: context,
1133
+ eventId: arg,
1134
+ receiverVersion: json.v
1135
+ })
1136
+ )
877
1137
  } else {
878
- // regular argument
879
1138
  unwrapped.push(arg)
880
1139
  }
881
1140
  }
@@ -899,112 +1158,40 @@ class VrpcAdapter {
899
1158
  }
900
1159
  }
901
1160
 
902
- static _generateCallback ({ eventId, clientId, instanceId, isCallAll }) {
903
- const speaks = VrpcAdapter._speaksFor(instanceId)
904
- return (...innerArgs) => {
905
- if (!speaks()) return
906
- // "callAll" event notifications expect the instanceId as first argument
907
- const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
908
- VrpcAdapter._callback({ a, s: clientId, i: eventId })
909
- }
910
- }
911
-
912
- static _registerListener ({
1161
+ static _generateCallback ({
913
1162
  eventId,
914
1163
  clientId,
915
1164
  instanceId,
916
- event,
917
- isCallAll
1165
+ isCallAll,
1166
+ receiverVersion
918
1167
  }) {
919
- if (!VrpcAdapter._listeners[instanceId]) {
920
- VrpcAdapter._listeners[instanceId] = {
921
- [eventId]: {
922
- event,
923
- clients: [clientId],
924
- listener: VrpcAdapter._generateListener({
925
- eventId,
926
- instanceId,
927
- isCallAll
928
- })
929
- }
930
- }
931
- return VrpcAdapter._listeners[instanceId][eventId].listener
932
- }
933
- if (!VrpcAdapter._listeners[instanceId][eventId]) {
934
- VrpcAdapter._listeners[instanceId][eventId] = {
935
- event,
936
- clients: [clientId],
937
- listener: VrpcAdapter._generateListener({
938
- eventId,
939
- instanceId,
940
- isCallAll
941
- })
942
- }
943
- return VrpcAdapter._listeners[instanceId][eventId].listener
944
- }
945
- VrpcAdapter._listeners[instanceId][eventId].clients.push(clientId)
946
- return VrpcAdapter._listeners[instanceId][eventId].listener
947
- }
948
-
949
- static _generateListener ({ eventId, instanceId, isCallAll }) {
950
1168
  const speaks = VrpcAdapter._speaksFor(instanceId)
951
1169
  return (...innerArgs) => {
952
1170
  if (!speaks()) return
1171
+ // "callAll" event notifications expect the instanceId as first argument
953
1172
  const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
954
- VrpcAdapter._callback({ a, i: eventId })
955
- }
956
- }
957
-
958
- static _unregisterListener ({ eventId, clientId, instanceId }) {
959
- if (
960
- VrpcAdapter._listeners[instanceId] &&
961
- VrpcAdapter._listeners[instanceId][eventId]
962
- ) {
963
- const { listener, clients } = VrpcAdapter._listeners[instanceId][eventId]
964
- const index = clients.indexOf(clientId)
965
- if (index !== -1) clients.splice(index, 1)
966
- if (clients.length === 0) {
967
- delete VrpcAdapter._listeners[instanceId][eventId]
968
- if (Object.keys(VrpcAdapter._listeners[instanceId]).length === 0) {
969
- delete VrpcAdapter._listeners[instanceId]
970
- }
971
- }
972
- return listener
1173
+ // the receiver's protocol version: whether it can be told of a
1174
+ // callback that cannot travel (CALL-10)
1175
+ VrpcAdapter._callback({ a, s: clientId, i: eventId, receiverVersion })
973
1176
  }
974
1177
  }
975
1178
 
976
- static _removeAllListeners (event, clientId, instanceId) {
977
- const eventIds = VrpcAdapter._listeners[instanceId]
978
- if (!eventIds) return
979
- Object.entries(eventIds).forEach(([ek, ev]) => {
980
- if (ev.event === event && ev.clients.includes(clientId)) {
981
- ev.clients = ev.clients.filter(x => x !== clientId)
982
- VrpcAdapter.getInstance(instanceId).removeAllListeners(ev.event)
983
- if (ev.clients.length === 0) {
984
- delete VrpcAdapter._listeners[instanceId][ek]
985
- }
986
- }
987
- })
988
- }
989
-
1179
+ /**
1180
+ * Answers a promise twice: at once with `r: '__p__<f>-<nonce>-<n>'`, and
1181
+ * once it settled with a second answer under that id, `{ s, i, r }` or
1182
+ * `{ s, i, e }`. A client waits for the second answer under the bare id,
1183
+ * whichever agent sends it: the nonce keeps the ids of two agents apart,
1184
+ * whose counters may well stand at the same value.
1185
+ */
990
1186
  static _handlePromise (json, promise) {
991
- try {
992
- const cid = VrpcAdapter._correlationId++ % Number.MAX_SAFE_INTEGER
993
- const id = `__p__${json.f}-${cid}`
994
- json.r = id
995
- promise
996
- .then(r => VrpcAdapter._callback({ ...json, r, i: id }))
997
- .catch(err =>
998
- VrpcAdapter._callback({
999
- ...json,
1000
- e: { message: err.message, cause: err.cause },
1001
- i: id
1002
- })
1003
- )
1004
- } catch (err) {
1005
- const { message, cause } = err
1006
- json.data.e = { message, cause }
1007
- }
1187
+ const cid = VrpcAdapter._correlationId++ % Number.MAX_SAFE_INTEGER
1188
+ const id = `__p__${json.f}-${VrpcAdapter._promiseNonce}-${cid}`
1189
+ const { s } = json
1190
+ json.r = id
1191
+ promise.then(
1192
+ r => VrpcAdapter._callback({ s, i: id, r }),
1193
+ err => VrpcAdapter._callback({ s, i: id, e: VrpcAdapter._error(err) })
1194
+ )
1008
1195
  }
1009
1196
 
1010
1197
  static _getClassEntry (className) {
@@ -1040,6 +1227,18 @@ class VrpcAdapter {
1040
1227
  static _extractMemberFunctions (klass) {
1041
1228
  let klass_ = klass
1042
1229
  let fs = []
1230
+ // Node's EventEmitter offers subscribing, withdrawing and once to
1231
+ // clients; emit (anyone could fake an event to every subscriber, or emit
1232
+ // 'error' and end the agent) and its internals stay inside the class
1233
+ const isEmitter =
1234
+ klass instanceof EventEmitter ||
1235
+ (!!klass.prototype && klass.prototype instanceof EventEmitter)
1236
+ const published = (names, level) => {
1237
+ if (level === EventEmitter.prototype) {
1238
+ return names.filter(x => VrpcAdapter._emitterPublic.has(x))
1239
+ }
1240
+ return isEmitter ? names.filter(x => x !== 'emit') : names
1241
+ }
1043
1242
  do {
1044
1243
  if (klass_.prototype) {
1045
1244
  const funcs = Object.getOwnPropertyNames(klass_.prototype).filter(
@@ -1047,12 +1246,12 @@ class VrpcAdapter {
1047
1246
  !VrpcAdapter._blackList.has(x) &&
1048
1247
  this._isFunction(klass_.prototype[x])
1049
1248
  )
1050
- fs = [...fs, ...funcs]
1249
+ fs = [...fs, ...published(funcs, klass_.prototype)]
1051
1250
  } else {
1052
1251
  const funcs = Object.getOwnPropertyNames(klass_).filter(
1053
1252
  x => !VrpcAdapter._blackList.has(x) && this._isFunction(klass_[x])
1054
1253
  )
1055
- fs = [...fs, ...funcs]
1254
+ fs = [...fs, ...published(funcs, klass_)]
1056
1255
  }
1057
1256
  klass_ = Object.getPrototypeOf(klass_)
1058
1257
  } while (klass_)
@@ -1064,13 +1263,16 @@ class VrpcAdapter {
1064
1263
  let klass_ = klass
1065
1264
  let fs = []
1066
1265
  do {
1067
- fs = [
1068
- ...fs,
1069
- ...Object.getOwnPropertyNames(klass_).filter(prop => {
1070
- const desc = Object.getOwnPropertyDescriptor(klass_, prop)
1071
- return !!desc && typeof desc.value === 'function'
1072
- })
1073
- ]
1266
+ // EventEmitter's own statics are Node helpers, not the class's API
1267
+ if (klass_ !== EventEmitter) {
1268
+ fs = [
1269
+ ...fs,
1270
+ ...Object.getOwnPropertyNames(klass_).filter(prop => {
1271
+ const desc = Object.getOwnPropertyDescriptor(klass_, prop)
1272
+ return !!desc && typeof desc.value === 'function'
1273
+ })
1274
+ ]
1275
+ }
1074
1276
  klass_ = Object.getPrototypeOf(klass_)
1075
1277
  } while (klass_ && klass_.name)
1076
1278
  // Filter out all duplicates
@@ -1121,11 +1323,23 @@ class VrpcAdapter {
1121
1323
  */
1122
1324
 
1123
1325
  /**
1124
- * Event 'delete'
1326
+ * Event 'beforeDelete'
1125
1327
  *
1126
- * Emitted on deletion of shared instance
1328
+ * Emitted when a deletion through the agent begins, while the instance can
1329
+ * still be reached
1127
1330
  *
1128
1331
  * @event VrpcAdapter#beforeDelete
1332
+ * @type {Object}
1333
+ * @property {String} className The class name of the instance
1334
+ * @property {String} instance The instance name
1335
+ */
1336
+
1337
+ /**
1338
+ * Event 'delete'
1339
+ *
1340
+ * Emitted once an instance was deleted through the agent (a client's delete,
1341
+ * or an isolated instance whose owner went)
1342
+ *
1129
1343
  * @event VrpcAdapter#delete
1130
1344
  * @type {Object}
1131
1345
  * @property {String} className The class name of the deleted instance
@@ -1146,15 +1360,61 @@ class VrpcAdapter {
1146
1360
  * @property {Error} error What went wrong
1147
1361
  */
1148
1362
 
1363
+ /**
1364
+ * Event 'releaseFailed'
1365
+ *
1366
+ * Emitted when the release of an event registration threw or rejected; the
1367
+ * registration is gone anyway
1368
+ *
1369
+ * @event VrpcAdapter#releaseFailed
1370
+ * @type {Object}
1371
+ * @property {String} context The instance (or class) of the registration
1372
+ * @property {String} function The event function
1373
+ * @property {Array.<Any>} args Its arguments, without handlers
1374
+ * @property {Error} error What went wrong
1375
+ */
1376
+
1377
+ /**
1378
+ * Event 'greetFailed'
1379
+ *
1380
+ * Emitted when a registration's greeting threw or rejected; the subscriber
1381
+ * stays subscribed
1382
+ *
1383
+ * @event VrpcAdapter#greetFailed
1384
+ * @type {Object}
1385
+ * @property {String} context The instance (or class) of the registration
1386
+ * @property {String} function The event function
1387
+ * @property {Array.<Any>} args Its arguments, without handlers
1388
+ * @property {Error} error What went wrong
1389
+ */
1390
+
1149
1391
  // Initialize static members
1150
1392
  VrpcAdapter._functionRegistry = new Map()
1151
1393
  VrpcAdapter._instances = new Map()
1152
1394
  VrpcAdapter._emitter = new EventEmitter()
1153
1395
  VrpcAdapter._correlationId = 0
1154
- VrpcAdapter._listeners = {}
1396
+ // tells this process's promise ids from those of every other agent
1397
+ VrpcAdapter._promiseNonce = nanoid(8)
1398
+ // the event registrations of every context and their subscriptions
1399
+ VrpcAdapter._registrations = new Registrations({
1400
+ publish: message => VrpcAdapter._callback(message),
1401
+ emit: (event, payload) => VrpcAdapter._emitter.emit(event, payload)
1402
+ })
1155
1403
  // the longest a delete waits for the dispose method of its instance
1156
1404
  VrpcAdapter.DISPOSE_TIMEOUT = 10000
1405
+ // the EventEmitter functions published to clients
1406
+ VrpcAdapter._emitterPublic = new Set([
1407
+ 'on',
1408
+ 'addListener',
1409
+ 'off',
1410
+ 'removeListener',
1411
+ 'once',
1412
+ 'removeAllListeners'
1413
+ ])
1157
1414
  VrpcAdapter._blackList = new Set([
1415
+ // a class's constructor is reached through __createShared__ and
1416
+ // __createIsolated__, never as a member function
1417
+ 'constructor',
1158
1418
  'caller',
1159
1419
  'callee',
1160
1420
  'arguments',
@@ -1177,6 +1437,8 @@ VrpcAdapter._blackList = new Set([
1177
1437
  * The sender the agent uses for its own housekeeping calls (deleting the
1178
1438
  * isolated instances of a departed connection). A Symbol: no message from
1179
1439
  * the wire can carry it, so it can never be claimed remotely.
1440
+ *
1441
+ * @private
1180
1442
  */
1181
1443
  VrpcAdapter.LOCAL_SENDER = Symbol('vrpc.local-sender')
1182
1444