@volter/tabnode 0.4.1 → 0.5.1
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 +259 -5
- package/dist/assets/{runtime-worker-LWfW6VWE.js → runtime-worker-DBZ2flHK.js} +4000 -2944
- package/dist/fetch-transport.d.ts +20 -0
- package/dist/guest-timers.d.ts +15 -0
- package/dist/http-client-transport.d.ts +19 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.mjs +5196 -3062
- package/dist/native-stream-binding.d.ts +57 -0
- package/dist/native-stream-owner.d.ts +121 -0
- package/dist/node-lib/binding/duplex_stream.d.ts +26 -0
- package/dist/node-lib/binding/fds.d.ts +8 -0
- package/dist/node-lib/binding/fs.d.ts +311 -78
- package/dist/node-lib/binding/misc.d.ts +1 -0
- package/dist/node-lib/binding/pipe_wrap.d.ts +4 -4
- package/dist/node-lib/binding/process_wrap.d.ts +12 -1
- package/dist/node-lib/binding/stream_wrap.d.ts +27 -18
- package/dist/node-lib/binding/tcp_wrap.d.ts +0 -1
- package/dist/node-lib/binding/tty_wrap.d.ts +43 -2
- package/dist/node-lib/binding/util.d.ts +83 -0
- package/dist/node-lib/binding/uv.d.ts +4 -0
- package/dist/node-lib/fs-module.d.ts +1 -0
- package/dist/node-lib/internals/buffer-and-streams.d.ts +30 -17
- package/dist/node-lib/internals/index.d.ts +1 -1
- package/dist/node-lib/load.d.ts +17 -3
- package/dist/node-lib/module-hooks.d.ts +1 -0
- package/dist/node-lib/public-modules.d.ts +1 -1
- package/dist/node-lib/timers.d.ts +38 -0
- package/dist/node-process-host.d.ts +41 -0
- package/dist/process-registry.d.ts +39 -0
- package/dist/process-tokens.d.ts +11 -0
- package/dist/promise-ownership.d.ts +3 -0
- package/dist/runtime.d.ts +10 -9
- package/dist/shims/child_process.d.ts +2 -0
- package/dist/shims/crypto.d.ts +660 -119
- package/dist/shims/dns.d.ts +76 -2
- package/dist/shims/process.d.ts +1 -0
- package/dist/shims/string_decoder.d.ts +19 -3
- package/dist/shims/tls.d.ts +853 -31
- package/dist/source-edits.d.ts +7 -0
- package/package.json +1 -1
- package/src/code-transforms.ts +2 -3
- package/src/fetch-transport.ts +98 -0
- package/src/guest-timers.ts +89 -0
- package/src/http-client-transport.ts +169 -0
- package/src/index.ts +18 -0
- package/src/native-stream-binding.ts +393 -0
- package/src/native-stream-owner.ts +317 -0
- package/src/node-lib/binding/duplex_stream.ts +95 -0
- package/src/node-lib/binding/fds.ts +18 -1
- package/src/node-lib/binding/fs.ts +45 -17
- package/src/node-lib/binding/misc.ts +2 -0
- package/src/node-lib/binding/pipe_wrap.ts +35 -12
- package/src/node-lib/binding/process_wrap.ts +50 -9
- package/src/node-lib/binding/stream_wrap.ts +196 -72
- package/src/node-lib/binding/tcp_wrap.ts +43 -8
- package/src/node-lib/binding/tty_wrap.ts +135 -8
- package/src/node-lib/binding/util.ts +24 -15
- package/src/node-lib/binding/uv.ts +4 -0
- package/src/node-lib/fs-module.ts +2 -18
- package/src/node-lib/internal/navigator.js +149 -0
- package/src/node-lib/internals/buffer-and-streams.ts +50 -11
- package/src/node-lib/internals/events-util.ts +5 -0
- package/src/node-lib/internals/index.ts +12 -2
- package/src/node-lib/load.ts +170 -62
- package/src/node-lib/module-hooks.ts +5 -2
- package/src/node-lib/public-modules.ts +12 -48
- package/src/node-lib/sources.ts +4 -0
- package/src/node-lib/timers.ts +93 -0
- package/src/node-process-host.ts +124 -0
- package/src/process-registry.ts +144 -0
- package/src/process-tokens.ts +55 -6
- package/src/promise-ownership.ts +28 -0
- package/src/runtime.ts +90 -214
- package/src/shims/child_process.ts +310 -80
- package/src/shims/crypto.ts +115 -100
- package/src/shims/dns.ts +17 -1
- package/src/shims/process.ts +33 -11
- package/src/shims/string_decoder.ts +10 -6
- package/src/shims/tls.ts +49 -19
- package/src/source-edits.ts +20 -0
package/dist/__sw__.js
CHANGED
|
@@ -15,6 +15,35 @@ let requestId = 0;
|
|
|
15
15
|
|
|
16
16
|
// Registered virtual server ports
|
|
17
17
|
const registeredPorts = new Set();
|
|
18
|
+
// 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();
|
|
21
|
+
const controlledUploads = new Set();
|
|
22
|
+
const MAX_STREAM_CHUNK_BYTES = 65536;
|
|
23
|
+
|
|
24
|
+
function updateFlowControl(type, data) {
|
|
25
|
+
if (type === 'server-registered' || type === 'server-unregistered') {
|
|
26
|
+
for (const upload of controlledUploads) {
|
|
27
|
+
if (upload.port === data?.port) upload.controller.abort(new Error('Virtual server registration changed'));
|
|
28
|
+
}
|
|
29
|
+
// A port number is not a listener identity. Rebinding must terminate the
|
|
30
|
+
// old connection, even when the replacement advertises identical limits.
|
|
31
|
+
// Otherwise an already-headed response can wait forever on its old owner.
|
|
32
|
+
for (const pending of pendingRequests.values()) {
|
|
33
|
+
if (pending.flowControl === 1 && pending.port === data?.port)
|
|
34
|
+
pending.fail(new Error(type === 'server-unregistered'
|
|
35
|
+
? 'Virtual server closed' : 'Virtual server registration changed'));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (type === 'server-unregistered') {
|
|
39
|
+
flowControlledPorts.delete(data?.port);
|
|
40
|
+
}
|
|
41
|
+
if (type !== 'server-registered' || !data) return;
|
|
42
|
+
if (data.flowControl === 1 && Number.isSafeInteger(data.maxRequestBytes)
|
|
43
|
+
&& 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);
|
|
46
|
+
}
|
|
18
47
|
|
|
19
48
|
/**
|
|
20
49
|
* Decode base64 string to Uint8Array
|
|
@@ -39,8 +68,24 @@ self.addEventListener('message', (event) => {
|
|
|
39
68
|
// When a MessagePort is transferred, it's in event.ports[0], not event.data.port
|
|
40
69
|
if (type === 'init' && event.ports && event.ports[0]) {
|
|
41
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.
|
|
42
84
|
mainPort = event.ports[0];
|
|
43
|
-
|
|
85
|
+
const initializedPort = mainPort;
|
|
86
|
+
mainPort.onmessage = (message) => {
|
|
87
|
+
if (mainPort === initializedPort) handleMainMessage(message);
|
|
88
|
+
};
|
|
44
89
|
if (data && Array.isArray(data.ownDocuments)) ownDocuments = new Set(data.ownDocuments.map((path) => ownPath(String(path))));
|
|
45
90
|
DEBUG && console.log('[SW] Initialized communication channel with transferred port');
|
|
46
91
|
// Re-claim clients so that pages opened after SW activation get controlled.
|
|
@@ -66,6 +111,13 @@ self.addEventListener('message', (event) => {
|
|
|
66
111
|
*/
|
|
67
112
|
function handleMainMessage(event) {
|
|
68
113
|
const { type, id, data, error } = event.data;
|
|
114
|
+
updateFlowControl(type, data);
|
|
115
|
+
|
|
116
|
+
const controlled = pendingRequests.get(id);
|
|
117
|
+
if (controlled?.flowControl === 1) {
|
|
118
|
+
controlled.message(type, data, error);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
69
121
|
|
|
70
122
|
// The page's bridge speaks over this port: which servers it holds, which
|
|
71
123
|
// is the preview's, and which documents are the page's own frames.
|
|
@@ -257,6 +309,150 @@ async function sendStreamingRequest(port, method, url, headers, body) {
|
|
|
257
309
|
return { stream, headersPromise, id };
|
|
258
310
|
}
|
|
259
311
|
|
|
312
|
+
// A Node HTTP producer must stop when its consumer stops reading and must
|
|
313
|
+
// observe disconnect. One pull credit bounds the service-worker response queue;
|
|
314
|
+
// 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');
|
|
317
|
+
if (signal.aborted) throw signal.reason;
|
|
318
|
+
const channel = mainPort;
|
|
319
|
+
const id = ++requestId;
|
|
320
|
+
let controller, resolveHeaders, rejectHeaders, resolvePull;
|
|
321
|
+
let headed = false, credited = false, ended = false;
|
|
322
|
+
const headersPromise = new Promise((resolve, reject) => {
|
|
323
|
+
resolveHeaders = resolve;
|
|
324
|
+
rejectHeaders = reject;
|
|
325
|
+
});
|
|
326
|
+
// A synchronous postMessage failure can happen before the first await.
|
|
327
|
+
void headersPromise.catch(() => {});
|
|
328
|
+
const send = (type) => channel.postMessage({ type, id });
|
|
329
|
+
const finish = () => {
|
|
330
|
+
ended = true;
|
|
331
|
+
clearTimeout(headerTimeout);
|
|
332
|
+
pendingRequests.delete(id);
|
|
333
|
+
signal.removeEventListener('abort', abort);
|
|
334
|
+
resolvePull?.();
|
|
335
|
+
resolvePull = undefined;
|
|
336
|
+
};
|
|
337
|
+
const fail = (error, notify = true) => {
|
|
338
|
+
if (ended) return;
|
|
339
|
+
if (notify) {
|
|
340
|
+
try { send('stream-cancel'); } catch { /* connection gone */ }
|
|
341
|
+
}
|
|
342
|
+
rejectHeaders(error);
|
|
343
|
+
controller.error(error);
|
|
344
|
+
finish();
|
|
345
|
+
};
|
|
346
|
+
const abort = () => fail(signal.reason ?? new DOMException('Request cancelled', 'AbortError'));
|
|
347
|
+
// Match the existing buffered request's finite header wait. A disappeared
|
|
348
|
+
// Host must not retain this request forever when no AbortSignal fires.
|
|
349
|
+
const headerTimeout = setTimeout(() => fail(new Error('Request header timeout')), 30000);
|
|
350
|
+
const stream = new ReadableStream({
|
|
351
|
+
start(value) { controller = value; },
|
|
352
|
+
pull() {
|
|
353
|
+
if (ended) return;
|
|
354
|
+
if (credited) throw new Error('Duplicate stream credit');
|
|
355
|
+
credited = true;
|
|
356
|
+
return new Promise((resolve) => {
|
|
357
|
+
resolvePull = resolve;
|
|
358
|
+
try { send('stream-pull'); } catch (error) { fail(error, false); }
|
|
359
|
+
});
|
|
360
|
+
},
|
|
361
|
+
cancel() {
|
|
362
|
+
if (ended) return;
|
|
363
|
+
try { send('stream-cancel'); } finally { finish(); }
|
|
364
|
+
},
|
|
365
|
+
}, { highWaterMark: 0 });
|
|
366
|
+
pendingRequests.set(id, {
|
|
367
|
+
flowControl: 1,
|
|
368
|
+
fail,
|
|
369
|
+
port,
|
|
370
|
+
message(type, data, error) {
|
|
371
|
+
try {
|
|
372
|
+
if (type === 'stream-start' && !headed) {
|
|
373
|
+
headed = true;
|
|
374
|
+
clearTimeout(headerTimeout);
|
|
375
|
+
resolveHeaders(data);
|
|
376
|
+
} else if (type === 'stream-chunk' && headed && credited) {
|
|
377
|
+
if (typeof data?.chunkBase64 !== 'string'
|
|
378
|
+
|| data.chunkBase64.length > Math.ceil(MAX_STREAM_CHUNK_BYTES / 3) * 4)
|
|
379
|
+
throw new Error('Stream chunk exceeds transport limit');
|
|
380
|
+
const bytes = base64ToBytes(data.chunkBase64);
|
|
381
|
+
if (bytes.byteLength > MAX_STREAM_CHUNK_BYTES)
|
|
382
|
+
throw new Error('Stream chunk exceeds transport limit');
|
|
383
|
+
credited = false;
|
|
384
|
+
controller.enqueue(bytes);
|
|
385
|
+
resolvePull?.();
|
|
386
|
+
resolvePull = undefined;
|
|
387
|
+
} else if (type === 'stream-end' && headed) {
|
|
388
|
+
controller.close();
|
|
389
|
+
finish();
|
|
390
|
+
} else if (type === 'stream-error' || (type === 'response' && error)) {
|
|
391
|
+
fail(new Error(error || data?.message || 'Host stream failed'), false);
|
|
392
|
+
} else throw new Error('Invalid flow-controlled stream message');
|
|
393
|
+
} catch (cause) { fail(cause); }
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
397
|
+
try {
|
|
398
|
+
channel.postMessage({ type: 'request', id,
|
|
399
|
+
data: { port, method, url, headers, body, streaming: true, flowControl: 1 } });
|
|
400
|
+
if (signal.aborted) abort();
|
|
401
|
+
const head = await headersPromise;
|
|
402
|
+
const noBody = method === 'HEAD' || [204, 205, 304].includes(head.statusCode);
|
|
403
|
+
if (noBody) await stream.cancel();
|
|
404
|
+
// Match the existing virtual-port document isolation policy. Negotiating
|
|
405
|
+
// flow control must not turn a working preview into a blocked iframe.
|
|
406
|
+
const responseHeaders = new Headers(head.headers);
|
|
407
|
+
responseHeaders.set('Cross-Origin-Embedder-Policy', 'credentialless');
|
|
408
|
+
responseHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');
|
|
409
|
+
responseHeaders.set('Cross-Origin-Resource-Policy', 'cross-origin');
|
|
410
|
+
responseHeaders.delete('X-Frame-Options');
|
|
411
|
+
return new Response(noBody ? null : stream, {
|
|
412
|
+
status: head.statusCode, statusText: head.statusMessage, headers: responseHeaders,
|
|
413
|
+
});
|
|
414
|
+
} catch (error) {
|
|
415
|
+
fail(error);
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function readRequestBody(request, limit, registrationSignal) {
|
|
421
|
+
if (!request.body) return null;
|
|
422
|
+
const reader = request.body.getReader();
|
|
423
|
+
const chunks = [];
|
|
424
|
+
let bytes = 0;
|
|
425
|
+
const checkAbort = () => {
|
|
426
|
+
if (request.signal.aborted) throw request.signal.reason;
|
|
427
|
+
if (registrationSignal.aborted) throw registrationSignal.reason;
|
|
428
|
+
};
|
|
429
|
+
const abort = () => { void reader.cancel().catch(() => {}); };
|
|
430
|
+
request.signal.addEventListener('abort', abort, { once: true });
|
|
431
|
+
registrationSignal.addEventListener('abort', abort, { once: true });
|
|
432
|
+
try {
|
|
433
|
+
while (true) {
|
|
434
|
+
checkAbort();
|
|
435
|
+
const { value, done } = await reader.read();
|
|
436
|
+
checkAbort();
|
|
437
|
+
if (done) break;
|
|
438
|
+
bytes += value.byteLength;
|
|
439
|
+
if (bytes > limit) throw Object.assign(new Error('Request body exceeds transport limit'), { status: 413 });
|
|
440
|
+
chunks.push(value);
|
|
441
|
+
}
|
|
442
|
+
const result = new Uint8Array(bytes);
|
|
443
|
+
let offset = 0;
|
|
444
|
+
for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.byteLength; }
|
|
445
|
+
return result.buffer;
|
|
446
|
+
} catch (error) {
|
|
447
|
+
void reader.cancel(error).catch(() => {});
|
|
448
|
+
throw error;
|
|
449
|
+
} finally {
|
|
450
|
+
request.signal.removeEventListener('abort', abort);
|
|
451
|
+
registrationSignal.removeEventListener('abort', abort);
|
|
452
|
+
reader.releaseLock();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
260
456
|
// The preview's server, whose documents are served at this origin's root as
|
|
261
457
|
// they would be at their own origin: an app reads its routes from
|
|
262
458
|
// `location.pathname`, and a document under /__virtual__/<port>/ is not at
|
|
@@ -268,6 +464,41 @@ let ownDocuments = new Set();
|
|
|
268
464
|
// The documents and workers served from a virtual server at the root, by
|
|
269
465
|
// client id, so their requests reach that server without a prefix to read.
|
|
270
466
|
const rootedClients = new Map();
|
|
467
|
+
const pendingBlobContexts = new Map();
|
|
468
|
+
|
|
469
|
+
// Blob workers have no script fetch at which to remember resultingClientId,
|
|
470
|
+
// and their URL contains no virtual port. Ask the creating preview document
|
|
471
|
+
// to identify its object URL, then derive the port from that document's own
|
|
472
|
+
// routing context. The document supplies ownership, never a trusted port.
|
|
473
|
+
// Querying also recovers attribution after this service worker restarts.
|
|
474
|
+
async function blobWorkerContext(client, requestUrl) {
|
|
475
|
+
if (pendingBlobContexts.has(client.id)) return pendingBlobContexts.get(client.id);
|
|
476
|
+
const pending = (async () => {
|
|
477
|
+
const windows = await self.clients.matchAll({ type: 'window' });
|
|
478
|
+
const candidates = await Promise.all(windows.map(async (owner) => {
|
|
479
|
+
const context = await virtualContext({ clientId: owner.id, request: {
|
|
480
|
+
url: requestUrl, referrer: owner.url, mode: 'cors', destination: '',
|
|
481
|
+
} });
|
|
482
|
+
if (!context) return null;
|
|
483
|
+
return new Promise((resolve) => {
|
|
484
|
+
const channel = new MessageChannel();
|
|
485
|
+
const finish = (value) => { clearTimeout(timer); channel.port1.close(); resolve(value); };
|
|
486
|
+
const timer = setTimeout(() => finish(null), 1000);
|
|
487
|
+
channel.port1.onmessage = (event) => finish(event.data === true ? context : null);
|
|
488
|
+
try { owner.postMessage({ type: 'virtual-blob-owner', url: client.url }, [channel.port2]); }
|
|
489
|
+
catch { finish(null); }
|
|
490
|
+
});
|
|
491
|
+
}));
|
|
492
|
+
const owners = candidates.filter(Boolean);
|
|
493
|
+
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];
|
|
498
|
+
})();
|
|
499
|
+
pendingBlobContexts.set(client.id, pending);
|
|
500
|
+
try { return await pending; } finally { pendingBlobContexts.delete(client.id); }
|
|
501
|
+
}
|
|
271
502
|
|
|
272
503
|
/**
|
|
273
504
|
* The virtual server a request outside /__virtual__/ is for, if any: the
|
|
@@ -302,6 +533,9 @@ async function virtualContext(event) {
|
|
|
302
533
|
} catch (e) { /* not a URL */ }
|
|
303
534
|
}
|
|
304
535
|
if (event.clientId && rootedClients.has(event.clientId)) return { prefix: null, port: rootedClients.get(event.clientId) };
|
|
536
|
+
if (client && (client.type === 'worker' || client.type === 'sharedworker') && client.url.startsWith('blob:')) {
|
|
537
|
+
return blobWorkerContext(client, event.request.url);
|
|
538
|
+
}
|
|
305
539
|
if (primaryPort === null) return null;
|
|
306
540
|
if (event.request.mode === 'navigate') {
|
|
307
541
|
if (event.request.destination !== 'iframe' && event.request.destination !== 'frame') return null;
|
|
@@ -395,16 +629,29 @@ self.addEventListener('fetch', (event) => {
|
|
|
395
629
|
*/
|
|
396
630
|
async function handleVirtualRequest(request, port, path, rooted = false) {
|
|
397
631
|
try {
|
|
632
|
+
const registration = flowControlledPorts.get(port);
|
|
398
633
|
// Build headers object
|
|
399
634
|
const headers = {};
|
|
400
635
|
request.headers.forEach((value, key) => {
|
|
401
636
|
headers[key] = value;
|
|
402
637
|
});
|
|
638
|
+
// Fetch omits Host from Request.headers. Preserve the authority addressed
|
|
639
|
+
// by the browser, as an HTTP socket would, rather than letting the server
|
|
640
|
+
// substitute its listening port. Routing to a virtual port does not change
|
|
641
|
+
// the client's authority; server-generated resource identities must agree
|
|
642
|
+
// with those the client computes from its own URL.
|
|
643
|
+
if (headers.host === undefined) headers.host = new URL(request.url).host;
|
|
403
644
|
|
|
404
645
|
// Get body if present
|
|
405
646
|
let body = null;
|
|
406
647
|
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
407
|
-
|
|
648
|
+
if (registration) {
|
|
649
|
+
const upload = { port, controller: new AbortController() };
|
|
650
|
+
controlledUploads.add(upload);
|
|
651
|
+
try {
|
|
652
|
+
body = await readRequestBody(request, registration.maxRequestBytes, upload.controller.signal);
|
|
653
|
+
} finally { controlledUploads.delete(upload); }
|
|
654
|
+
} else body = await request.arrayBuffer();
|
|
408
655
|
// A body's length is a header Node's server always has, content-length
|
|
409
656
|
// or a chunked transfer-encoding; a fetch's headers carry neither, and
|
|
410
657
|
// Express 5's body parser takes their absence for no body at all.
|
|
@@ -413,7 +660,13 @@ async function handleVirtualRequest(request, port, path, rooted = false) {
|
|
|
413
660
|
}
|
|
414
661
|
}
|
|
415
662
|
|
|
416
|
-
|
|
663
|
+
if (registration) {
|
|
664
|
+
if (flowControlledPorts.get(port) !== registration || mainPort !== registration.channel)
|
|
665
|
+
throw new Error('Virtual server registration changed during upload');
|
|
666
|
+
return await sendControlledRequest(port, request.method, path, headers, body, request.signal);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Legacy peers retain their old selection until they negotiate flow control.
|
|
417
670
|
const isStreamingCandidate = request.method === 'POST' && path.startsWith('/api/');
|
|
418
671
|
|
|
419
672
|
if (isStreamingCandidate) {
|
|
@@ -475,10 +728,11 @@ async function handleVirtualRequest(request, port, path, rooted = false) {
|
|
|
475
728
|
|
|
476
729
|
return finalResponse;
|
|
477
730
|
} catch (error) {
|
|
731
|
+
if (request.signal.aborted) throw error;
|
|
478
732
|
console.error('[SW] Error handling virtual request:', error);
|
|
479
733
|
return new Response(`Service Worker Error: ${error.message}`, {
|
|
480
|
-
status: 500,
|
|
481
|
-
statusText: 'Internal Server Error',
|
|
734
|
+
status: error.status === 413 ? 413 : 500,
|
|
735
|
+
statusText: error.status === 413 ? 'Content Too Large' : 'Internal Server Error',
|
|
482
736
|
headers: { 'Content-Type': 'text/plain' },
|
|
483
737
|
});
|
|
484
738
|
}
|