@volter/tabnode 0.5.22 → 0.5.24
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/dist/__sw__.js +174 -111
- package/package.json +1 -1
package/dist/__sw__.js
CHANGED
|
@@ -1,48 +1,125 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Service Worker for Mini WebContainers
|
|
3
3
|
* Intercepts fetch requests and routes them to virtual servers
|
|
4
|
-
* Version:
|
|
4
|
+
* Version: 16 - a host per page: each page's channel, servers and preview are its own
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const DEBUG = false;
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
|
|
9
|
+
// Every page that holds a container's servers is a host of its own: its own
|
|
10
|
+
// channel, its own servers, its own preview. Two pages of one origin, each
|
|
11
|
+
// with its container, share this worker and never each other's ports. A host
|
|
12
|
+
// is keyed by the id of the window that sent `init`.
|
|
13
|
+
const hosts = new Map();
|
|
14
|
+
let latestHost = null;
|
|
15
|
+
// An `init` whose sender is unknown replaces the previous such channel.
|
|
16
|
+
const UNNAMED_HOST = 'unnamed-host';
|
|
17
|
+
// The documents and workers a host's servers made, by client id, so their
|
|
18
|
+
// requests reach the host that served them.
|
|
19
|
+
const clientHosts = new Map();
|
|
20
|
+
// The event a request arrived with, so every route to a server can ask which
|
|
21
|
+
// host it is for without the event being passed along.
|
|
22
|
+
const requestEvents = new WeakMap();
|
|
23
|
+
|
|
24
|
+
function openHost(id, channel) {
|
|
25
|
+
const previous = hosts.get(id);
|
|
26
|
+
if (previous) {
|
|
27
|
+
for (const pending of pendingRequests.values()) {
|
|
28
|
+
if (pending.host === previous && pending.flowControl === 1) pending.fail(new Error('Host connection replaced'));
|
|
29
|
+
}
|
|
30
|
+
for (const upload of controlledUploads) {
|
|
31
|
+
if (upload.host === previous) upload.controller.abort(new Error('Host connection replaced'));
|
|
32
|
+
}
|
|
33
|
+
previous.port.onmessage = null;
|
|
34
|
+
previous.port.close();
|
|
35
|
+
}
|
|
36
|
+
const host = {
|
|
37
|
+
id,
|
|
38
|
+
port: channel,
|
|
39
|
+
registeredPorts: new Set(),
|
|
40
|
+
flowControlledPorts: new Map(),
|
|
41
|
+
primaryPort: null,
|
|
42
|
+
ownDocuments: new Set(),
|
|
43
|
+
};
|
|
44
|
+
hosts.set(id, host);
|
|
45
|
+
latestHost = host;
|
|
46
|
+
void pruneClients();
|
|
47
|
+
return host;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The host a client belongs to: a host's own window, or a document or worker one of its servers made. */
|
|
51
|
+
function ownerOf(clientId) {
|
|
52
|
+
if (!clientId) return null;
|
|
53
|
+
return hosts.get(clientId) ?? hosts.get(clientHosts.get(clientId)) ?? null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The host a request is for. One host is the answer while there is one. With
|
|
58
|
+
* more, the requesting client's own host; else the host that owns the
|
|
59
|
+
* document named by the referrer, which is how a frame's navigation carries
|
|
60
|
+
* its parent; else the only host holding the port; else the latest host.
|
|
61
|
+
*/
|
|
62
|
+
async function hostForEvent(event, port) {
|
|
63
|
+
if (hosts.size === 0) return null;
|
|
64
|
+
if (hosts.size === 1) return hosts.values().next().value;
|
|
65
|
+
const direct = ownerOf(event.clientId);
|
|
66
|
+
if (direct) return direct;
|
|
67
|
+
const referrer = event.request.referrer;
|
|
68
|
+
if (referrer) {
|
|
69
|
+
const all = await self.clients.matchAll({ type: 'all', includeUncontrolled: true });
|
|
70
|
+
const owners = new Set(all.filter((client) => client.url === referrer).map((client) => ownerOf(client.id)).filter(Boolean));
|
|
71
|
+
if (owners.size === 1) return owners.values().next().value;
|
|
72
|
+
}
|
|
73
|
+
if (port !== undefined) {
|
|
74
|
+
const holding = [...hosts.values()].filter((host) => host.registeredPorts.has(port));
|
|
75
|
+
if (holding.length === 1) return holding[0];
|
|
76
|
+
}
|
|
77
|
+
return latestHost;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The host a request is for, found through the event it arrived with; remembers the clients it makes. */
|
|
81
|
+
async function hostForRequest(request, port) {
|
|
82
|
+
const event = requestEvents.get(request);
|
|
83
|
+
const host = event ? await hostForEvent(event, port) : latestHost;
|
|
84
|
+
if (host && event) {
|
|
85
|
+
if (event.resultingClientId) clientHosts.set(event.resultingClientId, host.id);
|
|
86
|
+
if (event.clientId && !hosts.has(event.clientId)) clientHosts.set(event.clientId, host.id);
|
|
87
|
+
}
|
|
88
|
+
return host;
|
|
89
|
+
}
|
|
11
90
|
|
|
12
91
|
// Pending requests waiting for response
|
|
13
92
|
const pendingRequests = new Map();
|
|
14
93
|
let requestId = 0;
|
|
15
94
|
|
|
16
|
-
// Registered virtual server ports
|
|
17
|
-
const registeredPorts = new Set();
|
|
18
95
|
// HTTP response flow control is a transport property, never a path/package
|
|
19
|
-
// heuristic. Legacy ports keep their existing protocol until their host opts
|
|
20
|
-
|
|
96
|
+
// heuristic. Legacy ports keep their existing protocol until their host opts
|
|
97
|
+
// in; each host's `flowControlledPorts` holds what its ports negotiated.
|
|
21
98
|
const controlledUploads = new Set();
|
|
22
99
|
const MAX_STREAM_CHUNK_BYTES = 65536;
|
|
23
100
|
|
|
24
|
-
function updateFlowControl(type, data) {
|
|
101
|
+
function updateFlowControl(host, type, data) {
|
|
25
102
|
if (type === 'server-registered' || type === 'server-unregistered') {
|
|
26
103
|
for (const upload of controlledUploads) {
|
|
27
|
-
if (upload.port === data?.port) upload.controller.abort(new Error('Virtual server registration changed'));
|
|
104
|
+
if (upload.host === host && upload.port === data?.port) upload.controller.abort(new Error('Virtual server registration changed'));
|
|
28
105
|
}
|
|
29
106
|
// A port number is not a listener identity. Rebinding must terminate the
|
|
30
107
|
// old connection, even when the replacement advertises identical limits.
|
|
31
108
|
// Otherwise an already-headed response can wait forever on its old owner.
|
|
32
109
|
for (const pending of pendingRequests.values()) {
|
|
33
|
-
if (pending.flowControl === 1 && pending.port === data?.port)
|
|
110
|
+
if (pending.host === host && pending.flowControl === 1 && pending.port === data?.port)
|
|
34
111
|
pending.fail(new Error(type === 'server-unregistered'
|
|
35
112
|
? 'Virtual server closed' : 'Virtual server registration changed'));
|
|
36
113
|
}
|
|
37
114
|
}
|
|
38
115
|
if (type === 'server-unregistered') {
|
|
39
|
-
flowControlledPorts.delete(data?.port);
|
|
116
|
+
host.flowControlledPorts.delete(data?.port);
|
|
40
117
|
}
|
|
41
118
|
if (type !== 'server-registered' || !data) return;
|
|
42
119
|
if (data.flowControl === 1 && Number.isSafeInteger(data.maxRequestBytes)
|
|
43
120
|
&& data.maxRequestBytes > 0 && data.maxChunkBytes === MAX_STREAM_CHUNK_BYTES) {
|
|
44
|
-
flowControlledPorts.set(data.port, { maxRequestBytes: data.maxRequestBytes, channel:
|
|
45
|
-
} else flowControlledPorts.delete(data.port);
|
|
121
|
+
host.flowControlledPorts.set(data.port, { maxRequestBytes: data.maxRequestBytes, channel: host.port });
|
|
122
|
+
} else host.flowControlledPorts.delete(data.port);
|
|
46
123
|
}
|
|
47
124
|
|
|
48
125
|
/**
|
|
@@ -66,42 +143,35 @@ self.addEventListener('message', (event) => {
|
|
|
66
143
|
DEBUG && console.log('[SW] Received message:', type, 'hasPort in event.ports:', event.ports?.length > 0);
|
|
67
144
|
|
|
68
145
|
// When a MessagePort is transferred, it's in event.ports[0], not event.data.port
|
|
146
|
+
const source = event.source && event.source.id ? hosts.get(event.source.id) : null;
|
|
147
|
+
|
|
69
148
|
if (type === 'init' && event.ports && event.ports[0]) {
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
mainPort.onmessage = null;
|
|
77
|
-
mainPort.close();
|
|
78
|
-
}
|
|
79
|
-
registeredPorts.clear();
|
|
80
|
-
flowControlledPorts.clear();
|
|
81
|
-
primaryPort = null;
|
|
82
|
-
// Existing preview documents/workers retain their virtual origin identity;
|
|
83
|
-
// a channel reconnect must not send their root-relative requests to network.
|
|
84
|
-
mainPort = event.ports[0];
|
|
149
|
+
// A page's channel replaces only that page's: another page's requests
|
|
150
|
+
// and servers are its own. Existing preview documents/workers retain
|
|
151
|
+
// their virtual origin identity; a channel reconnect must not send their
|
|
152
|
+
// root-relative requests to network.
|
|
153
|
+
const mainPort = event.ports[0];
|
|
154
|
+
const host = openHost(event.source && event.source.id ? event.source.id : UNNAMED_HOST, mainPort);
|
|
85
155
|
const initializedPort = mainPort;
|
|
86
156
|
mainPort.onmessage = (message) => {
|
|
87
|
-
if (
|
|
157
|
+
if (hosts.get(host.id) === host && host.port === initializedPort) handleMainMessage(host, message);
|
|
88
158
|
};
|
|
89
|
-
if (data && Array.isArray(data.ownDocuments)) ownDocuments = new Set(data.ownDocuments.map((path) => ownPath(String(path))));
|
|
159
|
+
if (data && Array.isArray(data.ownDocuments)) host.ownDocuments = new Set(data.ownDocuments.map((path) => ownPath(String(path))));
|
|
90
160
|
DEBUG && console.log('[SW] Initialized communication channel with transferred port');
|
|
91
161
|
// Re-claim clients so that pages opened after SW activation get controlled.
|
|
92
162
|
// Without this, controllerchange never fires for late-arriving pages.
|
|
93
163
|
self.clients.claim();
|
|
94
164
|
}
|
|
95
165
|
|
|
96
|
-
if (type === 'server-registered' && data) {
|
|
97
|
-
registeredPorts.add(data.port);
|
|
98
|
-
if (data.primary) primaryPort = data.port;
|
|
166
|
+
if (type === 'server-registered' && data && source) {
|
|
167
|
+
source.registeredPorts.add(data.port);
|
|
168
|
+
if (data.primary) source.primaryPort = data.port;
|
|
99
169
|
DEBUG && console.log(`[SW] Server registered on port ${data.port}`);
|
|
100
170
|
}
|
|
101
171
|
|
|
102
|
-
if (type === 'server-unregistered' && data) {
|
|
103
|
-
registeredPorts.delete(data.port);
|
|
104
|
-
if (primaryPort === data.port) primaryPort = null;
|
|
172
|
+
if (type === 'server-unregistered' && data && source) {
|
|
173
|
+
source.registeredPorts.delete(data.port);
|
|
174
|
+
if (source.primaryPort === data.port) source.primaryPort = null;
|
|
105
175
|
DEBUG && console.log(`[SW] Server unregistered from port ${data.port}`);
|
|
106
176
|
}
|
|
107
177
|
});
|
|
@@ -109,11 +179,13 @@ self.addEventListener('message', (event) => {
|
|
|
109
179
|
/**
|
|
110
180
|
* Handle response messages from main thread
|
|
111
181
|
*/
|
|
112
|
-
function handleMainMessage(event) {
|
|
182
|
+
function handleMainMessage(host, event) {
|
|
113
183
|
const { type, id, data, error } = event.data;
|
|
114
|
-
updateFlowControl(type, data);
|
|
184
|
+
updateFlowControl(host, type, data);
|
|
115
185
|
|
|
116
186
|
const controlled = pendingRequests.get(id);
|
|
187
|
+
// A request is answered only by the host it was sent to.
|
|
188
|
+
if (controlled && controlled.host !== host) return;
|
|
117
189
|
if (controlled?.flowControl === 1) {
|
|
118
190
|
controlled.message(type, data, error);
|
|
119
191
|
return;
|
|
@@ -122,18 +194,18 @@ function handleMainMessage(event) {
|
|
|
122
194
|
// The page's bridge speaks over this port: which servers it holds, which
|
|
123
195
|
// is the preview's, and which documents are the page's own frames.
|
|
124
196
|
if (type === 'server-registered' && data) {
|
|
125
|
-
registeredPorts.add(data.port);
|
|
126
|
-
if (data.primary) primaryPort = data.port;
|
|
127
|
-
else if (primaryPort === data.port) primaryPort = null;
|
|
197
|
+
host.registeredPorts.add(data.port);
|
|
198
|
+
if (data.primary) host.primaryPort = data.port;
|
|
199
|
+
else if (host.primaryPort === data.port) host.primaryPort = null;
|
|
128
200
|
return;
|
|
129
201
|
}
|
|
130
202
|
if (type === 'server-unregistered' && data) {
|
|
131
|
-
registeredPorts.delete(data.port);
|
|
132
|
-
if (primaryPort === data.port) primaryPort = null;
|
|
203
|
+
host.registeredPorts.delete(data.port);
|
|
204
|
+
if (host.primaryPort === data.port) host.primaryPort = null;
|
|
133
205
|
return;
|
|
134
206
|
}
|
|
135
207
|
if (type === 'own-documents' && data && Array.isArray(data.paths)) {
|
|
136
|
-
ownDocuments = new Set(data.paths.map((path) => ownPath(String(path))));
|
|
208
|
+
host.ownDocuments = new Set(data.paths.map((path) => ownPath(String(path))));
|
|
137
209
|
return;
|
|
138
210
|
}
|
|
139
211
|
|
|
@@ -213,30 +285,17 @@ function handleMainMessage(event) {
|
|
|
213
285
|
/**
|
|
214
286
|
* Send request to main thread and wait for response
|
|
215
287
|
*/
|
|
216
|
-
async function sendRequest(port, method, url, headers, body) {
|
|
217
|
-
DEBUG && console.log('[SW] sendRequest called,
|
|
218
|
-
|
|
219
|
-
if (!
|
|
220
|
-
|
|
221
|
-
const allClients = await self.clients.matchAll({ type: 'window' });
|
|
222
|
-
for (const client of allClients) {
|
|
223
|
-
client.postMessage({ type: 'sw-needs-init' });
|
|
224
|
-
}
|
|
225
|
-
// Wait up to 5s for a client to re-initialize the port
|
|
226
|
-
// (main thread may be busy with heavy operations like CLI execution)
|
|
227
|
-
await new Promise(resolve => {
|
|
228
|
-
const check = setInterval(() => { if (mainPort) { clearInterval(check); resolve(); } }, 50);
|
|
229
|
-
setTimeout(() => { clearInterval(check); resolve(); }, 5000);
|
|
230
|
-
});
|
|
231
|
-
if (!mainPort) {
|
|
232
|
-
throw new Error('Service Worker not initialized - no connection to main thread');
|
|
233
|
-
}
|
|
288
|
+
async function sendRequest(host, port, method, url, headers, body) {
|
|
289
|
+
DEBUG && console.log('[SW] sendRequest called, host:', !!host, 'url:', url);
|
|
290
|
+
|
|
291
|
+
if (!host) {
|
|
292
|
+
throw new Error('Service Worker not initialized - no connection to main thread');
|
|
234
293
|
}
|
|
235
294
|
|
|
236
295
|
const id = ++requestId;
|
|
237
296
|
|
|
238
297
|
return new Promise((resolve, reject) => {
|
|
239
|
-
pendingRequests.set(id, { resolve, reject });
|
|
298
|
+
pendingRequests.set(id, { host, resolve, reject });
|
|
240
299
|
|
|
241
300
|
// Set timeout for request
|
|
242
301
|
setTimeout(() => {
|
|
@@ -246,7 +305,7 @@ async function sendRequest(port, method, url, headers, body) {
|
|
|
246
305
|
}
|
|
247
306
|
}, 30000);
|
|
248
307
|
|
|
249
|
-
|
|
308
|
+
host.port.postMessage({
|
|
250
309
|
type: 'request',
|
|
251
310
|
id,
|
|
252
311
|
data: { port, method, url, headers, body },
|
|
@@ -258,22 +317,11 @@ async function sendRequest(port, method, url, headers, body) {
|
|
|
258
317
|
* Send streaming request to main thread
|
|
259
318
|
* Returns a ReadableStream that receives chunks from main thread
|
|
260
319
|
*/
|
|
261
|
-
async function sendStreamingRequest(port, method, url, headers, body) {
|
|
320
|
+
async function sendStreamingRequest(host, port, method, url, headers, body) {
|
|
262
321
|
DEBUG && console.log('[SW] sendStreamingRequest called, url:', url);
|
|
263
322
|
|
|
264
|
-
if (!
|
|
265
|
-
|
|
266
|
-
const allClients = await self.clients.matchAll({ type: 'window' });
|
|
267
|
-
for (const client of allClients) {
|
|
268
|
-
client.postMessage({ type: 'sw-needs-init' });
|
|
269
|
-
}
|
|
270
|
-
await new Promise(resolve => {
|
|
271
|
-
const check = setInterval(() => { if (mainPort) { clearInterval(check); resolve(); } }, 50);
|
|
272
|
-
setTimeout(() => { clearInterval(check); resolve(); }, 5000);
|
|
273
|
-
});
|
|
274
|
-
if (!mainPort) {
|
|
275
|
-
throw new Error('Service Worker not initialized');
|
|
276
|
-
}
|
|
323
|
+
if (!host) {
|
|
324
|
+
throw new Error('Service Worker not initialized');
|
|
277
325
|
}
|
|
278
326
|
|
|
279
327
|
const id = ++requestId;
|
|
@@ -288,6 +336,7 @@ async function sendStreamingRequest(port, method, url, headers, body) {
|
|
|
288
336
|
|
|
289
337
|
// Store in pending requests so handleMainMessage can find it
|
|
290
338
|
pendingRequests.set(id, {
|
|
339
|
+
host,
|
|
291
340
|
resolve: () => {},
|
|
292
341
|
reject: (err) => controller.error(err),
|
|
293
342
|
streamController: controller,
|
|
@@ -295,7 +344,7 @@ async function sendStreamingRequest(port, method, url, headers, body) {
|
|
|
295
344
|
});
|
|
296
345
|
|
|
297
346
|
// Send request to main thread with streaming flag
|
|
298
|
-
|
|
347
|
+
host.port.postMessage({
|
|
299
348
|
type: 'request',
|
|
300
349
|
id,
|
|
301
350
|
data: { port, method, url, headers, body, streaming: true },
|
|
@@ -312,10 +361,10 @@ async function sendStreamingRequest(port, method, url, headers, body) {
|
|
|
312
361
|
// A Node HTTP producer must stop when its consumer stops reading and must
|
|
313
362
|
// observe disconnect. One pull credit bounds the service-worker response queue;
|
|
314
363
|
// this protocol requires a paired host bridge and is negotiated per port.
|
|
315
|
-
async function sendControlledRequest(port, method, url, headers, body, signal) {
|
|
316
|
-
if (!
|
|
364
|
+
async function sendControlledRequest(host, port, method, url, headers, body, signal) {
|
|
365
|
+
if (!host) throw new Error('Service Worker not initialized');
|
|
317
366
|
if (signal.aborted) throw signal.reason;
|
|
318
|
-
const channel =
|
|
367
|
+
const channel = host.port;
|
|
319
368
|
const id = ++requestId;
|
|
320
369
|
let controller, resolveHeaders, rejectHeaders, resolvePull;
|
|
321
370
|
let headed = false, credited = false, ended = false;
|
|
@@ -364,6 +413,7 @@ async function sendControlledRequest(port, method, url, headers, body, signal) {
|
|
|
364
413
|
},
|
|
365
414
|
}, { highWaterMark: 0 });
|
|
366
415
|
pendingRequests.set(id, {
|
|
416
|
+
host,
|
|
367
417
|
flowControl: 1,
|
|
368
418
|
fail,
|
|
369
419
|
port,
|
|
@@ -457,10 +507,9 @@ async function readRequestBody(request, limit, registrationSignal) {
|
|
|
457
507
|
// they would be at their own origin: an app reads its routes from
|
|
458
508
|
// `location.pathname`, and a document under /__virtual__/<port>/ is not at
|
|
459
509
|
// the path its router expects.
|
|
460
|
-
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
let ownDocuments = new Set();
|
|
510
|
+
// Each host's `primaryPort`. The page's own documents that are frames of it,
|
|
511
|
+
// told by `init`, are each host's `ownDocuments`, so a nested document at one
|
|
512
|
+
// of these paths is the page's and not the preview's.
|
|
464
513
|
// The documents and workers served from a virtual server at the root, by
|
|
465
514
|
// client id, so their requests reach that server without a prefix to read.
|
|
466
515
|
const rootedClients = new Map();
|
|
@@ -480,21 +529,23 @@ async function blobWorkerContext(client, requestUrl) {
|
|
|
480
529
|
url: requestUrl, referrer: owner.url, mode: 'cors', destination: '',
|
|
481
530
|
} });
|
|
482
531
|
if (!context) return null;
|
|
532
|
+
const host = ownerOf(owner.id);
|
|
483
533
|
return new Promise((resolve) => {
|
|
484
534
|
const channel = new MessageChannel();
|
|
485
535
|
const finish = (value) => { clearTimeout(timer); channel.port1.close(); resolve(value); };
|
|
486
536
|
const timer = setTimeout(() => finish(null), 1000);
|
|
487
|
-
channel.port1.onmessage = (event) => finish(event.data === true ? context : null);
|
|
537
|
+
channel.port1.onmessage = (event) => finish(event.data === true ? { context, host } : null);
|
|
488
538
|
try { owner.postMessage({ type: 'virtual-blob-owner', url: client.url }, [channel.port2]); }
|
|
489
539
|
catch { finish(null); }
|
|
490
540
|
});
|
|
491
541
|
}));
|
|
492
542
|
const owners = candidates.filter(Boolean);
|
|
493
543
|
if (owners.length === 0) return null;
|
|
494
|
-
if (owners.some(owner => owner.port !== owners[0].port)) throw new Error('Ambiguous virtual blob owner');
|
|
495
|
-
rootedClients.set(client.id, owners[0].port);
|
|
496
|
-
if (
|
|
497
|
-
|
|
544
|
+
if (owners.some(owner => owner.context.port !== owners[0].context.port || owner.host !== owners[0].host)) throw new Error('Ambiguous virtual blob owner');
|
|
545
|
+
rootedClients.set(client.id, owners[0].context.port);
|
|
546
|
+
if (owners[0].host) clientHosts.set(client.id, owners[0].host.id);
|
|
547
|
+
if (rootedClients.size > 256) pruneClients();
|
|
548
|
+
return owners[0].context;
|
|
498
549
|
})();
|
|
499
550
|
pendingBlobContexts.set(client.id, pending);
|
|
500
551
|
try { return await pending; } finally { pendingBlobContexts.delete(client.id); }
|
|
@@ -522,7 +573,7 @@ async function virtualContext(event) {
|
|
|
522
573
|
// or a frame's navigation, asks the page for its registrations first, as a
|
|
523
574
|
// virtual request asks for its port, so a restart does not send the
|
|
524
575
|
// preview's own requests to the network.
|
|
525
|
-
if (
|
|
576
|
+
if (hosts.size === 0 && ((client && client.frameType === 'nested') || (event.request.mode === 'navigate' && (event.request.destination === 'iframe' || event.request.destination === 'frame')))) {
|
|
526
577
|
await askForInit();
|
|
527
578
|
}
|
|
528
579
|
const contextUrl = event.request.referrer || (client ? client.url : '');
|
|
@@ -536,13 +587,14 @@ async function virtualContext(event) {
|
|
|
536
587
|
if (client && (client.type === 'worker' || client.type === 'sharedworker') && client.url.startsWith('blob:')) {
|
|
537
588
|
return blobWorkerContext(client, event.request.url);
|
|
538
589
|
}
|
|
539
|
-
|
|
590
|
+
const host = await hostForEvent(event);
|
|
591
|
+
if (!host || host.primaryPort === null) return null;
|
|
540
592
|
if (event.request.mode === 'navigate') {
|
|
541
593
|
if (event.request.destination !== 'iframe' && event.request.destination !== 'frame') return null;
|
|
542
|
-
if (ownDocuments.has(ownPath(url.pathname))) return null;
|
|
543
|
-
return { prefix: null, port: primaryPort };
|
|
594
|
+
if (host.ownDocuments.has(ownPath(url.pathname))) return null;
|
|
595
|
+
return { prefix: null, port: host.primaryPort };
|
|
544
596
|
}
|
|
545
|
-
if (client && client.frameType === 'nested' && !ownDocuments.has(ownPath(new URL(client.url).pathname))) return { prefix: null, port: primaryPort };
|
|
597
|
+
if (client && client.frameType === 'nested' && !host.ownDocuments.has(ownPath(new URL(client.url).pathname))) return { prefix: null, port: host.primaryPort };
|
|
546
598
|
return null;
|
|
547
599
|
}
|
|
548
600
|
|
|
@@ -556,7 +608,7 @@ async function askForInit() {
|
|
|
556
608
|
const allClients = await self.clients.matchAll({ type: 'window' });
|
|
557
609
|
for (const client of allClients) client.postMessage({ type: 'sw-needs-init' });
|
|
558
610
|
await new Promise((resolve) => {
|
|
559
|
-
const check = setInterval(() => { if (
|
|
611
|
+
const check = setInterval(() => { if (hosts.size > 0) { clearInterval(check); resolve(); } }, 50);
|
|
560
612
|
setTimeout(() => { clearInterval(check); resolve(); }, 5000);
|
|
561
613
|
});
|
|
562
614
|
}
|
|
@@ -565,15 +617,23 @@ async function askForInit() {
|
|
|
565
617
|
function handleRootedRequest(event, port, path) {
|
|
566
618
|
if (event.resultingClientId) {
|
|
567
619
|
rootedClients.set(event.resultingClientId, port);
|
|
568
|
-
if (rootedClients.size > 256)
|
|
620
|
+
if (rootedClients.size > 256) pruneClients();
|
|
569
621
|
}
|
|
570
622
|
return handleVirtualRequest(event.request, port, path, true);
|
|
571
623
|
}
|
|
572
624
|
|
|
573
|
-
/** Forgets the documents and
|
|
574
|
-
async function
|
|
575
|
-
const alive = new Set((await self.clients.matchAll({ type: 'all' })).map((client) => client.id));
|
|
625
|
+
/** Forgets the documents, workers and hosts that are gone. */
|
|
626
|
+
async function pruneClients() {
|
|
627
|
+
const alive = new Set((await self.clients.matchAll({ type: 'all', includeUncontrolled: true })).map((client) => client.id));
|
|
576
628
|
for (const id of [...rootedClients.keys()]) if (!alive.has(id)) rootedClients.delete(id);
|
|
629
|
+
for (const [id, host] of [...clientHosts]) if (!alive.has(id) || !hosts.has(host)) clientHosts.delete(id);
|
|
630
|
+
for (const [id, host] of [...hosts]) {
|
|
631
|
+
if (alive.has(id) || id === UNNAMED_HOST) continue;
|
|
632
|
+
hosts.delete(id);
|
|
633
|
+
host.port.onmessage = null;
|
|
634
|
+
host.port.close();
|
|
635
|
+
if (latestHost === host) latestHost = [...hosts.values()].pop() ?? null;
|
|
636
|
+
}
|
|
577
637
|
}
|
|
578
638
|
|
|
579
639
|
/**
|
|
@@ -581,8 +641,9 @@ async function pruneRootedClients() {
|
|
|
581
641
|
*/
|
|
582
642
|
self.addEventListener('fetch', (event) => {
|
|
583
643
|
const url = new URL(event.request.url);
|
|
644
|
+
requestEvents.set(event.request, event);
|
|
584
645
|
|
|
585
|
-
DEBUG && console.log('[SW] Fetch:', url.pathname, '
|
|
646
|
+
DEBUG && console.log('[SW] Fetch:', url.pathname, 'hosts:', hosts.size);
|
|
586
647
|
|
|
587
648
|
// Check if this is a virtual server request
|
|
588
649
|
const match = url.pathname.match(/^\/__virtual__\/(\d+)(\/.*)?$/);
|
|
@@ -595,7 +656,7 @@ self.addEventListener('fetch', (event) => {
|
|
|
595
656
|
// the referrer, which a document sent with no referrer never has, is
|
|
596
657
|
// the fallback. A document at the root is the preview's server's, and
|
|
597
658
|
// its own requests are that server's with it.
|
|
598
|
-
if (event.request.referrer || event.clientId || (event.request.mode === 'navigate' && primaryPort !== null)) {
|
|
659
|
+
if (event.request.referrer || event.clientId || (event.request.mode === 'navigate' && [...hosts.values()].some((host) => host.primaryPort !== null))) {
|
|
599
660
|
event.respondWith((async () => {
|
|
600
661
|
const context = await virtualContext(event);
|
|
601
662
|
if (!context) return fetch(event.request);
|
|
@@ -629,7 +690,9 @@ self.addEventListener('fetch', (event) => {
|
|
|
629
690
|
*/
|
|
630
691
|
async function handleVirtualRequest(request, port, path, rooted = false) {
|
|
631
692
|
try {
|
|
632
|
-
|
|
693
|
+
if (hosts.size === 0) await askForInit();
|
|
694
|
+
const host = await hostForRequest(request, port);
|
|
695
|
+
const registration = host ? host.flowControlledPorts.get(port) : undefined;
|
|
633
696
|
// Build headers object
|
|
634
697
|
const headers = {};
|
|
635
698
|
request.headers.forEach((value, key) => {
|
|
@@ -646,7 +709,7 @@ async function handleVirtualRequest(request, port, path, rooted = false) {
|
|
|
646
709
|
let body = null;
|
|
647
710
|
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
648
711
|
if (registration) {
|
|
649
|
-
const upload = { port, controller: new AbortController() };
|
|
712
|
+
const upload = { host, port, controller: new AbortController() };
|
|
650
713
|
controlledUploads.add(upload);
|
|
651
714
|
try {
|
|
652
715
|
body = await readRequestBody(request, registration.maxRequestBytes, upload.controller.signal);
|
|
@@ -661,9 +724,9 @@ async function handleVirtualRequest(request, port, path, rooted = false) {
|
|
|
661
724
|
}
|
|
662
725
|
|
|
663
726
|
if (registration) {
|
|
664
|
-
if (flowControlledPorts.get(port) !== registration ||
|
|
727
|
+
if (host.flowControlledPorts.get(port) !== registration || host.port !== registration.channel || hosts.get(host.id) !== host)
|
|
665
728
|
throw new Error('Virtual server registration changed during upload');
|
|
666
|
-
return await sendControlledRequest(port, request.method, path, headers, body, request.signal);
|
|
729
|
+
return await sendControlledRequest(host, port, request.method, path, headers, body, request.signal);
|
|
667
730
|
}
|
|
668
731
|
|
|
669
732
|
// Legacy peers retain their old selection until they negotiate flow control.
|
|
@@ -671,12 +734,12 @@ async function handleVirtualRequest(request, port, path, rooted = false) {
|
|
|
671
734
|
|
|
672
735
|
if (isStreamingCandidate) {
|
|
673
736
|
DEBUG && console.log('[SW] Using streaming mode for:', path);
|
|
674
|
-
return handleStreamingRequest(port, request.method, path, headers, body);
|
|
737
|
+
return handleStreamingRequest(host, port, request.method, path, headers, body);
|
|
675
738
|
}
|
|
676
739
|
DEBUG && console.log('[SW] Using non-streaming mode for:', request.method, path);
|
|
677
740
|
|
|
678
741
|
// Send to main thread
|
|
679
|
-
const response = await sendRequest(port, request.method, path, headers, body);
|
|
742
|
+
const response = await sendRequest(host, port, request.method, path, headers, body);
|
|
680
743
|
|
|
681
744
|
DEBUG && console.log('[SW] Got response from main thread:', {
|
|
682
745
|
statusCode: response.statusCode,
|
|
@@ -741,8 +804,8 @@ async function handleVirtualRequest(request, port, path, rooted = false) {
|
|
|
741
804
|
/**
|
|
742
805
|
* Handle a streaming request
|
|
743
806
|
*/
|
|
744
|
-
async function handleStreamingRequest(port, method, path, headers, body) {
|
|
745
|
-
const { stream, headersPromise, id } = await sendStreamingRequest(port, method, path, headers, body);
|
|
807
|
+
async function handleStreamingRequest(host, port, method, path, headers, body) {
|
|
808
|
+
const { stream, headersPromise, id } = await sendStreamingRequest(host, port, method, path, headers, body);
|
|
746
809
|
|
|
747
810
|
// Wait for headers to arrive
|
|
748
811
|
const responseData = await headersPromise;
|