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