@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.
Files changed (2) hide show
  1. package/dist/__sw__.js +174 -111
  2. 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: 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
+ // 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 in.
20
- const flowControlledPorts = new Map();
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: mainPort });
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
- // 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];
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 (mainPort === initializedPort) handleMainMessage(message);
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, 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
- }
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
- mainPort.postMessage({
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 (!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
- }
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
- mainPort.postMessage({
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 (!mainPort) throw new Error('Service Worker not initialized');
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 = mainPort;
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
- 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();
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 (rootedClients.size > 256) pruneRootedClients();
497
- return owners[0];
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 (primaryPort === null && !mainPort && ((client && client.frameType === 'nested') || (event.request.mode === 'navigate' && (event.request.destination === 'iframe' || event.request.destination === 'frame')))) {
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
- if (primaryPort === null) return null;
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 (mainPort) { clearInterval(check); resolve(); } }, 50);
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) pruneRootedClients();
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 workers that are gone. */
574
- async function pruneRootedClients() {
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, 'mainPort:', !!mainPort);
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
- const registration = flowControlledPorts.get(port);
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 || mainPort !== registration.channel)
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/tabnode",
3
- "version": "0.5.22",
3
+ "version": "0.5.24",
4
4
  "description": "Node's own library, in a browser tab.",
5
5
  "type": "module",
6
6
  "license": "MIT",