@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.
- package/.github/FUNDING.yml +13 -0
- package/.github/workflows/publish.yml +48 -0
- package/.gitignore +6 -0
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/dist/index.html +51 -0
- package/dist/main.js +2 -0
- package/dist/main.js.map +7 -0
- package/package.json +53 -0
- package/src/BroadcastChannelPlus.js +25 -0
- package/src/MessageChannelPlus.js +16 -0
- package/src/MessageEventPlus.js +94 -0
- package/src/MessagePortPlus.js +829 -0
- package/src/RelayPort.js +50 -0
- package/src/StarPort.js +74 -0
- package/src/WebSocketPort.js +47 -0
- package/src/index.browser.js +2 -0
- package/src/index.js +7 -0
- package/test/basic.test.js +38 -0
|
@@ -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
|
+
}
|