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.
- package/LICENSE +1 -1
- package/README.md +100 -77
- package/browser/vrpc.js +1 -7
- package/index.js +7 -7
- package/package.json +23 -15
- package/vrpc/Registrations.js +850 -0
- package/vrpc/SubscribeRetrier.js +33 -0
- package/vrpc/VrpcAdapter.js +556 -294
- package/vrpc/VrpcAgent.js +230 -153
- package/vrpc/VrpcBrowserAdapter.js +435 -286
- package/vrpc/VrpcBrowserAgent.js +239 -128
- package/vrpc/VrpcClient.js +1190 -224
- package/vrpc/VrpcNative.js +10 -5
- package/vrpc/VrpcPersistor.js +1 -1
- package/vrpc/adapter.hpp +1 -1
- package/vrpc/addon.cpp +1 -1
- package/vrpc/comment-parser/parser.js +23 -0
- package/vrpc/namePattern.js +86 -0
- package/vrpc/vrpc-agent-js +3 -11
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const EventEmitter = require('events')
|
|
4
|
+
|
|
5
|
+
// Node before 18.18 / 20.4 and older browsers have no dispose symbols: the
|
|
6
|
+
// registry's stand-ins, as the adapters install them
|
|
7
|
+
if (typeof Symbol.dispose !== 'symbol') {
|
|
8
|
+
Symbol.dispose = Symbol.for('nodejs.dispose')
|
|
9
|
+
}
|
|
10
|
+
if (typeof Symbol.asyncDispose !== 'symbol') {
|
|
11
|
+
Symbol.asyncDispose = Symbol.for('nodejs.asyncDispose')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The agent's book of event registrations (protocol version 4).
|
|
16
|
+
*
|
|
17
|
+
* A *registration* is the one call of an event function on one context (an
|
|
18
|
+
* instance, or a class for static functions) with one set of arguments. A
|
|
19
|
+
* *subscription* is a client's wish to receive its events; it travels as the
|
|
20
|
+
* `__e__<id>` argument in the handler's position, and that id already names
|
|
21
|
+
* the client connection it belongs to.
|
|
22
|
+
*
|
|
23
|
+
* The book keeps three promises, whatever clients repeat, flap or restart:
|
|
24
|
+
* - the event function is called once per context, function and arguments
|
|
25
|
+
* (handler arguments excluded, the rest compared by value);
|
|
26
|
+
* - every event reaches every subscription once: the class holds one
|
|
27
|
+
* fan-out listener per handler position, which hands each event to all
|
|
28
|
+
* subscriptions of the registration;
|
|
29
|
+
* - subscriptions are sets: declaring one again changes nothing, and the
|
|
30
|
+
* last one out releases the registration, after which its listener is
|
|
31
|
+
* silent.
|
|
32
|
+
*
|
|
33
|
+
* A registration may greet: its `greet(...handlers)` is called once for
|
|
34
|
+
* every new subscription - the first one included - with handlers that reach
|
|
35
|
+
* that subscription alone, so every subscriber gets what the class greets
|
|
36
|
+
* with (its current state), not only the one whose call registered it.
|
|
37
|
+
*
|
|
38
|
+
* A subscription id names one registration per context: a protocol-4
|
|
39
|
+
* client gives every target, function, arguments and callback its own id,
|
|
40
|
+
* and a request bringing a held id with other function or arguments to the
|
|
41
|
+
* same context is refused - the book never guesses whether that meant
|
|
42
|
+
* "replace" or "add". Clients before protocol 4 derived ids from less (the
|
|
43
|
+
* first string argument, else the function name); their ids may hold
|
|
44
|
+
* several registrations on one context, as they did before.
|
|
45
|
+
*
|
|
46
|
+
* Shared by VrpcAdapter and VrpcBrowserAdapter: both adapters follow the
|
|
47
|
+
* same laws, so they keep them in one place.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
// registration states
|
|
51
|
+
const PENDING = 0 // the first call is still running (async event function)
|
|
52
|
+
const LIVE = 1
|
|
53
|
+
const SILENT = 2 // nobody listens, and the class offers no way to release
|
|
54
|
+
const GONE = 3 // released, ended, failed or forgotten
|
|
55
|
+
|
|
56
|
+
// stand for a handler (subscription) and a one-shot callback argument in a
|
|
57
|
+
// registration key
|
|
58
|
+
const HANDLER = '\u0000vrpc-handler'
|
|
59
|
+
const CALLBACK = '\u0000vrpc-callback'
|
|
60
|
+
|
|
61
|
+
// the window events per second are measured over
|
|
62
|
+
const RATE_WINDOW = 5000
|
|
63
|
+
|
|
64
|
+
// a protocol-4 client is told when its subscription ends
|
|
65
|
+
const NOTICE_VERSION = 4
|
|
66
|
+
|
|
67
|
+
// from protocol 4 on, an id names one registration per context
|
|
68
|
+
const ID_VERSION = 4
|
|
69
|
+
|
|
70
|
+
function isFunction (obj) {
|
|
71
|
+
return typeof obj === 'function'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isPromise (obj) {
|
|
75
|
+
return (
|
|
76
|
+
!!obj &&
|
|
77
|
+
(typeof obj === 'object' || typeof obj === 'function') &&
|
|
78
|
+
typeof obj.then === 'function'
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Whether a returned value is a registration: any object that can be
|
|
84
|
+
* disposed (`[Symbol.dispose]()` or `[Symbol.asyncDispose]()`), a
|
|
85
|
+
* DisposableStack included.
|
|
86
|
+
*/
|
|
87
|
+
function isRegistration (value) {
|
|
88
|
+
return (
|
|
89
|
+
!!value &&
|
|
90
|
+
(typeof value === 'object' || typeof value === 'function') &&
|
|
91
|
+
(isFunction(value[Symbol.asyncDispose]) || isFunction(value[Symbol.dispose]))
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// EventEmitter instances answer `on` with themselves - nothing to serialize
|
|
96
|
+
function sanitize (ret) {
|
|
97
|
+
return ret &&
|
|
98
|
+
typeof ret === 'object' &&
|
|
99
|
+
ret._events !== undefined &&
|
|
100
|
+
ret._eventsCount !== undefined
|
|
101
|
+
? true
|
|
102
|
+
: ret
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function sortKeys (value) {
|
|
106
|
+
if (Array.isArray(value)) return value.map(sortKeys)
|
|
107
|
+
if (value && typeof value === 'object' && value.constructor === Object) {
|
|
108
|
+
const sorted = {}
|
|
109
|
+
for (const key of Object.keys(value).sort()) sorted[key] = sortKeys(value[key])
|
|
110
|
+
return sorted
|
|
111
|
+
}
|
|
112
|
+
return value
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Arguments compared by value: equal JSON once object keys are sorted.
|
|
117
|
+
*/
|
|
118
|
+
function canonical (args) {
|
|
119
|
+
return JSON.stringify(sortKeys(args))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Functions whose call with a subscription id withdraws that subscription:
|
|
124
|
+
* Node's `off`/`removeListener` and the `off<Name>` twins of `on<Name>`
|
|
125
|
+
* event functions - and, from clients before protocol 4, the twins of their
|
|
126
|
+
* deprecated prefixes (`unmonitor<Name>`, `unsignal<Name>`,
|
|
127
|
+
* `remove<Name>`). Old engines release this way; the class is never called
|
|
128
|
+
* for it (its one listener belongs to all subscriptions).
|
|
129
|
+
*/
|
|
130
|
+
function isRemover (func, v) {
|
|
131
|
+
if (func === 'off' || func === 'removeListener' || /^off[A-Z]/.test(func)) {
|
|
132
|
+
return true
|
|
133
|
+
}
|
|
134
|
+
return !(v >= ID_VERSION) && /^(unmonitor|unsignal|remove)[A-Z]/.test(func)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The event functions a remover undoes: `on` and `addListener` for `off` and
|
|
139
|
+
* `removeListener`, `onValue` for `offValue`, `monitorX` for `unmonitorX`;
|
|
140
|
+
* null when the name tells none (`remove<Name>`).
|
|
141
|
+
*/
|
|
142
|
+
function twinsOf (func) {
|
|
143
|
+
if (func === 'off' || func === 'removeListener') return ['on', 'addListener']
|
|
144
|
+
if (/^off[A-Z]/.test(func)) return [`on${func.slice(3)}`]
|
|
145
|
+
if (/^un(monitor|signal)[A-Z]/.test(func)) return [func.slice(2)]
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The function that undoes an event function's call when the class returns
|
|
151
|
+
* no registration (deprecated): `offValue` for `onValue`, `off` for a
|
|
152
|
+
* class's own `on`, `unmonitorX` for `monitorX`; null when none is named.
|
|
153
|
+
*/
|
|
154
|
+
function releaseTwinOf (func) {
|
|
155
|
+
if (func === 'on') return 'off'
|
|
156
|
+
if (func === 'addListener') return 'removeListener'
|
|
157
|
+
if (/^on[A-Z]/.test(func)) return `off${func.slice(2)}`
|
|
158
|
+
if (/^(monitor|signal)[A-Z]/.test(func)) return `un${func}`
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isEventId (arg) {
|
|
163
|
+
return typeof arg === 'string' && arg.startsWith('__e__')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Whether a subscription id names a topic of its sender: `__e__<sender>/`
|
|
168
|
+
* followed by one or more levels, none of them empty or a wildcard. Clients
|
|
169
|
+
* since 3.6 derive their ids this way.
|
|
170
|
+
*/
|
|
171
|
+
function ownsTopic (sender, id) {
|
|
172
|
+
if (typeof sender !== 'string') return false
|
|
173
|
+
const prefix = `__e__${sender}/`
|
|
174
|
+
if (!id.startsWith(prefix)) return false
|
|
175
|
+
return id
|
|
176
|
+
.slice(prefix.length)
|
|
177
|
+
.split('/')
|
|
178
|
+
.every(level => level.length > 0 && !/[+#]/.test(level))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
class Registrations {
|
|
182
|
+
/**
|
|
183
|
+
* @param {Object} hooks
|
|
184
|
+
* @param {Function} hooks.publish Sends a message `{ a, i }` (an event) or
|
|
185
|
+
* `{ i, end }` (an ended notice) to the subscription's topic
|
|
186
|
+
* @param {Function} hooks.emit Emits an adapter event (`releaseFailed`)
|
|
187
|
+
*/
|
|
188
|
+
constructor ({ publish, emit }) {
|
|
189
|
+
this._publish = publish
|
|
190
|
+
this._emit = emit
|
|
191
|
+
// contextKey -> Map(key -> registration)
|
|
192
|
+
this._contexts = new Map()
|
|
193
|
+
// subscription id -> Map(contextKey + key -> subscription): one per
|
|
194
|
+
// context, but for clients before protocol 4
|
|
195
|
+
this._held = new Map()
|
|
196
|
+
// client connection id -> Set(subscription id)
|
|
197
|
+
this._byClient = new Map()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Whether a call carries subscription ids (`__e__<id>`) and so is a
|
|
202
|
+
* subscription, or the withdrawal of one, rather than a plain call.
|
|
203
|
+
*/
|
|
204
|
+
static carriesEventIds (args) {
|
|
205
|
+
if (!Array.isArray(args)) return false
|
|
206
|
+
for (let i = 0; i < args.length; i++) {
|
|
207
|
+
if (isEventId(args[i])) return true
|
|
208
|
+
}
|
|
209
|
+
return false
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Handles a call that carries subscription ids.
|
|
214
|
+
*
|
|
215
|
+
* @param {Object} options
|
|
216
|
+
* @param {Object} options.json The request (sender `s`, version `v`,
|
|
217
|
+
* optional subscriber labels `l: { [id]: label }`)
|
|
218
|
+
* @param {String} options.contextKey Book key of the context
|
|
219
|
+
* @param {String} options.contextName Instance id (or class name)
|
|
220
|
+
* @param {Object} options.target The instance (or class)
|
|
221
|
+
* @param {String} options.func Function name
|
|
222
|
+
* @param {Array} options.args Wire arguments
|
|
223
|
+
* @param {Boolean} options.isCallAll Events carry the instance id first
|
|
224
|
+
* @param {Function} options.speaks Whether the context still speaks
|
|
225
|
+
* @param {Function} options.makeCallback Builds a one-shot `__f__` callback
|
|
226
|
+
* @returns {Object} `{ value, ids }`: the answer (a value, or a promise of
|
|
227
|
+
* it) and the subscription ids the context now holds for this call
|
|
228
|
+
*/
|
|
229
|
+
call ({
|
|
230
|
+
json,
|
|
231
|
+
contextKey,
|
|
232
|
+
contextName,
|
|
233
|
+
target,
|
|
234
|
+
func,
|
|
235
|
+
args,
|
|
236
|
+
isCallAll,
|
|
237
|
+
speaks,
|
|
238
|
+
makeCallback
|
|
239
|
+
}) {
|
|
240
|
+
const clientId = json.s
|
|
241
|
+
const ids = []
|
|
242
|
+
const keyArgs = new Array(args.length)
|
|
243
|
+
for (let i = 0; i < args.length; i++) {
|
|
244
|
+
const arg = args[i]
|
|
245
|
+
if (isEventId(arg)) {
|
|
246
|
+
ids.push(arg)
|
|
247
|
+
keyArgs[i] = HANDLER
|
|
248
|
+
} else if (typeof arg === 'string' && arg.startsWith('__f__')) {
|
|
249
|
+
keyArgs[i] = CALLBACK
|
|
250
|
+
} else {
|
|
251
|
+
keyArgs[i] = arg
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// events go to the topic an id names: only ever one of the sender's own
|
|
255
|
+
// topics, never one of somebody else
|
|
256
|
+
for (const id of ids) {
|
|
257
|
+
if (!ownsTopic(clientId, id)) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
"A subscription id must name a topic under its sender's connection id"
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (isRemover(func, json.v)) {
|
|
264
|
+
this._withdrawByRemover(func, keyArgs, ids, clientId, contextKey)
|
|
265
|
+
return { value: true, ids: [] }
|
|
266
|
+
}
|
|
267
|
+
if (!isFunction(target[func])) {
|
|
268
|
+
throw new Error(`Could not find function: ${func}`)
|
|
269
|
+
}
|
|
270
|
+
// a one-shot is no subscription (clients before 3.15 send `once` with
|
|
271
|
+
// an event id): each call gets its own listener, as it always did
|
|
272
|
+
if (func === 'once' || func === 'prependOnceListener') {
|
|
273
|
+
const isSpeaking = speaks || (() => true)
|
|
274
|
+
const unwrapped = args.map((arg, i) => {
|
|
275
|
+
if (keyArgs[i] === CALLBACK) return makeCallback(arg)
|
|
276
|
+
if (keyArgs[i] !== HANDLER) return arg
|
|
277
|
+
return (...innerArgs) => {
|
|
278
|
+
if (!isSpeaking()) return
|
|
279
|
+
this._publish({
|
|
280
|
+
a: isCallAll ? [contextName, ...innerArgs] : innerArgs,
|
|
281
|
+
i: arg,
|
|
282
|
+
receiverVersion: json.v
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
return { value: sanitize(target[func].apply(target, unwrapped)), ids: [] }
|
|
287
|
+
}
|
|
288
|
+
const key = `${func}\u0000${canonical(keyArgs)}`
|
|
289
|
+
// protocol 4: an id names one registration per context - another one
|
|
290
|
+
// under the same id is a client's mistake, never a replacement
|
|
291
|
+
if (json.v >= ID_VERSION) {
|
|
292
|
+
for (const id of ids) {
|
|
293
|
+
const other = this._subscriptionsOn(id, contextKey).find(
|
|
294
|
+
sub => sub.reg.key !== key
|
|
295
|
+
)
|
|
296
|
+
if (other) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
`Subscription id already held here by ${other.reg.func} with other arguments: every target, function and arguments needs its own id`
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
let regs = this._contexts.get(contextKey)
|
|
304
|
+
if (!regs) {
|
|
305
|
+
regs = new Map()
|
|
306
|
+
this._contexts.set(contextKey, regs)
|
|
307
|
+
}
|
|
308
|
+
let reg = regs.get(key)
|
|
309
|
+
if (reg) {
|
|
310
|
+
const added = this._join(reg, ids, json, isCallAll)
|
|
311
|
+
if (reg.state === SILENT) reg.state = LIVE
|
|
312
|
+
// a pending registration greets everybody once it settled
|
|
313
|
+
this._greet(reg, added)
|
|
314
|
+
return { value: reg.state === PENDING ? reg.promise : reg.r, ids }
|
|
315
|
+
}
|
|
316
|
+
const nodeEmitter =
|
|
317
|
+
(func === 'on' || func === 'addListener') &&
|
|
318
|
+
target[func] === EventEmitter.prototype[func]
|
|
319
|
+
reg = {
|
|
320
|
+
contextKey,
|
|
321
|
+
contextName,
|
|
322
|
+
target,
|
|
323
|
+
func,
|
|
324
|
+
key,
|
|
325
|
+
args: keyArgs.filter(x => x !== HANDLER && x !== CALLBACK),
|
|
326
|
+
event: func === 'on' || func === 'addListener' ? args[0] : null,
|
|
327
|
+
nodeEmitter,
|
|
328
|
+
state: PENDING,
|
|
329
|
+
positions: [],
|
|
330
|
+
r: undefined,
|
|
331
|
+
promise: null,
|
|
332
|
+
registration: null,
|
|
333
|
+
events: 0,
|
|
334
|
+
bucketStart: Date.now(),
|
|
335
|
+
bucketCount: 0,
|
|
336
|
+
rate: null
|
|
337
|
+
}
|
|
338
|
+
const isSpeaking = speaks || (() => true)
|
|
339
|
+
reg.speaks = isSpeaking
|
|
340
|
+
for (let k = 0; k < ids.length; k++) {
|
|
341
|
+
reg.positions.push({
|
|
342
|
+
subs: new Map(),
|
|
343
|
+
fanout: this._fanout(reg, k, contextName, isSpeaking)
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
regs.set(key, reg)
|
|
347
|
+
this._join(reg, ids, json, isCallAll)
|
|
348
|
+
let k = 0
|
|
349
|
+
const unwrapped = args.map((arg, i) => {
|
|
350
|
+
if (keyArgs[i] === CALLBACK) return makeCallback(arg)
|
|
351
|
+
if (keyArgs[i] !== HANDLER) return arg
|
|
352
|
+
return reg.positions[k++].fanout
|
|
353
|
+
})
|
|
354
|
+
// a twin that releases (deprecated) is called with the same arguments
|
|
355
|
+
reg.unwrapped = unwrapped
|
|
356
|
+
let ret
|
|
357
|
+
try {
|
|
358
|
+
ret = target[func].apply(target, unwrapped)
|
|
359
|
+
} catch (err) {
|
|
360
|
+
this._fail(reg)
|
|
361
|
+
throw err
|
|
362
|
+
}
|
|
363
|
+
if (isPromise(ret)) {
|
|
364
|
+
reg.promise = Promise.resolve(ret).then(
|
|
365
|
+
value => {
|
|
366
|
+
this._settle(reg, value)
|
|
367
|
+
return reg.r
|
|
368
|
+
},
|
|
369
|
+
err => {
|
|
370
|
+
this._fail(reg)
|
|
371
|
+
throw err
|
|
372
|
+
}
|
|
373
|
+
)
|
|
374
|
+
return { value: reg.promise, ids }
|
|
375
|
+
}
|
|
376
|
+
this._settle(reg, ret)
|
|
377
|
+
return { value: reg.r, ids }
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Withdraws subscriptions by id wherever the sender holds them - or, for
|
|
382
|
+
* an entry `{ id, context }`, on that one context (instance) only: a
|
|
383
|
+
* callAll subscription holds one id on every instance of its class, and
|
|
384
|
+
* an end that reached one of them withdraws it there alone.
|
|
385
|
+
*
|
|
386
|
+
* @param {Array.<String|Object>} ids Subscription ids, or `{ id, context }`
|
|
387
|
+
* @param {String} clientId The sender: only its own subscriptions go
|
|
388
|
+
* @returns {Number} How many subscriptions were withdrawn
|
|
389
|
+
*/
|
|
390
|
+
unsubscribe (ids, clientId) {
|
|
391
|
+
let count = 0
|
|
392
|
+
for (const entry of Array.isArray(ids) ? ids : [ids]) {
|
|
393
|
+
if (isEventId(entry)) {
|
|
394
|
+
count += this._withdraw(entry, clientId)
|
|
395
|
+
} else if (
|
|
396
|
+
entry &&
|
|
397
|
+
typeof entry === 'object' &&
|
|
398
|
+
isEventId(entry.id) &&
|
|
399
|
+
typeof entry.context === 'string'
|
|
400
|
+
) {
|
|
401
|
+
count += this._withdraw(entry.id, clientId, entry.context)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return count
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* A client connection went: all its subscriptions are withdrawn.
|
|
409
|
+
*/
|
|
410
|
+
withdrawClient (clientId) {
|
|
411
|
+
const ids = this._byClient.get(clientId)
|
|
412
|
+
if (!ids) return
|
|
413
|
+
for (const id of Array.from(ids)) this._withdraw(id, clientId)
|
|
414
|
+
this._byClient.delete(clientId)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* A client's `removeAllListeners(event)`: withdraws that client's
|
|
419
|
+
* subscriptions of the event on the context (all its `on` subscriptions
|
|
420
|
+
* there without an event). The emitter itself is never touched.
|
|
421
|
+
*/
|
|
422
|
+
withdrawEvent (contextKey, event, clientId) {
|
|
423
|
+
const regs = this._contexts.get(contextKey)
|
|
424
|
+
if (!regs) return
|
|
425
|
+
for (const reg of Array.from(regs.values())) {
|
|
426
|
+
if (reg.func !== 'on' && reg.func !== 'addListener') continue
|
|
427
|
+
if (event !== undefined && event !== null && reg.event !== event) continue
|
|
428
|
+
for (const sub of this._subscriptionsOf(reg)) {
|
|
429
|
+
if (sub.clientId === clientId) this._remove(sub)
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Lists the registrations of a context.
|
|
436
|
+
*
|
|
437
|
+
* @returns {Array.<Object>} `{ function, args, state, subscriptions:
|
|
438
|
+
* [{ label, callAll }], events, rate }` per registration, rate in events
|
|
439
|
+
* per second (subscription ids are credentials and never listed)
|
|
440
|
+
*/
|
|
441
|
+
list (contextKey) {
|
|
442
|
+
const regs = this._contexts.get(contextKey)
|
|
443
|
+
if (!regs) return []
|
|
444
|
+
const now = Date.now()
|
|
445
|
+
return Array.from(regs.values()).map(reg => ({
|
|
446
|
+
function: reg.func,
|
|
447
|
+
args: reg.args,
|
|
448
|
+
state: ['pending', 'live', 'silent', 'gone'][reg.state],
|
|
449
|
+
subscriptions: this._subscriptionsOf(reg).map(sub => ({
|
|
450
|
+
label: sub.label === undefined ? null : sub.label,
|
|
451
|
+
callAll: sub.isCallAll
|
|
452
|
+
})),
|
|
453
|
+
events: reg.events,
|
|
454
|
+
rate: Registrations._rate(reg, now)
|
|
455
|
+
}))
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Ends one registration for all its subscriptions: released, and every
|
|
460
|
+
* (protocol 4) subscription is told it ended and by whom.
|
|
461
|
+
*
|
|
462
|
+
* @returns {Boolean} Whether such a registration existed
|
|
463
|
+
*/
|
|
464
|
+
end (contextKey, func, args = [], by = null) {
|
|
465
|
+
const regs = this._contexts.get(contextKey)
|
|
466
|
+
if (!regs) return false
|
|
467
|
+
const wanted = canonical(Array.isArray(args) ? args : [args])
|
|
468
|
+
for (const reg of regs.values()) {
|
|
469
|
+
if (reg.func !== func || canonical(reg.args) !== wanted) continue
|
|
470
|
+
this._ended(reg, { reason: 'ended', by })
|
|
471
|
+
return true
|
|
472
|
+
}
|
|
473
|
+
return false
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* The context (an instance) ended: its registrations are forgotten and
|
|
478
|
+
* their listeners fall silent. Its own dispose releases what they run;
|
|
479
|
+
* an instance without one gets the releases the book knows (legacy).
|
|
480
|
+
* Node's own `on` is always undone: that is in-memory and safe.
|
|
481
|
+
*/
|
|
482
|
+
forgetContext (contextKey, { release = true } = {}) {
|
|
483
|
+
const regs = this._contexts.get(contextKey)
|
|
484
|
+
if (!regs) return
|
|
485
|
+
this._contexts.delete(contextKey)
|
|
486
|
+
for (const reg of regs.values()) {
|
|
487
|
+
this._unlink(reg)
|
|
488
|
+
if (release || reg.nodeEmitter) this._release(reg)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Whether a client holds a subscription id on a context.
|
|
494
|
+
*/
|
|
495
|
+
holds (contextKey, id, clientId) {
|
|
496
|
+
return this._subscriptionsOn(id, contextKey).some(
|
|
497
|
+
sub => clientId === undefined || sub.clientId === clientId
|
|
498
|
+
)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// private:
|
|
502
|
+
|
|
503
|
+
_fanout (reg, k, contextName, speaks) {
|
|
504
|
+
return (...innerArgs) => {
|
|
505
|
+
if (reg.state >= SILENT || !speaks()) return
|
|
506
|
+
this._count(reg)
|
|
507
|
+
for (const sub of reg.positions[k].subs.values()) {
|
|
508
|
+
// the subscriber's version: whether it can be told of an event
|
|
509
|
+
// that cannot travel (EV-22)
|
|
510
|
+
this._publish({
|
|
511
|
+
a: sub.isCallAll ? [contextName, ...innerArgs] : innerArgs,
|
|
512
|
+
i: sub.id,
|
|
513
|
+
receiverVersion: sub.v
|
|
514
|
+
})
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
_count (reg) {
|
|
520
|
+
reg.events++
|
|
521
|
+
const now = Date.now()
|
|
522
|
+
if (now - reg.bucketStart >= RATE_WINDOW) {
|
|
523
|
+
reg.rate = (reg.bucketCount * 1000) / (now - reg.bucketStart)
|
|
524
|
+
reg.bucketStart = now
|
|
525
|
+
reg.bucketCount = 0
|
|
526
|
+
}
|
|
527
|
+
reg.bucketCount++
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
static _rate (reg, now) {
|
|
531
|
+
const elapsed = now - reg.bucketStart
|
|
532
|
+
if (elapsed >= RATE_WINDOW || reg.rate === null) {
|
|
533
|
+
return (reg.bucketCount * 1000) / Math.max(elapsed, 1000)
|
|
534
|
+
}
|
|
535
|
+
return reg.rate
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// adds the subscriptions a declaration carries; answers the new ones
|
|
539
|
+
_join (reg, ids, json, isCallAll) {
|
|
540
|
+
const added = []
|
|
541
|
+
const labels = json.l && typeof json.l === 'object' ? json.l : {}
|
|
542
|
+
for (let k = 0; k < ids.length && k < reg.positions.length; k++) {
|
|
543
|
+
const id = ids[k]
|
|
544
|
+
// one callback in two handler positions is one subscription
|
|
545
|
+
if (ids.indexOf(id) !== k) continue
|
|
546
|
+
const position = reg.positions[k]
|
|
547
|
+
let sub = position.subs.get(id)
|
|
548
|
+
if (!sub) {
|
|
549
|
+
sub = {
|
|
550
|
+
id,
|
|
551
|
+
clientId: json.s,
|
|
552
|
+
isCallAll: !!isCallAll,
|
|
553
|
+
reg,
|
|
554
|
+
position,
|
|
555
|
+
contextKey: reg.contextKey
|
|
556
|
+
}
|
|
557
|
+
position.subs.set(id, sub)
|
|
558
|
+
let held = this._held.get(id)
|
|
559
|
+
if (!held) {
|
|
560
|
+
held = new Map()
|
|
561
|
+
this._held.set(id, held)
|
|
562
|
+
}
|
|
563
|
+
held.set(Registrations._holdKey(sub), sub)
|
|
564
|
+
let byClient = this._byClient.get(json.s)
|
|
565
|
+
if (!byClient) {
|
|
566
|
+
byClient = new Set()
|
|
567
|
+
this._byClient.set(json.s, byClient)
|
|
568
|
+
}
|
|
569
|
+
byClient.add(id)
|
|
570
|
+
added.push(sub)
|
|
571
|
+
}
|
|
572
|
+
// the latest declaration speaks for the subscription
|
|
573
|
+
sub.v = typeof json.v === 'number' ? json.v : 0
|
|
574
|
+
if (labels[id] !== undefined) sub.label = labels[id]
|
|
575
|
+
}
|
|
576
|
+
return added
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Greets new subscriptions of a live registration that can greet: its
|
|
581
|
+
* `greet` gets one handler per handler position, reaching the new
|
|
582
|
+
* subscription alone (a no-op where it holds no position). A greeting that
|
|
583
|
+
* throws or rejects is reported (`greetFailed`); the subscription stays.
|
|
584
|
+
*/
|
|
585
|
+
_greet (reg, subs) {
|
|
586
|
+
const { registration } = reg
|
|
587
|
+
if (reg.state !== LIVE || !registration || !isFunction(registration.greet)) {
|
|
588
|
+
return
|
|
589
|
+
}
|
|
590
|
+
for (const sub of subs) {
|
|
591
|
+
const fail = error =>
|
|
592
|
+
this._emit('greetFailed', {
|
|
593
|
+
context: reg.contextName,
|
|
594
|
+
function: reg.func,
|
|
595
|
+
args: reg.args,
|
|
596
|
+
error
|
|
597
|
+
})
|
|
598
|
+
const handlers = reg.positions.map(position =>
|
|
599
|
+
position === sub.position ? this._direct(reg, sub) : () => {}
|
|
600
|
+
)
|
|
601
|
+
try {
|
|
602
|
+
const result = registration.greet(...handlers)
|
|
603
|
+
if (isPromise(result)) result.then(null, fail)
|
|
604
|
+
} catch (err) {
|
|
605
|
+
fail(err)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// a handler that reaches one subscription alone, while it is held
|
|
611
|
+
_direct (reg, sub) {
|
|
612
|
+
return (...innerArgs) => {
|
|
613
|
+
if (reg.state >= SILENT || !reg.speaks()) return
|
|
614
|
+
if (sub.position.subs.get(sub.id) !== sub) return
|
|
615
|
+
this._publish({
|
|
616
|
+
a: sub.isCallAll ? [reg.contextName, ...innerArgs] : innerArgs,
|
|
617
|
+
i: sub.id,
|
|
618
|
+
receiverVersion: sub.v
|
|
619
|
+
})
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
static _holdKey (sub) {
|
|
624
|
+
return `${sub.contextKey}\u0000${sub.reg.key}`
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// what an id holds on a context: one registration's subscription (more
|
|
628
|
+
// only for clients before protocol 4)
|
|
629
|
+
_subscriptionsOn (id, contextKey) {
|
|
630
|
+
const held = this._held.get(id)
|
|
631
|
+
if (!held) return []
|
|
632
|
+
return Array.from(held.values()).filter(sub => sub.contextKey === contextKey)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
_subscriptionsOf (reg) {
|
|
636
|
+
const subs = []
|
|
637
|
+
for (const position of reg.positions) {
|
|
638
|
+
for (const sub of position.subs.values()) subs.push(sub)
|
|
639
|
+
}
|
|
640
|
+
return subs
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
_withdraw (id, clientId, contextKey) {
|
|
644
|
+
const held = this._held.get(id)
|
|
645
|
+
if (!held) return 0
|
|
646
|
+
let count = 0
|
|
647
|
+
for (const sub of Array.from(held.values())) {
|
|
648
|
+
if (contextKey !== undefined && sub.contextKey !== contextKey) continue
|
|
649
|
+
if (sub.clientId !== clientId) continue
|
|
650
|
+
this._remove(sub)
|
|
651
|
+
count++
|
|
652
|
+
}
|
|
653
|
+
return count
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* A remover call carrying subscription ids (legacy) withdraws what it
|
|
658
|
+
* names: the subscription held on the registration of its twin event
|
|
659
|
+
* function with the remover's own arguments (offValue('x', id) undoes
|
|
660
|
+
* onValue('x', id), not onValue('y', id)), else every one the id holds
|
|
661
|
+
* on that twin function on the context (offValue(id)), or on any
|
|
662
|
+
* function when the remover names none.
|
|
663
|
+
*/
|
|
664
|
+
_withdrawByRemover (func, keyArgs, ids, clientId, contextKey) {
|
|
665
|
+
const twins = twinsOf(func)
|
|
666
|
+
const named = new Set(
|
|
667
|
+
(twins || []).map(twin => `${twin}\u0000${canonical(keyArgs)}`)
|
|
668
|
+
)
|
|
669
|
+
for (const id of ids) {
|
|
670
|
+
const held = this._subscriptionsOn(id, contextKey).filter(
|
|
671
|
+
sub =>
|
|
672
|
+
sub.clientId === clientId &&
|
|
673
|
+
(twins === null || twins.includes(sub.reg.func))
|
|
674
|
+
)
|
|
675
|
+
const exact = held.filter(sub => named.has(sub.reg.key))
|
|
676
|
+
for (const sub of exact.length > 0 ? exact : held) this._remove(sub)
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// takes a subscription out; the last one out releases its registration
|
|
681
|
+
_remove (sub) {
|
|
682
|
+
this._forget(sub)
|
|
683
|
+
const { reg } = sub
|
|
684
|
+
for (const position of reg.positions) {
|
|
685
|
+
if (position.subs.size > 0) return
|
|
686
|
+
}
|
|
687
|
+
this._lastOut(reg)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// drops a subscription from every index (its registration decides apart)
|
|
691
|
+
_forget (sub) {
|
|
692
|
+
sub.position.subs.delete(sub.id)
|
|
693
|
+
const held = this._held.get(sub.id)
|
|
694
|
+
const holdKey = Registrations._holdKey(sub)
|
|
695
|
+
if (held && held.get(holdKey) === sub) {
|
|
696
|
+
held.delete(holdKey)
|
|
697
|
+
if (held.size === 0) {
|
|
698
|
+
this._held.delete(sub.id)
|
|
699
|
+
const byClient = this._byClient.get(sub.clientId)
|
|
700
|
+
if (byClient) {
|
|
701
|
+
byClient.delete(sub.id)
|
|
702
|
+
if (byClient.size === 0) this._byClient.delete(sub.clientId)
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
_lastOut (reg) {
|
|
709
|
+
// a pending first call decides once it settled
|
|
710
|
+
if (reg.state !== LIVE) return
|
|
711
|
+
if (Registrations._canRelease(reg)) {
|
|
712
|
+
this._unlink(reg)
|
|
713
|
+
this._release(reg)
|
|
714
|
+
} else {
|
|
715
|
+
// no way to release: kept, silent, and reused by the next subscriber
|
|
716
|
+
reg.state = SILENT
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
_settle (reg, value) {
|
|
721
|
+
// an emitter answering with itself (Node's `on`, a chaining wrapper)
|
|
722
|
+
// returns no registration, even when it can be disposed as an instance
|
|
723
|
+
const registration =
|
|
724
|
+
!reg.nodeEmitter && value !== reg.target && isRegistration(value)
|
|
725
|
+
if (reg.state !== PENDING) {
|
|
726
|
+
// forgotten meanwhile (the instance ended): nobody owns it any more
|
|
727
|
+
if (registration) {
|
|
728
|
+
reg.registration = value
|
|
729
|
+
this._release(reg)
|
|
730
|
+
}
|
|
731
|
+
return
|
|
732
|
+
}
|
|
733
|
+
if (registration) {
|
|
734
|
+
reg.registration = value
|
|
735
|
+
// what the call answered before registrations (a node id, a
|
|
736
|
+
// subscription id) rides along as `value`; the object itself never
|
|
737
|
+
// travels
|
|
738
|
+
reg.r = 'value' in value ? sanitize(value.value) : true
|
|
739
|
+
const { ended } = value
|
|
740
|
+
if (isPromise(ended)) {
|
|
741
|
+
ended.then(
|
|
742
|
+
result =>
|
|
743
|
+
this._ended(reg, {
|
|
744
|
+
reason: typeof result === 'string' && result ? result : 'ended',
|
|
745
|
+
by: 'source'
|
|
746
|
+
}),
|
|
747
|
+
err =>
|
|
748
|
+
this._ended(reg, {
|
|
749
|
+
reason: (err && err.message) || String(err),
|
|
750
|
+
by: 'source',
|
|
751
|
+
failed: true
|
|
752
|
+
})
|
|
753
|
+
)
|
|
754
|
+
}
|
|
755
|
+
} else {
|
|
756
|
+
reg.r = sanitize(value)
|
|
757
|
+
}
|
|
758
|
+
reg.state = LIVE
|
|
759
|
+
const subs = this._subscriptionsOf(reg)
|
|
760
|
+
if (subs.length === 0) this._lastOut(reg)
|
|
761
|
+
else this._greet(reg, subs)
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// the first call failed: every subscriber learns it, nothing is kept
|
|
765
|
+
_fail (reg) {
|
|
766
|
+
this._unlink(reg)
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// takes a registration out of the book; its listeners fall silent
|
|
770
|
+
_unlink (reg) {
|
|
771
|
+
reg.state = GONE
|
|
772
|
+
const regs = this._contexts.get(reg.contextKey)
|
|
773
|
+
if (regs && regs.get(reg.key) === reg) {
|
|
774
|
+
regs.delete(reg.key)
|
|
775
|
+
if (regs.size === 0) this._contexts.delete(reg.contextKey)
|
|
776
|
+
}
|
|
777
|
+
for (const sub of this._subscriptionsOf(reg)) this._forget(sub)
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
_ended (reg, end) {
|
|
781
|
+
if (reg.state === GONE) return
|
|
782
|
+
const subs = this._subscriptionsOf(reg)
|
|
783
|
+
this._unlink(reg)
|
|
784
|
+
this._release(reg)
|
|
785
|
+
for (const sub of subs) {
|
|
786
|
+
if (!(sub.v >= NOTICE_VERSION)) continue
|
|
787
|
+
this._publish({
|
|
788
|
+
i: sub.id,
|
|
789
|
+
end: { ...end, context: reg.contextName }
|
|
790
|
+
})
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// whether the agent knows how to undo the registration's call
|
|
795
|
+
static _canRelease (reg) {
|
|
796
|
+
const twin = releaseTwinOf(reg.func)
|
|
797
|
+
return (
|
|
798
|
+
!!reg.registration ||
|
|
799
|
+
reg.nodeEmitter ||
|
|
800
|
+
(twin !== null && isFunction(reg.target[twin])) ||
|
|
801
|
+
isFunction(reg.target.removeListener)
|
|
802
|
+
)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// undoes the call: the registration's dispose, Node's removeListener for
|
|
806
|
+
// Node's own `on`, else (deprecated) the class's twin of the event function
|
|
807
|
+
// with the same arguments, or its removeListener
|
|
808
|
+
_release (reg) {
|
|
809
|
+
const fail = error =>
|
|
810
|
+
this._emit('releaseFailed', {
|
|
811
|
+
context: reg.contextName,
|
|
812
|
+
function: reg.func,
|
|
813
|
+
args: reg.args,
|
|
814
|
+
error
|
|
815
|
+
})
|
|
816
|
+
try {
|
|
817
|
+
const { registration, target } = reg
|
|
818
|
+
if (registration) {
|
|
819
|
+
reg.registration = null
|
|
820
|
+
if (isFunction(registration[Symbol.asyncDispose])) {
|
|
821
|
+
const result = registration[Symbol.asyncDispose]()
|
|
822
|
+
if (isPromise(result)) result.then(null, fail)
|
|
823
|
+
} else {
|
|
824
|
+
registration[Symbol.dispose]()
|
|
825
|
+
}
|
|
826
|
+
} else if (reg.nodeEmitter) {
|
|
827
|
+
for (const { fanout } of reg.positions) {
|
|
828
|
+
target.removeListener(reg.event, fanout)
|
|
829
|
+
}
|
|
830
|
+
} else if (isFunction(target[releaseTwinOf(reg.func)])) {
|
|
831
|
+
const result = target[releaseTwinOf(reg.func)](...reg.unwrapped)
|
|
832
|
+
if (isPromise(result)) result.then(null, fail)
|
|
833
|
+
} else if (isFunction(target.removeListener)) {
|
|
834
|
+
for (const { fanout } of reg.positions) {
|
|
835
|
+
target.removeListener(reg.event, fanout)
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
} catch (err) {
|
|
839
|
+
fail(err)
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
Registrations.isRegistration = isRegistration
|
|
845
|
+
Registrations.isRemover = isRemover
|
|
846
|
+
Registrations.canonical = canonical
|
|
847
|
+
Registrations.sanitize = sanitize
|
|
848
|
+
Registrations.NOTICE_VERSION = NOTICE_VERSION
|
|
849
|
+
|
|
850
|
+
module.exports = Registrations
|