vrpc 3.13.0 → 3.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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