vrpc 3.12.1 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/browser/vrpc.js +1 -1
- package/package.json +3 -2
- package/vrpc/VrpcAdapter.js +169 -24
- package/vrpc/VrpcAgent.js +12 -0
- package/vrpc/VrpcBrowserAdapter.js +174 -28
- package/vrpc/VrpcBrowserAgent.js +12 -0
- package/vrpc/VrpcClient.js +21 -1
- package/vrpc/VrpcPersistor.js +15 -2
- package/vrpc/comment-parser/parser.js +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vrpc",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.0",
|
|
4
4
|
"description": "Variadic Remote Procedure Calls",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"directories": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"build:addon": "ln -sf binding.gyp.no-auto binding.gyp && node-gyp rebuild && rm binding.gyp",
|
|
13
13
|
"build:browser": "./node_modules/.bin/webpack --stats-error-details",
|
|
14
14
|
"build:doc": "./node_modules/.bin/jsdoc2md -f vrpc/VrpcAdapter.js vrpc/VrpcAgent.js vrpc/VrpcClient.js vrpc/VrpcNative.js --param-list-format=list --separators > docs/api.md",
|
|
15
|
-
"test": "npm run test:adapter && npm run test:agent && npm run test:client && npm run test:persistor && npm run test:lifecycle && npm run test:production",
|
|
15
|
+
"test": "npm run test:adapter && npm run test:agent && npm run test:client && npm run test:persistor && npm run test:lifecycle && npm run test:legacy-runtime && npm run test:production",
|
|
16
16
|
"test:adapter": "./node_modules/.bin/mocha tests/adapter/*.js --exit",
|
|
17
17
|
"test:native": "./node_modules/.bin/mocha tests/native/*.js --exit",
|
|
18
18
|
"test:agent": "tests/agent/test.sh",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"test:performance": "tests/performance/test.sh",
|
|
21
21
|
"test:persistor": "./node_modules/.bin/mocha tests/persistor/*.js --timeout 30000 --exit",
|
|
22
22
|
"test:lifecycle": "./node_modules/.bin/mocha tests/lifecycle/*.js --timeout 30000 --exit",
|
|
23
|
+
"test:legacy-runtime": "tests/legacy-runtime/test.sh",
|
|
23
24
|
"test:production": "./node_modules/.bin/mocha tests/production/lifeCycleTest.js --timeout 30000 --exit && tests/production/test.sh"
|
|
24
25
|
},
|
|
25
26
|
"bin": {
|
package/vrpc/VrpcAdapter.js
CHANGED
|
@@ -43,6 +43,17 @@ const commentParser = require('./comment-parser')
|
|
|
43
43
|
const EventEmitter = require('events')
|
|
44
44
|
const { nanoid } = require('nanoid')
|
|
45
45
|
|
|
46
|
+
// Node before 18.18 / 20.4 has no dispose symbols, and a class's
|
|
47
|
+
// [Symbol.asyncDispose]() would become a method named 'undefined'. Node's
|
|
48
|
+
// own stand-ins live in the symbol registry, so a class file and the adapter
|
|
49
|
+
// meet on one symbol whichever of them installs it first.
|
|
50
|
+
if (typeof Symbol.dispose !== 'symbol') {
|
|
51
|
+
Symbol.dispose = Symbol.for('nodejs.dispose')
|
|
52
|
+
}
|
|
53
|
+
if (typeof Symbol.asyncDispose !== 'symbol') {
|
|
54
|
+
Symbol.asyncDispose = Symbol.for('nodejs.asyncDispose')
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
/**
|
|
47
58
|
* Generates an adapter layer for existing code and enables further VRPC-based
|
|
48
59
|
* communication.
|
|
@@ -197,7 +208,10 @@ class VrpcAdapter {
|
|
|
197
208
|
}
|
|
198
209
|
|
|
199
210
|
/**
|
|
200
|
-
* Deletes an instance
|
|
211
|
+
* Deletes an instance: its name is free at once, the adapter takes back
|
|
212
|
+
* every listener it attached, and the object releases what it runs
|
|
213
|
+
* through its `[Symbol.asyncDispose]` or `[Symbol.dispose]` method, which
|
|
214
|
+
* runs in the background here (a remote delete answers once it settled).
|
|
201
215
|
*
|
|
202
216
|
* @param {String|Object} instance Instance (name or object itself) to be deleted
|
|
203
217
|
* @returns {Boolean} True in case of success, false otherwise
|
|
@@ -208,10 +222,7 @@ class VrpcAdapter {
|
|
|
208
222
|
}
|
|
209
223
|
if (typeof instance === 'object') {
|
|
210
224
|
for (const [k, v] of VrpcAdapter._instances) {
|
|
211
|
-
if (v.instance === instance)
|
|
212
|
-
VrpcAdapter._instances.delete(k)
|
|
213
|
-
return true
|
|
214
|
-
}
|
|
225
|
+
if (v.instance === instance) return VrpcAdapter._delete(k)
|
|
215
226
|
}
|
|
216
227
|
return false
|
|
217
228
|
}
|
|
@@ -577,11 +588,17 @@ class VrpcAdapter {
|
|
|
577
588
|
instance,
|
|
578
589
|
className: json.c
|
|
579
590
|
})
|
|
580
|
-
|
|
591
|
+
const { ended, disposed } = VrpcAdapter._end(instance)
|
|
581
592
|
VrpcAdapter._emitter.emit('delete', {
|
|
582
593
|
instance,
|
|
583
594
|
className: json.c
|
|
584
595
|
})
|
|
596
|
+
// a delete answers once the object released what it runs, so that a
|
|
597
|
+
// constructor right after it never meets a port or a session the old
|
|
598
|
+
// object still holds; the agent's own housekeeping awaits no answer
|
|
599
|
+
if (disposed && json.s !== VrpcAdapter.LOCAL_SENDER) {
|
|
600
|
+
VrpcAdapter._handlePromise(json, disposed.then(() => ended))
|
|
601
|
+
} else json.r = ended
|
|
585
602
|
} catch (err) {
|
|
586
603
|
const { message, cause } = err
|
|
587
604
|
json.e = { message, cause }
|
|
@@ -603,6 +620,15 @@ class VrpcAdapter {
|
|
|
603
620
|
json.r = true
|
|
604
621
|
return
|
|
605
622
|
}
|
|
623
|
+
// an unknown context fails before its arguments are unwrapped: a
|
|
624
|
+
// listener registered for a name that does not exist would outlive the
|
|
625
|
+
// failed call and pass for attached once the name is created
|
|
626
|
+
if (
|
|
627
|
+
!VrpcAdapter._functionRegistry.has(json.c) &&
|
|
628
|
+
!VrpcAdapter._instances.has(json.c)
|
|
629
|
+
) {
|
|
630
|
+
throw new Error(`Could not find context: ${json.c}`)
|
|
631
|
+
}
|
|
606
632
|
const unwrapped = VrpcAdapter._unwrapArguments(json)
|
|
607
633
|
// special case: removing a listener the adapter does not hold (the
|
|
608
634
|
// client's subscription outlived a restart of this agent, or it sent
|
|
@@ -671,33 +697,115 @@ class VrpcAdapter {
|
|
|
671
697
|
}
|
|
672
698
|
|
|
673
699
|
static _delete (instanceId) {
|
|
700
|
+
return VrpcAdapter._end(instanceId).ended
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Ends an instance - the one teardown behind every door (a remote delete,
|
|
705
|
+
* the auto-delete of an isolated instance whose client went, an
|
|
706
|
+
* in-process delete). Node has no destructor, so an object that runs a
|
|
707
|
+
* clock or holds a connection lives on as long as anything reaches it;
|
|
708
|
+
* ending it takes three steps:
|
|
709
|
+
* 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);
|
|
712
|
+
* 3. the object releases what it runs through the ECMAScript dispose
|
|
713
|
+
* protocol, `[Symbol.asyncDispose]` or else `[Symbol.dispose]`.
|
|
714
|
+
*
|
|
715
|
+
* @returns {Object} `{ ended, disposed }`: whether the name was known, and
|
|
716
|
+
* a promise when an asynchronous dispose is under way (it never rejects)
|
|
717
|
+
*/
|
|
718
|
+
static _end (instanceId) {
|
|
674
719
|
const entry = VrpcAdapter._instances.get(instanceId)
|
|
675
|
-
if (entry) {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
720
|
+
if (!entry) return { ended: false, disposed: null }
|
|
721
|
+
VrpcAdapter._instances.delete(instanceId)
|
|
722
|
+
VrpcAdapter._detachListeners(instanceId, entry.instance)
|
|
723
|
+
return { ended: true, disposed: VrpcAdapter._dispose(instanceId, entry) }
|
|
724
|
+
}
|
|
725
|
+
|
|
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
|
|
679
737
|
}
|
|
680
|
-
delete VrpcAdapter._listeners[instanceId]
|
|
681
|
-
VrpcAdapter._instances.delete(instanceId)
|
|
682
|
-
return true
|
|
683
738
|
}
|
|
684
|
-
|
|
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
|
+
/**
|
|
750
|
+
* Runs the object's dispose method. A dispose that fails or outlasts
|
|
751
|
+
* VrpcAdapter.DISPOSE_TIMEOUT is reported (event 'disposeFailed') and
|
|
752
|
+
* never thrown: the instance is gone either way.
|
|
753
|
+
*
|
|
754
|
+
* @returns {Promise|null} Settles when an asynchronous dispose is done
|
|
755
|
+
*/
|
|
756
|
+
static _dispose (instanceId, { instance, className }) {
|
|
757
|
+
const fail = error =>
|
|
758
|
+
VrpcAdapter._emitter.emit('disposeFailed', {
|
|
759
|
+
instance: instanceId,
|
|
760
|
+
className,
|
|
761
|
+
error
|
|
762
|
+
})
|
|
763
|
+
let result
|
|
764
|
+
try {
|
|
765
|
+
if (VrpcAdapter._isFunction(instance[Symbol.asyncDispose])) {
|
|
766
|
+
result = instance[Symbol.asyncDispose]()
|
|
767
|
+
} else if (VrpcAdapter._isFunction(instance[Symbol.dispose])) {
|
|
768
|
+
result = instance[Symbol.dispose]()
|
|
769
|
+
}
|
|
770
|
+
} catch (err) {
|
|
771
|
+
fail(err)
|
|
772
|
+
return null
|
|
773
|
+
}
|
|
774
|
+
if (!VrpcAdapter._isPromise(result)) return null
|
|
775
|
+
let timer
|
|
776
|
+
const bound = new Promise(resolve => {
|
|
777
|
+
timer = setTimeout(() => {
|
|
778
|
+
fail(
|
|
779
|
+
new Error(
|
|
780
|
+
`dispose did not settle within ${VrpcAdapter.DISPOSE_TIMEOUT} ms`
|
|
781
|
+
)
|
|
782
|
+
)
|
|
783
|
+
resolve()
|
|
784
|
+
}, VrpcAdapter.DISPOSE_TIMEOUT)
|
|
785
|
+
})
|
|
786
|
+
const settled = result.then(
|
|
787
|
+
() => {},
|
|
788
|
+
err => fail(err)
|
|
789
|
+
)
|
|
790
|
+
return Promise.race([settled, bound]).then(() => clearTimeout(timer))
|
|
685
791
|
}
|
|
686
792
|
|
|
687
793
|
static _unregisterClient (clientId) {
|
|
688
794
|
Object.entries(VrpcAdapter._listeners).forEach(([ik, iv]) => {
|
|
689
795
|
Object.entries(iv).forEach(([ek, ev]) => {
|
|
690
|
-
const {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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)
|
|
700
807
|
}
|
|
808
|
+
delete VrpcAdapter._listeners[ik][ek]
|
|
701
809
|
}
|
|
702
810
|
})
|
|
703
811
|
})
|
|
@@ -774,8 +882,27 @@ class VrpcAdapter {
|
|
|
774
882
|
return unwrapped
|
|
775
883
|
}
|
|
776
884
|
|
|
885
|
+
/**
|
|
886
|
+
* Whether the object under `instanceId` is still the one the adapter saw
|
|
887
|
+
* when it built a listener or a callback for it: a deleted instance never
|
|
888
|
+
* speaks on the bus again (#1705), even when its class kept the listener
|
|
889
|
+
* or its clock kept running. A static context (a class name) has no
|
|
890
|
+
* object and always speaks.
|
|
891
|
+
*/
|
|
892
|
+
static _speaksFor (instanceId) {
|
|
893
|
+
const entry = VrpcAdapter._instances.get(instanceId)
|
|
894
|
+
if (!entry) return () => true
|
|
895
|
+
const { instance } = entry
|
|
896
|
+
return () => {
|
|
897
|
+
const live = VrpcAdapter._instances.get(instanceId)
|
|
898
|
+
return live !== undefined && live.instance === instance
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
777
902
|
static _generateCallback ({ eventId, clientId, instanceId, isCallAll }) {
|
|
903
|
+
const speaks = VrpcAdapter._speaksFor(instanceId)
|
|
778
904
|
return (...innerArgs) => {
|
|
905
|
+
if (!speaks()) return
|
|
779
906
|
// "callAll" event notifications expect the instanceId as first argument
|
|
780
907
|
const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
|
|
781
908
|
VrpcAdapter._callback({ a, s: clientId, i: eventId })
|
|
@@ -820,7 +947,9 @@ class VrpcAdapter {
|
|
|
820
947
|
}
|
|
821
948
|
|
|
822
949
|
static _generateListener ({ eventId, instanceId, isCallAll }) {
|
|
950
|
+
const speaks = VrpcAdapter._speaksFor(instanceId)
|
|
823
951
|
return (...innerArgs) => {
|
|
952
|
+
if (!speaks()) return
|
|
824
953
|
const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
|
|
825
954
|
VrpcAdapter._callback({ a, i: eventId })
|
|
826
955
|
}
|
|
@@ -1003,12 +1132,28 @@ class VrpcAdapter {
|
|
|
1003
1132
|
* @property {String} instance The instance name
|
|
1004
1133
|
*/
|
|
1005
1134
|
|
|
1135
|
+
/**
|
|
1136
|
+
* Event 'disposeFailed'
|
|
1137
|
+
*
|
|
1138
|
+
* Emitted when the dispose method of a deleted instance throws, rejects or
|
|
1139
|
+
* does not settle within VrpcAdapter.DISPOSE_TIMEOUT. The instance is gone
|
|
1140
|
+
* regardless.
|
|
1141
|
+
*
|
|
1142
|
+
* @event VrpcAdapter#disposeFailed
|
|
1143
|
+
* @type {Object}
|
|
1144
|
+
* @property {String} className The class name of the deleted instance
|
|
1145
|
+
* @property {String} instance The instance name
|
|
1146
|
+
* @property {Error} error What went wrong
|
|
1147
|
+
*/
|
|
1148
|
+
|
|
1006
1149
|
// Initialize static members
|
|
1007
1150
|
VrpcAdapter._functionRegistry = new Map()
|
|
1008
1151
|
VrpcAdapter._instances = new Map()
|
|
1009
1152
|
VrpcAdapter._emitter = new EventEmitter()
|
|
1010
1153
|
VrpcAdapter._correlationId = 0
|
|
1011
1154
|
VrpcAdapter._listeners = {}
|
|
1155
|
+
// the longest a delete waits for the dispose method of its instance
|
|
1156
|
+
VrpcAdapter.DISPOSE_TIMEOUT = 10000
|
|
1012
1157
|
VrpcAdapter._blackList = new Set([
|
|
1013
1158
|
'caller',
|
|
1014
1159
|
'callee',
|
package/vrpc/VrpcAgent.js
CHANGED
|
@@ -145,6 +145,13 @@ class VrpcAgent extends EventEmitter {
|
|
|
145
145
|
}
|
|
146
146
|
this._baseTopic = `${this._domain}/${this._agent}`
|
|
147
147
|
VrpcAdapter.onCallback(this._handleVrpcCallback.bind(this))
|
|
148
|
+
// a deleted instance whose dispose failed is gone anyway: say why
|
|
149
|
+
this._handleDisposeFailed = ({ instance, className, error }) =>
|
|
150
|
+
this._log.warn(
|
|
151
|
+
error,
|
|
152
|
+
`Dispose of deleted instance ${className}::${instance} failed: ${error.message}`
|
|
153
|
+
)
|
|
154
|
+
VrpcAdapter.on('disposeFailed', this._handleDisposeFailed)
|
|
148
155
|
// maps clientId to instanceId
|
|
149
156
|
this._isolatedInstances = new Map()
|
|
150
157
|
this._sharedInstances = new Map()
|
|
@@ -232,6 +239,7 @@ class VrpcAgent extends EventEmitter {
|
|
|
232
239
|
* @returns {Promise} Resolves when disconnected and ended
|
|
233
240
|
*/
|
|
234
241
|
async end ({ unregister = false } = {}) {
|
|
242
|
+
VrpcAdapter.off('disposeFailed', this._handleDisposeFailed)
|
|
235
243
|
this._subscribeRetrier.cancel()
|
|
236
244
|
try {
|
|
237
245
|
if (!this._client || !this._client.connected) {
|
|
@@ -744,7 +752,9 @@ class VrpcAgent extends EventEmitter {
|
|
|
744
752
|
const entry = this._isolatedInstances.get(connectionId)
|
|
745
753
|
if (entry) {
|
|
746
754
|
entry.forEach(instanceId => {
|
|
755
|
+
const known = VrpcAdapter._instances.get(instanceId)
|
|
747
756
|
const json = {
|
|
757
|
+
c: known && known.className,
|
|
748
758
|
f: '__delete__',
|
|
749
759
|
a: [instanceId],
|
|
750
760
|
r: null,
|
|
@@ -752,6 +762,8 @@ class VrpcAgent extends EventEmitter {
|
|
|
752
762
|
}
|
|
753
763
|
VrpcAdapter._call(json)
|
|
754
764
|
if (json.r) {
|
|
765
|
+
// gone like a remote delete: no request topic outlives it
|
|
766
|
+
this._unsubscribeMethodsOfDeletedInstance(known.className, instanceId)
|
|
755
767
|
this._log.debug(`Auto-deleted isolated instance: ${instanceId}`)
|
|
756
768
|
}
|
|
757
769
|
})
|
|
@@ -39,6 +39,16 @@ const Ajv = require('ajv')
|
|
|
39
39
|
const EventEmitter = require('events')
|
|
40
40
|
const { nanoid } = require('nanoid')
|
|
41
41
|
|
|
42
|
+
// A runtime without dispose symbols (older browsers, Node before 18.18 /
|
|
43
|
+
// 20.4) gets Node's own stand-ins from the symbol registry, so a class file
|
|
44
|
+
// and the adapter meet on one symbol whichever of them installs it first.
|
|
45
|
+
if (typeof Symbol.dispose !== 'symbol') {
|
|
46
|
+
Symbol.dispose = Symbol.for('nodejs.dispose')
|
|
47
|
+
}
|
|
48
|
+
if (typeof Symbol.asyncDispose !== 'symbol') {
|
|
49
|
+
Symbol.asyncDispose = Symbol.for('nodejs.asyncDispose')
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
const VRPC_GLOBAL = '__global__'
|
|
43
53
|
const VRPC_WINDOW_NS = 'vrpc'
|
|
44
54
|
|
|
@@ -188,7 +198,10 @@ class VrpcAdapter {
|
|
|
188
198
|
}
|
|
189
199
|
|
|
190
200
|
/**
|
|
191
|
-
* Deletes an instance
|
|
201
|
+
* Deletes an instance: its name is free at once, the adapter takes back
|
|
202
|
+
* every listener it attached, and the object releases what it runs
|
|
203
|
+
* through its `[Symbol.asyncDispose]` or `[Symbol.dispose]` method, which
|
|
204
|
+
* runs in the background here (a remote delete answers once it settled).
|
|
192
205
|
*
|
|
193
206
|
* @param {String|Object} instance Instance (name or object itself) to be deleted
|
|
194
207
|
* @returns {Boolean} True in case of success, false otherwise
|
|
@@ -199,10 +212,7 @@ class VrpcAdapter {
|
|
|
199
212
|
}
|
|
200
213
|
if (typeof instance === 'object') {
|
|
201
214
|
for (const [k, v] of VrpcAdapter._instances) {
|
|
202
|
-
if (v.instance === instance)
|
|
203
|
-
VrpcAdapter._instances.delete(k)
|
|
204
|
-
return true
|
|
205
|
-
}
|
|
215
|
+
if (v.instance === instance) return VrpcAdapter._delete(k)
|
|
206
216
|
}
|
|
207
217
|
return false
|
|
208
218
|
}
|
|
@@ -457,11 +467,17 @@ class VrpcAdapter {
|
|
|
457
467
|
instance,
|
|
458
468
|
className: json.c
|
|
459
469
|
})
|
|
460
|
-
|
|
470
|
+
const { ended, disposed } = VrpcAdapter._end(instance)
|
|
461
471
|
VrpcAdapter._emitter.emit('delete', {
|
|
462
472
|
instance,
|
|
463
473
|
className: json.c
|
|
464
474
|
})
|
|
475
|
+
// a delete answers once the object released what it runs, so that a
|
|
476
|
+
// constructor right after it never meets a resource the old object
|
|
477
|
+
// still holds; the agent's own housekeeping awaits no answer
|
|
478
|
+
if (disposed && json.s !== VrpcAdapter.LOCAL_SENDER) {
|
|
479
|
+
VrpcAdapter._handlePromise(json, disposed.then(() => ended))
|
|
480
|
+
} else json.r = ended
|
|
465
481
|
} catch (err) {
|
|
466
482
|
json.e = err.message
|
|
467
483
|
}
|
|
@@ -482,6 +498,15 @@ class VrpcAdapter {
|
|
|
482
498
|
json.r = true
|
|
483
499
|
return
|
|
484
500
|
}
|
|
501
|
+
// an unknown context fails before its arguments are unwrapped: a
|
|
502
|
+
// listener registered for a name that does not exist would outlive the
|
|
503
|
+
// failed call and pass for attached once the name is created
|
|
504
|
+
if (
|
|
505
|
+
!VrpcAdapter._functionRegistry.has(json.c) &&
|
|
506
|
+
!VrpcAdapter._instances.has(json.c)
|
|
507
|
+
) {
|
|
508
|
+
throw new Error(`Could not find context: ${json.c}`)
|
|
509
|
+
}
|
|
485
510
|
const unwrapped = VrpcAdapter._unwrapArguments(json)
|
|
486
511
|
if (!unwrapped) {
|
|
487
512
|
// this call must be skipped (event emitter stuff)
|
|
@@ -557,33 +582,112 @@ class VrpcAdapter {
|
|
|
557
582
|
}
|
|
558
583
|
|
|
559
584
|
static _delete (instanceId) {
|
|
585
|
+
return VrpcAdapter._end(instanceId).ended
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Ends an instance - the one teardown behind every door (a remote delete,
|
|
590
|
+
* 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
|
|
594
|
+
* ECMAScript dispose protocol, `[Symbol.asyncDispose]` or else
|
|
595
|
+
* `[Symbol.dispose]`.
|
|
596
|
+
*
|
|
597
|
+
* @returns {Object} `{ ended, disposed }`: whether the name was known, and
|
|
598
|
+
* a promise when an asynchronous dispose is under way (it never rejects)
|
|
599
|
+
*/
|
|
600
|
+
static _end (instanceId) {
|
|
560
601
|
const entry = VrpcAdapter._instances.get(instanceId)
|
|
561
|
-
if (entry) {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
602
|
+
if (!entry) return { ended: false, disposed: null }
|
|
603
|
+
VrpcAdapter._instances.delete(instanceId)
|
|
604
|
+
VrpcAdapter._detachListeners(instanceId, entry.instance)
|
|
605
|
+
return { ended: true, disposed: VrpcAdapter._dispose(instanceId, entry) }
|
|
606
|
+
}
|
|
607
|
+
|
|
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
|
|
565
619
|
}
|
|
566
|
-
delete VrpcAdapter._listeners[instanceId]
|
|
567
|
-
VrpcAdapter._instances.delete(instanceId)
|
|
568
|
-
return true
|
|
569
620
|
}
|
|
570
|
-
|
|
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
|
+
/**
|
|
632
|
+
* Runs the object's dispose method. A dispose that fails or outlasts
|
|
633
|
+
* VrpcAdapter.DISPOSE_TIMEOUT is reported (event 'disposeFailed') and
|
|
634
|
+
* never thrown: the instance is gone either way.
|
|
635
|
+
*
|
|
636
|
+
* @returns {Promise|null} Settles when an asynchronous dispose is done
|
|
637
|
+
*/
|
|
638
|
+
static _dispose (instanceId, { instance, className }) {
|
|
639
|
+
const fail = error =>
|
|
640
|
+
VrpcAdapter._emitter.emit('disposeFailed', {
|
|
641
|
+
instance: instanceId,
|
|
642
|
+
className,
|
|
643
|
+
error
|
|
644
|
+
})
|
|
645
|
+
let result
|
|
646
|
+
try {
|
|
647
|
+
if (VrpcAdapter._isFunction(instance[Symbol.asyncDispose])) {
|
|
648
|
+
result = instance[Symbol.asyncDispose]()
|
|
649
|
+
} else if (VrpcAdapter._isFunction(instance[Symbol.dispose])) {
|
|
650
|
+
result = instance[Symbol.dispose]()
|
|
651
|
+
}
|
|
652
|
+
} catch (err) {
|
|
653
|
+
fail(err)
|
|
654
|
+
return null
|
|
655
|
+
}
|
|
656
|
+
if (!VrpcAdapter._isPromise(result)) return null
|
|
657
|
+
let timer
|
|
658
|
+
const bound = new Promise(resolve => {
|
|
659
|
+
timer = setTimeout(() => {
|
|
660
|
+
fail(
|
|
661
|
+
new Error(
|
|
662
|
+
`dispose did not settle within ${VrpcAdapter.DISPOSE_TIMEOUT} ms`
|
|
663
|
+
)
|
|
664
|
+
)
|
|
665
|
+
resolve()
|
|
666
|
+
}, VrpcAdapter.DISPOSE_TIMEOUT)
|
|
667
|
+
})
|
|
668
|
+
const settled = result.then(
|
|
669
|
+
() => {},
|
|
670
|
+
err => fail(err)
|
|
671
|
+
)
|
|
672
|
+
return Promise.race([settled, bound]).then(() => clearTimeout(timer))
|
|
571
673
|
}
|
|
572
674
|
|
|
573
675
|
static _unregisterClient (clientId) {
|
|
574
676
|
Object.entries(VrpcAdapter._listeners).forEach(([ik, iv]) => {
|
|
575
677
|
Object.entries(iv).forEach(([ek, ev]) => {
|
|
576
|
-
const {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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)
|
|
586
689
|
}
|
|
690
|
+
delete VrpcAdapter._listeners[ik][ek]
|
|
587
691
|
}
|
|
588
692
|
})
|
|
589
693
|
})
|
|
@@ -622,6 +726,7 @@ class VrpcAdapter {
|
|
|
622
726
|
// event registration
|
|
623
727
|
if (func === 'on' || func === 'addListener') {
|
|
624
728
|
VrpcAdapter._mustTrackClient = true
|
|
729
|
+
const held = VrpcAdapter._holdsListener(context, arg)
|
|
625
730
|
const listener = VrpcAdapter._registerListener({
|
|
626
731
|
clientId,
|
|
627
732
|
isCallAll,
|
|
@@ -629,9 +734,8 @@ class VrpcAdapter {
|
|
|
629
734
|
eventId: arg,
|
|
630
735
|
event: args[0]
|
|
631
736
|
})
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
}
|
|
737
|
+
// attached already: skip the call, one listener hears once
|
|
738
|
+
if (held) return null
|
|
635
739
|
unwrapped.push(listener)
|
|
636
740
|
// event un-registration
|
|
637
741
|
} else if (func === 'off' || func === 'removeListener') {
|
|
@@ -666,8 +770,32 @@ class VrpcAdapter {
|
|
|
666
770
|
return unwrapped
|
|
667
771
|
}
|
|
668
772
|
|
|
773
|
+
/**
|
|
774
|
+
* Whether the object under `instanceId` is still the one the adapter saw
|
|
775
|
+
* when it built a listener or a callback for it: a deleted instance never
|
|
776
|
+
* speaks on the bus again, even when its class kept the listener or its
|
|
777
|
+
* clock kept running. A static context (a class name) has no object and
|
|
778
|
+
* always speaks.
|
|
779
|
+
*/
|
|
780
|
+
static _speaksFor (instanceId) {
|
|
781
|
+
const entry = VrpcAdapter._instances.get(instanceId)
|
|
782
|
+
if (!entry) return () => true
|
|
783
|
+
const { instance } = entry
|
|
784
|
+
return () => {
|
|
785
|
+
const live = VrpcAdapter._instances.get(instanceId)
|
|
786
|
+
return live !== undefined && live.instance === instance
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
static _holdsListener (instanceId, eventId) {
|
|
791
|
+
const registrations = VrpcAdapter._listeners[instanceId]
|
|
792
|
+
return Boolean(registrations && registrations[eventId])
|
|
793
|
+
}
|
|
794
|
+
|
|
669
795
|
static _generateCallback ({ eventId, clientId, instanceId, isCallAll }) {
|
|
796
|
+
const speaks = VrpcAdapter._speaksFor(instanceId)
|
|
670
797
|
return (...innerArgs) => {
|
|
798
|
+
if (!speaks()) return
|
|
671
799
|
// "callAll" event notifications expect the instanceId as first argument
|
|
672
800
|
const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
|
|
673
801
|
VrpcAdapter._callback({ a, s: clientId, i: eventId })
|
|
@@ -708,11 +836,13 @@ class VrpcAdapter {
|
|
|
708
836
|
return VrpcAdapter._listeners[instanceId][eventId].listener
|
|
709
837
|
}
|
|
710
838
|
VrpcAdapter._listeners[instanceId][eventId].clients.push(clientId)
|
|
711
|
-
return
|
|
839
|
+
return VrpcAdapter._listeners[instanceId][eventId].listener
|
|
712
840
|
}
|
|
713
841
|
|
|
714
842
|
static _generateListener ({ eventId, instanceId, isCallAll }) {
|
|
843
|
+
const speaks = VrpcAdapter._speaksFor(instanceId)
|
|
715
844
|
return (...innerArgs) => {
|
|
845
|
+
if (!speaks()) return
|
|
716
846
|
const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
|
|
717
847
|
VrpcAdapter._callback({ a, i: eventId })
|
|
718
848
|
}
|
|
@@ -892,12 +1022,28 @@ class VrpcAdapter {
|
|
|
892
1022
|
* @property {String} instance The instance name
|
|
893
1023
|
*/
|
|
894
1024
|
|
|
1025
|
+
/**
|
|
1026
|
+
* Event 'disposeFailed'
|
|
1027
|
+
*
|
|
1028
|
+
* Emitted when the dispose method of a deleted instance throws, rejects or
|
|
1029
|
+
* does not settle within VrpcAdapter.DISPOSE_TIMEOUT. The instance is gone
|
|
1030
|
+
* regardless.
|
|
1031
|
+
*
|
|
1032
|
+
* @event VrpcAdapter#disposeFailed
|
|
1033
|
+
* @type {Object}
|
|
1034
|
+
* @property {String} className The class name of the deleted instance
|
|
1035
|
+
* @property {String} instance The instance name
|
|
1036
|
+
* @property {Error} error What went wrong
|
|
1037
|
+
*/
|
|
1038
|
+
|
|
895
1039
|
// Initialize static members
|
|
896
1040
|
VrpcAdapter._functionRegistry = new Map()
|
|
897
1041
|
VrpcAdapter._instances = new Map()
|
|
898
1042
|
VrpcAdapter._emitter = new EventEmitter()
|
|
899
1043
|
VrpcAdapter._correlationId = 0
|
|
900
1044
|
VrpcAdapter._listeners = {}
|
|
1045
|
+
// the longest a delete waits for the dispose method of its instance
|
|
1046
|
+
VrpcAdapter.DISPOSE_TIMEOUT = 10000
|
|
901
1047
|
VrpcAdapter._blackList = new Set([
|
|
902
1048
|
'caller',
|
|
903
1049
|
'callee',
|
package/vrpc/VrpcBrowserAgent.js
CHANGED
|
@@ -68,6 +68,13 @@ class VrpcAgent extends EventEmitter {
|
|
|
68
68
|
}
|
|
69
69
|
this._baseTopic = `${this._domain}/${this._agent}`
|
|
70
70
|
VrpcAdapter.onCallback(this._handleVrpcCallback.bind(this))
|
|
71
|
+
// a deleted instance whose dispose failed is gone anyway: say why
|
|
72
|
+
this._handleDisposeFailed = ({ instance, className, error }) =>
|
|
73
|
+
this._log.warn(
|
|
74
|
+
error,
|
|
75
|
+
`Dispose of deleted instance ${className}::${instance} failed: ${error.message}`
|
|
76
|
+
)
|
|
77
|
+
VrpcAdapter.on('disposeFailed', this._handleDisposeFailed)
|
|
71
78
|
// maps clientId to instanceId
|
|
72
79
|
this._isolatedInstances = new Map()
|
|
73
80
|
this._sharedInstances = new Map()
|
|
@@ -146,6 +153,7 @@ class VrpcAgent extends EventEmitter {
|
|
|
146
153
|
* @returns {Promise} Resolves when disconnected and ended
|
|
147
154
|
*/
|
|
148
155
|
async end ({ unregister = false } = {}) {
|
|
156
|
+
VrpcAdapter.off('disposeFailed', this._handleDisposeFailed)
|
|
149
157
|
try {
|
|
150
158
|
if (!this._client || !this._client.connected) {
|
|
151
159
|
this.emit('end')
|
|
@@ -542,7 +550,9 @@ class VrpcAgent extends EventEmitter {
|
|
|
542
550
|
const entry = this._isolatedInstances.get(connectionId)
|
|
543
551
|
if (entry) {
|
|
544
552
|
entry.forEach(instanceId => {
|
|
553
|
+
const known = VrpcAdapter._instances.get(instanceId)
|
|
545
554
|
const json = {
|
|
555
|
+
c: known && known.className,
|
|
546
556
|
f: '__delete__',
|
|
547
557
|
a: [instanceId],
|
|
548
558
|
r: null,
|
|
@@ -550,6 +560,8 @@ class VrpcAgent extends EventEmitter {
|
|
|
550
560
|
}
|
|
551
561
|
VrpcAdapter._call(json)
|
|
552
562
|
if (json.r) {
|
|
563
|
+
// gone like a remote delete: no request topic outlives it
|
|
564
|
+
this._unsubscribeMethodsOfDeletedInstance(known.className, instanceId)
|
|
553
565
|
this._log.debug(`Auto-deleted isolated instance: ${instanceId}`)
|
|
554
566
|
}
|
|
555
567
|
})
|