@webqit/port-plus 0.1.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,829 @@
1
+ import { _wq as $wq } from '@webqit/util/js/index.js';
2
+ import { _isObject, _isTypeObject } from '@webqit/util/js/index.js';
3
+ import { MessageEventPlus } from './MessageEventPlus.js';
4
+ import Observer from '@webqit/observer';
5
+
6
+ export const _wq = (target, ...args) => $wq(target, 'port+', ...args);
7
+ export const _options = (target) => $wq(target, 'port+', 'meta').get('options') || {};
8
+ export const _meta = (target) => $wq(target, 'port+', 'meta');
9
+
10
+ const portPlusMethods = [
11
+ 'addEventListener',
12
+ 'addRequestListener',
13
+ 'postMessage',
14
+ 'postRequest',
15
+ 'dispatchEvent',
16
+ 'forwardPort',
17
+ 'start',
18
+ 'readyStateChange',
19
+ 'removeEventListener',
20
+ 'close',
21
+ ];
22
+ const portPlusProps = [
23
+ 'options',
24
+ 'readyState',
25
+ 'onmessage',
26
+ 'onmessageerror',
27
+ ];
28
+
29
+ export class MessagePortPlus extends MessagePortPlusMixin(EventTarget) {
30
+
31
+ constructor(options = {}) {
32
+ super();
33
+ const portPlusMeta = _wq(this, 'meta');
34
+ portPlusMeta.set('options', options);
35
+ }
36
+
37
+ static [Symbol.hasInstance](instance) {
38
+ // Direct subclass?
39
+ if (Function.prototype[Symbol.hasInstance].call(this, instance)) return true;
40
+ // Duct-type checking
41
+ return portPlusMethods.every((m) => typeof instance[m] === 'function')
42
+ || portPlusProps.every((m) => m in instance);
43
+ }
44
+ }
45
+
46
+ export function MessagePortPlusMixin(superClass) {
47
+ return class extends superClass {
48
+
49
+ static upgradeInPlace(port) {
50
+ if (port instanceof MessagePortPlus) {
51
+ return port;
52
+ }
53
+
54
+ const proto = this.prototype;
55
+
56
+ for (const prop of portPlusMethods) {
57
+ const original = port[prop];
58
+ const plus = proto[prop];
59
+
60
+ if (original) Object.defineProperty(port, `_${prop}`, { value: original.bind(port), configurable: true });
61
+ Object.defineProperty(port, prop, { value: plus.bind(port), configurable: true });
62
+ }
63
+
64
+ for (const prop of portPlusProps) {
65
+ const original = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(port), prop);
66
+ const plus = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(proto), prop);
67
+
68
+ if (original) Object.defineProperty(port, `_${prop}`, { ...original, configurable: true });
69
+ Object.defineProperty(port, prop, { ...plus, configurable: true });
70
+ }
71
+
72
+ this.upgradeEvents(port);
73
+ }
74
+
75
+ static upgradeEvents(port, portPlus = null) {
76
+ if (!portPlus) portPlus = port;
77
+
78
+ const rawPortMeta = _meta(port);
79
+ if (rawPortMeta.get('events+')) return; // Already wrapped
80
+
81
+ const portPlusMeta = _meta(portPlus);
82
+ const options = _options(portPlus);
83
+
84
+ const garbageCollection = getGarbageCollection.call(portPlus);
85
+ const readyStateInternals = getReadyStateInternals.call(portPlus);
86
+
87
+ // ------------- SETUP
88
+
89
+ if (port instanceof BroadcastChannel) {
90
+ if (options.clientServerMode === 'server') {
91
+ portPlusMeta.set('clients', new Set);
92
+ } else if (options.clientServerMode === 'client') {
93
+ portPlusMeta.set('client_id', `client-${(0 | Math.random() * 9e6).toString(36)}`);
94
+ }
95
+ }
96
+
97
+ // ------------- MESSAGE
98
+
99
+ const messageHandler = (e) => {
100
+ if (e instanceof MessageEventPlus) return;
101
+ // Stop propagation if the event for portPlus
102
+ // since we'll repost it there
103
+ if (port === portPlus) e.stopImmediatePropagation?.();
104
+ // Hydrate event. Typically a WebSocket-specific feature
105
+ e = this._hydrateMessage?.(portPlus, e) || e;
106
+
107
+ /*
108
+ * Handle lifecycle events from the other end
109
+ */
110
+
111
+ if (e.data.ping === 'connect'
112
+ && typeof e.data?.['.wq']?.eventID === 'string'
113
+ && (!(port instanceof WebSocket) || !options.naturalOpen)) {
114
+ // This is a special ping from a MessagePort or BroadcastChannel instance
115
+ // that helps us simulate an "open" event
116
+ // If !options.naturalOpen, WebSockets too
117
+
118
+ let reply = true;
119
+
120
+ if (port instanceof BroadcastChannel) {
121
+ if (options.clientServerMode === 'server'
122
+ && typeof e.data.id === 'string') {
123
+ portPlusMeta.get('clients').add(e.data.id);
124
+ reply = 'server';
125
+ } else if (e.data.id === 'server'
126
+ && portPlusMeta.has('client_id')) {
127
+ reply = portPlusMeta.get('client_id');
128
+ }
129
+ }
130
+
131
+ e.ports?.forEach((p) => p.postMessage(reply));
132
+
133
+ portPlusMeta.set('remote.start.called', true);
134
+ portPlus.start();
135
+ return;
136
+ }
137
+
138
+ if (e.data.ping === 'disconnect'
139
+ && typeof e.data?.['.wq']?.eventID === 'string') {
140
+ // This is a special ping from a BroadcastChannel instance
141
+ // that helps us simulate a "close" event
142
+ // WebSockets naturally fire a close event
143
+ // In nodejs only, MessagePort naturally fire a close event
144
+ // so we need to support simulated closing for when in the browser
145
+
146
+ const close = () => {
147
+ portPlusMeta.set('remote.close.called', true);
148
+ portPlus.close();
149
+ };
150
+
151
+ if (port instanceof BroadcastChannel) {
152
+ if (options.clientServerMode === 'server'
153
+ && typeof e.data.id === 'string') {
154
+ // Expel a client
155
+ portPlusMeta.get('clients').delete(e.data.id);
156
+ // Declare closed if no clients remain
157
+ if (!portPlusMeta.get('clients').size
158
+ && options.autoClose
159
+ && !readyStateInternals.close.state) {
160
+ close();
161
+ }
162
+ } else if (options.clientServerMode === 'client'
163
+ && e.data.id === 'server') {
164
+ // Server has closed
165
+ close();
166
+ }
167
+ } else if (port instanceof MessagePort) {
168
+ close();
169
+ }
170
+
171
+ return;
172
+ }
173
+
174
+ /*
175
+ * Do event rewrites and the Webflo live object magic
176
+ */
177
+
178
+ let message = e.data;
179
+ let wqOptions = {};
180
+ if (typeof e.data?.['.wq']?.eventID === 'string') {
181
+ ({ message, ['.wq']: wqOptions } = e.data);
182
+ }
183
+
184
+ const eventPlus = new MessageEventPlus(message, {
185
+ originalTarget: port,
186
+ ...wqOptions,
187
+ ports: e.ports,
188
+ });
189
+
190
+ portPlus.dispatchEvent(eventPlus);
191
+ };
192
+
193
+ // ------------- OPEN
194
+
195
+ const openHandler = (e) => {
196
+ // Native "open" event fired by WebSocket
197
+ if (port instanceof WebSocket
198
+ && options.naturalOpen
199
+ && !(e instanceof MessageEventPlus)) {
200
+ portPlusMeta.set('remote.start.called', true);
201
+ portPlus.start();
202
+ }
203
+ };
204
+
205
+ // ------------- CLOSE
206
+
207
+ const closeHandler = (e) => {
208
+ // Native "close" events fired by MessagePort and WebSocket
209
+ if ((port instanceof WebSocket || port instanceof MessagePort)
210
+ && !(e instanceof MessageEventPlus)) {
211
+ portPlusMeta.set('remote.close.called', true);
212
+ portPlus.close();
213
+ }
214
+ };
215
+
216
+ rawPortMeta.set('internal_call', true);
217
+ port.addEventListener('message', messageHandler);
218
+ port.addEventListener('error', messageHandler);
219
+ port.addEventListener('open', openHandler);
220
+ port.addEventListener('close', closeHandler);
221
+ rawPortMeta.delete('internal_call');
222
+
223
+ rawPortMeta.set('events+', true);
224
+
225
+ garbageCollection.add(() => {
226
+ port.removeEventListener('message', messageHandler);
227
+ port.removeEventListener('error', messageHandler);
228
+ port.removeEventListener('open', openHandler);
229
+ port.removeEventListener('close', closeHandler);
230
+
231
+ rawPortMeta.set('events+', false);
232
+ });
233
+ }
234
+
235
+ get options() { return { ..._options(this) }; }
236
+
237
+ // Messages
238
+
239
+ get onmessageerror() {
240
+ if (typeof super.onmessageerror !== 'undefined') {
241
+ return super.onmessageerror;
242
+ }
243
+ if (typeof this._onmessageerror !== 'undefined') {
244
+ return this._onmessageerror;
245
+ }
246
+ return null;
247
+ }
248
+
249
+ set onmessageerror(v) {
250
+ if (typeof super.onmessageerror !== 'undefined') {
251
+ super.onmessageerror = v;
252
+ return;
253
+ }
254
+
255
+ if (v !== null && typeof v !== 'function') {
256
+ throw new TypeError('onmessageerror must be a function');
257
+ }
258
+
259
+ if (Object.getOwnPropertyDescriptor(this, '_onmessageerror')?.set) {
260
+ this._onmessageerror = v;
261
+ return;
262
+ }
263
+
264
+ if (this._onmessageerror) this.removeEventListener('messageerror', this._onmessageerror);
265
+ this.addEventListener('messageerror', v);
266
+ this._onmessageerror = v;
267
+ }
268
+
269
+ get onmessage() {
270
+ if (typeof super.onmessage !== 'undefined') {
271
+ return super.onmessage;
272
+ }
273
+ if (typeof this._onmessage !== 'undefined') {
274
+ return this._onmessage;
275
+ }
276
+ return null;
277
+ }
278
+
279
+ set onmessage(v) {
280
+ // Auto-start?
281
+ const portPlusMeta = _meta(this);
282
+ const options = _options(this);
283
+ if (!portPlusMeta.get('internal_call')
284
+ && options.autoStart) {
285
+ this.start();
286
+ }
287
+
288
+ if (typeof super.onmessage !== 'undefined') {
289
+ super.onmessage = v;
290
+ return;
291
+ }
292
+
293
+ if (v !== null && typeof v !== 'function') {
294
+ throw new TypeError('onmessage must be a function');
295
+ }
296
+
297
+ if (Object.getOwnPropertyDescriptor(this, '_onmessage')?.set) {
298
+ this._onmessage = v;
299
+ return;
300
+ }
301
+
302
+ if (this._onmessage) this.removeEventListener('message', this._onmessage);
303
+ this.addEventListener('message', v);
304
+ this._onmessage = v;
305
+ }
306
+
307
+ addEventListener(...args) {
308
+ // Auto-start?
309
+ const portPlusMeta = _meta(this);
310
+ const options = _options(this);
311
+ if (!portPlusMeta.get('internal_call')
312
+ && options.autoStart) {
313
+ this.start();
314
+ }
315
+
316
+ // Add to registry
317
+ const garbageCollection = getGarbageCollection.call(this);
318
+ garbageCollection.add(() => {
319
+ this._removeEventListener
320
+ ? this._removeEventListener(...args)
321
+ : super.removeEventListener(...args);
322
+ });
323
+
324
+ // Execute addEventListener()
325
+ return this._addEventListener
326
+ ? this._addEventListener(...args)
327
+ : super.addEventListener(...args);
328
+ }
329
+
330
+ dispatchEvent(event) {
331
+ const returnValue = this._dispatchEvent
332
+ ? this._dispatchEvent(event)
333
+ : super.dispatchEvent(event);
334
+ if (event instanceof MessageEventPlus) {
335
+ // Bubble semantics
336
+ propagateEvent.call(this, event);
337
+ }
338
+ return returnValue;
339
+ }
340
+
341
+ postMessage(message, transferOrOptions = {}) {
342
+ // Auto-start?
343
+ const portPlusMeta = _meta(this);
344
+ const options = _options(this);
345
+ if (!portPlusMeta.get('internal_call')
346
+ && options.autoStart) {
347
+ this.start();
348
+ }
349
+
350
+ // Update readyState
351
+ const readyStateInternals = getReadyStateInternals.call(this);
352
+ readyStateInternals.messaging.state = true;
353
+ readyStateInternals.messaging.resolve();
354
+
355
+ // Format payload if not yet in the ['.wq'] format
356
+ let _relayedFrom;
357
+ if (!_isObject(message?.['.wq'])) {
358
+ const { portOptions, wqOptions: { relayedFrom, ...wqOptions } } = preProcessPostMessage.call(this, message, transferOrOptions);
359
+ message = { message, ['.wq']: wqOptions };
360
+ transferOrOptions = portOptions;
361
+ _relayedFrom = relayedFrom;
362
+ }
363
+
364
+ // Exec
365
+ const post = () => {
366
+ this._postMessage
367
+ ? this._postMessage(message, transferOrOptions, _relayedFrom)
368
+ : super.postMessage(message, transferOrOptions);
369
+ };
370
+
371
+ // If client-server mode, wait for open ready state
372
+ if (options.postAwaitsOpen) readyStateInternals.open.promise.then(post);
373
+ else post();
374
+ }
375
+
376
+ // Requests
377
+
378
+ addRequestListener(type, handler, options = {}) {
379
+ const handle = async (e) => {
380
+ const response = await handler(e);
381
+ for (const port of e.ports) {
382
+ port.postMessage(response);
383
+ }
384
+ };
385
+ this.addEventListener(type, handle, options);
386
+ }
387
+
388
+ postRequest(data, callback, options = {}) {
389
+ let returnValue;
390
+
391
+ if (_isObject(callback)) {
392
+ options = { once: true, ...callback };
393
+ returnValue = new Promise((resolve) => {
394
+ callback = resolve;
395
+ });
396
+ }
397
+
398
+ const messageChannel = new MessageChannel;
399
+ this.constructor.upgradeEvents(messageChannel.port1);
400
+ messageChannel.port1.start();
401
+
402
+ const { signal = null, once = false, transfer = [], ..._options } = options;
403
+
404
+ messageChannel.port1.addEventListener('message', (e) => callback(e), { signal, once });
405
+ signal?.addEventListener('abort', () => {
406
+ messageChannel.port1.close();
407
+ messageChannel.port2.close();
408
+ });
409
+
410
+ this.postMessage(data, { ..._options, transfer: [messageChannel.port2].concat(transfer) });
411
+ return returnValue;
412
+ }
413
+
414
+ // Forwarding
415
+
416
+ forwardPort(eventTypes, targetPort, { resolveData = null, bidirectional = false, namespace1 = null, namespace2 = null } = {}) {
417
+ if (!(this instanceof MessagePortPlus) || !(targetPort instanceof MessagePortPlus)) {
418
+ throw new Error('Both ports must be instance of MessagePortPlus.');
419
+ }
420
+ if (typeof eventTypes !== 'function' && !(eventTypes = [].concat(eventTypes)).length) {
421
+ throw new Error('Event types must be specified.');
422
+ }
423
+
424
+ const downstreamRegistry = getDownstreamRegistry.call(this);
425
+ const registration = { targetPort, eventTypes, options: { resolveData, namespace1, namespace2 } };
426
+ downstreamRegistry.add(registration);
427
+
428
+ let cleanup2;
429
+ if (bidirectional) {
430
+ cleanup2 = this.forwardPort.call(
431
+ targetPort,
432
+ typeof eventTypes === 'function' ? eventTypes : eventTypes.filter((s) => s !== 'close'),
433
+ this,
434
+ { resolveData, bidirectional: false, namespace1: namespace2, namespace2: namespace1 }
435
+ );
436
+ }
437
+
438
+ return () => {
439
+ downstreamRegistry.delete(registration);
440
+ cleanup2?.();
441
+ };
442
+ }
443
+
444
+ // Lifecycle
445
+
446
+ get readyState() {
447
+ const readyStateInternals = getReadyStateInternals.call(this);
448
+ return readyStateInternals.close.state ? 'closed'
449
+ : (readyStateInternals.open.state ? 'open' : 'connecting');
450
+ }
451
+
452
+ readyStateChange(query) {
453
+ if (!['open', 'messaging', 'error', 'close'].includes(query)) {
454
+ throw new Error(`Invalid readyState query "${query}"`);
455
+ }
456
+ const readyStateInternals = getReadyStateInternals.call(this);
457
+ return readyStateInternals[query].promise;
458
+ }
459
+
460
+ start() {
461
+ const readyStateInternals = getReadyStateInternals.call(this);
462
+ if (readyStateInternals.open.state) return;
463
+
464
+ let messageChannel;
465
+
466
+ const readyStateOpen = () => {
467
+ if (readyStateInternals.open.state) return;
468
+ readyStateInternals.open.state = true;
469
+ readyStateInternals.open.resolve();
470
+
471
+ const openEvent = new MessageEventPlus(null, { type: 'open' });
472
+ this._dispatchEvent
473
+ ? this._dispatchEvent(openEvent)
474
+ : super.dispatchEvent(openEvent);
475
+
476
+ messageChannel?.port1.close();
477
+ messageChannel?.port2.close();
478
+ };
479
+
480
+ const portPlusMeta = _meta(this);
481
+ const options = _options(this);
482
+
483
+ if (portPlusMeta.get('remote.start.called')) {
484
+ readyStateOpen();
485
+ return;
486
+ }
487
+
488
+ if (portPlusMeta.get('start.called')) return;
489
+ portPlusMeta.set('start.called', true);
490
+
491
+ this._start
492
+ ? this._start()
493
+ : super.start?.();
494
+
495
+ messageChannel = new MessageChannel;
496
+
497
+ messageChannel.port1.onmessage = (e) => {
498
+ if (this instanceof BroadcastChannel
499
+ && options.clientServerMode === 'server'
500
+ && typeof e.data === 'string') {
501
+ // Register clients that replied
502
+ portPlusMeta.get('clients').add(e.data);
503
+ }
504
+ readyStateOpen();
505
+ };
506
+
507
+ const { wqOptions } = preProcessPostMessage.call(this);
508
+ const id = options.clientServerMode === 'server' ? 'server'
509
+ : (options.clientServerMode === 'client' ? portPlusMeta.get('client_id') : null);
510
+ const pingData = { ['.wq']: wqOptions, ping: 'connect', id };
511
+
512
+ this._postMessage
513
+ ? this._postMessage(pingData, { transfer: [messageChannel.port2] })
514
+ : super.postMessage(pingData, { transfer: [messageChannel.port2] });
515
+ }
516
+
517
+ close(...args) {
518
+ const readyStateInternals = getReadyStateInternals.call(this);
519
+
520
+ if (readyStateInternals.close.state) return;
521
+ readyStateInternals.close.state = true;
522
+
523
+ const portPlusMeta = _meta(this);
524
+ const options = _options(this);
525
+
526
+ if (!portPlusMeta.get('remote.close.called')
527
+ && (this instanceof BroadcastChannel || this instanceof MessagePort)) {
528
+
529
+ const { wqOptions } = preProcessPostMessage.call(this);
530
+ const id = options.clientServerMode === 'server' ? 'server'
531
+ : (options.clientServerMode === 'client' ? portPlusMeta.get('client_id') : null);
532
+ const pingData = { ['.wq']: wqOptions, ping: 'disconnect', id };
533
+
534
+ this._postMessage
535
+ ? this._postMessage(pingData)
536
+ : super.postMessage(pingData);
537
+ }
538
+
539
+ // This should come before event:
540
+ // Nodejs natively sends a close event to other port (port2) for MessagePort
541
+ // this ensures:
542
+ // port2.ping() -> port2.close() -> port2.customCloseEvent()
543
+ // -> port1.customCloseEvent()
544
+ this._close
545
+ ? this._close(...args)
546
+ : super.close(...args);
547
+
548
+ readyStateInternals.close.resolve();
549
+
550
+ const openEvent = new MessageEventPlus(null, { type: 'close' });
551
+ this._dispatchEvent
552
+ ? this._dispatchEvent(openEvent)
553
+ : super.dispatchEvent(openEvent);
554
+
555
+ garbageCollect.call(this);
556
+ }
557
+ };
558
+ }
559
+
560
+ export function MessagePortPlusMockPortsMixin(superClass) {
561
+ return class extends MessagePortPlusMixin(superClass) {
562
+
563
+ static _hydrateMessage(portPlus, event) {
564
+ // But only one with ['.wq'].numPorts & ['.wq'].eventID is valid for port hydration
565
+ if (typeof event.data?.['.wq']?.numPorts !== 'number' || typeof event.data['.wq'].eventID !== 'string') {
566
+ return event;
567
+ }
568
+
569
+ const garbageCollection = getGarbageCollection.call(portPlus);
570
+ const numPorts = event.data['.wq'].numPorts;
571
+ Object.defineProperty(event, 'ports', { value: [], configurable: true });
572
+
573
+ for (let i = 0; i < numPorts; i++) {
574
+ const channel = new MessageChannel;
575
+ channel.port1.start();
576
+
577
+ MessagePortPlus.upgradeInPlace(channel.port1);
578
+ garbageCollection.add(portPlus.forwardPort('*', channel.port1, { bidirectional: true, namespace1: `${event.data['.wq'].eventID}:${i}` }));
579
+ event.ports.push(channel.port2);
580
+
581
+ portPlus.readyStateChange('close').then(() => {
582
+ channel.port1.close();
583
+ channel.port2.close();
584
+ });
585
+ }
586
+
587
+ return event;
588
+ }
589
+
590
+ _postMessage(payload, portOptions = {}) {
591
+ const { transfer = [], ..._portOptions } = portOptions;
592
+
593
+ if (typeof payload?.['.wq']?.eventID === 'string') {
594
+ const garbageCollection = getGarbageCollection.call(this);
595
+
596
+ const messagePorts = transfer.filter((t) => t instanceof MessagePort);
597
+ const numPorts = messagePorts.length;
598
+
599
+ for (let i = 0; i < numPorts; i++) {
600
+ MessagePortPlus.upgradeInPlace(messagePorts[i]);
601
+ garbageCollection.add(this.forwardPort('*', messagePorts[i], { bidirectional: true, namespace1: `${payload['.wq'].eventID}:${i}` }));
602
+ }
603
+
604
+ payload['.wq'].numPorts = numPorts; // IMPORTANT: numPorts must be set before ports are added
605
+ }
606
+
607
+ return this.__postMessage(payload, _portOptions);
608
+ }
609
+ };
610
+ }
611
+
612
+ export function propagateEvent(event) {
613
+ if (event.propagationStopped) return;
614
+ const portMeta = _meta(this);
615
+
616
+ if (portMeta.get('parentNode') instanceof EventTarget && (
617
+ event.bubbles
618
+ || portMeta.get('parentNode')?.findPort?.((port) => port === this) && event instanceof MessageEventPlus)
619
+ ) {
620
+ portMeta.get('parentNode').dispatchEvent(event);
621
+ // "parentNode" is typically a StarPort feature
622
+ // in case "this" is a RelayPort
623
+ }
624
+
625
+ const downstreamRegistry = getDownstreamRegistry.call(this);
626
+ if (!downstreamRegistry.size) return;
627
+
628
+ if (event instanceof MessageEventPlus) {
629
+ const { type, eventID, data, live, bubbles, ports } = event;
630
+
631
+ const called = new WeakSet;
632
+ for (const { targetPort, eventTypes, options } of downstreamRegistry) {
633
+ if (called.has(targetPort)) continue;
634
+
635
+ let $type = type;
636
+ if (options.namespace1) {
637
+ [, $type] = (new RegExp(`^${options.namespace1.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:([^:]+)$`)).exec(type) || [];
638
+ if (!$type) continue;
639
+ }
640
+
641
+ const matches = typeof eventTypes === 'function'
642
+ ? eventTypes($type, this, targetPort, options)
643
+ : [].concat(eventTypes).find((t) => {
644
+ return t === $type || t === '*';
645
+ });
646
+
647
+ if (!matches) continue;
648
+ called.add(targetPort);
649
+
650
+ targetPort.postMessage(options.resolveData ? options.resolveData(data, this, targetPort, options) : data, {
651
+ transfer: ports,
652
+ type: options.namespace2 ? `${options.namespace2}:${$type}` : $type,
653
+ eventID,
654
+ bubbles,
655
+ live,
656
+ forwarded: true,
657
+ relayedFrom: this
658
+ // "relayedFrom" is typically a RelayPort feature
659
+ // in case targetPort is a RelayPort
660
+ });
661
+ }
662
+ }
663
+ }
664
+
665
+ export function getGarbageCollection() {
666
+ const portPlusMeta = _meta(this);
667
+ if (!portPlusMeta.has('garbage_collection')) {
668
+ portPlusMeta.set('garbage_collection', new Set);
669
+ }
670
+ return portPlusMeta.get('garbage_collection');
671
+ }
672
+
673
+ export function getDownstreamRegistry() {
674
+ const portPlusMeta = _meta(this);
675
+ if (!portPlusMeta.has('downstream_registry')) {
676
+ portPlusMeta.set('downstream_registry', new Set);
677
+ }
678
+ return portPlusMeta.get('downstream_registry');
679
+ }
680
+
681
+ export function getReadyStateInternals() {
682
+ const portPlusMeta = _meta(this);
683
+ if (!portPlusMeta.has('readystate_registry')) {
684
+ const $ref = (o) => {
685
+ o.promise = new Promise((resolve) => o.resolve = resolve);
686
+ return o;
687
+ };
688
+ portPlusMeta.set('readystate_registry', {
689
+ open: $ref({}),
690
+ messaging: $ref({}),
691
+ error: $ref({}),
692
+ close: $ref({}),
693
+ });
694
+ }
695
+ return portPlusMeta.get('readystate_registry');
696
+ }
697
+
698
+ export function garbageCollect() {
699
+ const portPlusMeta = _meta(this);
700
+
701
+ for (const dispose of portPlusMeta.get('garbage_collection') || []) {
702
+ if (dispose instanceof AbortController) {
703
+ dispose.abort();
704
+ } else if (typeof dispose === 'function') dispose();
705
+ }
706
+ portPlusMeta.get('garbage_collection')?.clear();
707
+ portPlusMeta.get('downstream_registry')?.clear();
708
+ }
709
+
710
+ export function preProcessPostMessage(message = undefined, transferOrOptions = {}) {
711
+ if (Array.isArray(transferOrOptions)) {
712
+ transferOrOptions = { transfer: transferOrOptions };
713
+ } else if (!transferOrOptions
714
+ || typeof transferOrOptions !== 'object') {
715
+ throw new TypeError('transferOrOptions must be an array or an object');
716
+ }
717
+
718
+ let {
719
+ type = 'message',
720
+ eventID = null,
721
+ live = false,
722
+ observing = false,
723
+ bubbles = false,
724
+ forwarded = false,
725
+ relayedFrom = null,
726
+ signal = null,
727
+ withArrayMethodDescriptors = false,
728
+ honourDoneMutationFlags = false,
729
+ ...portOptions
730
+ } = transferOrOptions;
731
+
732
+ if (!eventID) eventID = `${type}-${(0 | Math.random() * 9e6).toString(36)}`;
733
+
734
+ if (!observing && !forwarded && _isTypeObject(message) && live && !type?.endsWith('.mutate')) {
735
+ publishMutations.call(this, message, eventID, { signal, withArrayMethodDescriptors, honourDoneMutationFlags });
736
+ observing = true;
737
+ }
738
+
739
+ return {
740
+ portOptions,
741
+ wqOptions: {
742
+ type,
743
+ eventID,
744
+ live,
745
+ observing,
746
+ honourDoneMutationFlags,
747
+ bubbles,
748
+ forwarded,
749
+ relayedFrom
750
+ }
751
+ };
752
+ }
753
+
754
+ export function publishMutations(message, eventID, { signal, withArrayMethodDescriptors = true, honourDoneMutationFlags = false } = {}) {
755
+ if (!_isTypeObject(message)) throw new TypeError('data must be a plain object and not a stream');
756
+ if (typeof eventID !== 'string') throw new TypeError('eventID must be a non-empty string');
757
+
758
+ const mutationHandler = (mutations) => {
759
+ let mutationsDone;
760
+
761
+ if (withArrayMethodDescriptors
762
+ && Array.isArray(mutations[0].target)
763
+ && !mutations[0].argumentsList
764
+ && !['set', 'defineProperty', 'deleteProperty'].includes(mutations[0].operation)) return;
765
+
766
+ this.postMessage(
767
+ mutations.map((m) => {
768
+ mutationsDone = !mutationsDone
769
+ && honourDoneMutationFlags
770
+ && m.detail?.done;
771
+ return { ...m, target: undefined };
772
+ }),
773
+ { type: `${eventID}.mutate` }
774
+ );
775
+
776
+ if (mutationsDone) dispose.abort();
777
+ };
778
+
779
+ const dispose = Observer.observe(message, Observer.subtree(), mutationHandler, { signal, withArrayMethodDescriptors });
780
+ const garbageCollection = getGarbageCollection.call(this);
781
+ garbageCollection.add(dispose);
782
+
783
+ return dispose;
784
+ }
785
+
786
+ export function applyMutations(message, eventID, { signal, honourDoneMutationFlags = false } = {}) {
787
+ if (!_isTypeObject(message)) throw new TypeError('data must be a plain object and not a stream');
788
+ if (typeof eventID !== 'string') throw new TypeError('eventID must be a non-empty string');
789
+
790
+ const messageHandler = (e) => {
791
+ if (!e.data?.length) return;
792
+
793
+ let mutationsDone;
794
+
795
+ Observer.batch(message, () => {
796
+ for (const mutation of e.data) {
797
+ mutationsDone = !mutationsDone
798
+ && honourDoneMutationFlags
799
+ && mutation.detail?.done;
800
+
801
+ if (mutation.argumentsList) {
802
+ const target = !mutation.path.length ? message : Observer.get(message, Observer.path(...mutation.path));
803
+ Observer.proxy(target)[mutation.operation](...mutation.argumentsList);
804
+ continue;
805
+ }
806
+
807
+ if (mutation.key !== 'length'
808
+ || ['set', 'defineProperty', 'deleteProperty'].includes(mutation.operation)) {
809
+ const target = mutation.path.length === 1 ? message : Observer.get(message, Observer.path(...mutation.path.slice(0, -1)));
810
+ if (mutation.type === 'delete') {
811
+ Observer.deleteProperty(target, mutation.key);
812
+ } else {
813
+ Observer.set(target, mutation.key, mutation.value);
814
+ }
815
+ }
816
+ }
817
+ });
818
+
819
+ if (mutationsDone) cleanup();
820
+ };
821
+
822
+ this.addEventListener(`${eventID}.mutate`, messageHandler, { signal });
823
+ const cleanup = () => this.removeEventListener(`${eventID}.mutate`, messageHandler);
824
+
825
+ const garbageCollection = getGarbageCollection.call(this);
826
+ garbageCollection.add(cleanup);
827
+
828
+ return cleanup;
829
+ }