@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 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: 15 - cleanup: extract helpers, gate debug logs, remove test endpoints
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
- // Communication port with main thread
10
- let mainPort = null;
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 in.
20
- const flowControlledPorts = new Map();
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: mainPort });
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
- // Initialize communication channel
71
- for (const pending of pendingRequests.values()) {
72
- if (pending.flowControl === 1) pending.fail(new Error('Host connection replaced'));
73
- }
74
- for (const upload of controlledUploads) upload.controller.abort(new Error('Host connection replaced'));
75
- if (mainPort) {
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 (mainPort === initializedPort) handleMainMessage(message);
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, mainPort:', !!mainPort, 'url:', url);
218
-
219
- if (!mainPort) {
220
- // Ask all clients to re-send the init message
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
- mainPort.postMessage({
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 (!mainPort) {
265
- // Ask all clients to re-send the init message
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
- mainPort.postMessage({
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 (!mainPort) throw new Error('Service Worker not initialized');
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 = mainPort;
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
- let primaryPort = null;
461
- // The page's own documents that are frames of it, told by `init`, so a
462
- // nested document at one of these paths is the page's and not the preview's.
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 (rootedClients.size > 256) pruneRootedClients();
497
- return owners[0];
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 (primaryPort === null && !mainPort && ((client && client.frameType === 'nested') || (event.request.mode === 'navigate' && (event.request.destination === 'iframe' || event.request.destination === 'frame')))) {
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
- if (primaryPort === null) return null;
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 (mainPort) { clearInterval(check); resolve(); } }, 50);
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) pruneRootedClients();
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 workers that are gone. */
574
- async function pruneRootedClients() {
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, 'mainPort:', !!mainPort);
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
- const registration = flowControlledPorts.get(port);
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 || mainPort !== registration.channel)
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 = requireFunctionApply();
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 = requireFunctionApply();
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 = requireFunctionApply();
22089
+ var $apply2 = functionApply;
22097
22090
  var actualApply$1 = actualApply;
22098
22091
  applyBind = function applyBind2() {
22099
22092
  return actualApply$1(bind3, $apply2, arguments);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/tabnode",
3
- "version": "0.5.22",
3
+ "version": "0.5.23",
4
4
  "description": "Node's own library, in a browser tab.",
5
5
  "type": "module",
6
6
  "license": "MIT",