@volter/tabnode 0.5.12 → 0.5.14
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/index.d.ts +1 -1
- package/dist/index.mjs +121 -5
- package/dist/node-lib/http-bridge.d.ts +11 -1
- package/dist/server-bridge.d.ts +12 -2
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/node-lib/http-bridge.ts +17 -0
- package/src/server-bridge.ts +89 -2
package/dist/index.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export { PackageManager, install } from './npm';
|
|
|
33
33
|
export { ServerBridge, getServerBridge, resetServerBridge } from './server-bridge';
|
|
34
34
|
export type { InitServiceWorkerOptions } from './server-bridge';
|
|
35
35
|
/** What a page's request is answered with, the shape the bridge answers. */
|
|
36
|
-
export type { ResponseData } from './node-lib/http-bridge';
|
|
36
|
+
export type { ResponseData, LoopbackStreamFlow } from './node-lib/http-bridge';
|
|
37
37
|
export * as esbuild from './shims/esbuild';
|
|
38
38
|
export * as rollup from './shims/rollup';
|
|
39
39
|
export * as module from './shims/module';
|
package/dist/index.mjs
CHANGED
|
@@ -37278,13 +37278,14 @@ function __requestOverLoopback(port, method, url2, headers, body) {
|
|
|
37278
37278
|
if (body && body.length > 0) socket.write(body);
|
|
37279
37279
|
});
|
|
37280
37280
|
}
|
|
37281
|
-
function __streamOverLoopback(port, method, url2, headers, body, onStart, onChunk, onEnd) {
|
|
37281
|
+
function __streamOverLoopback(port, method, url2, headers, body, onStart, onChunk, onEnd, flow) {
|
|
37282
37282
|
if (!__listening(port)) {
|
|
37283
37283
|
onStart(503, "Service Unavailable", { "Content-Type": "text/plain" });
|
|
37284
37284
|
onEnd();
|
|
37285
37285
|
return Promise.resolve();
|
|
37286
37286
|
}
|
|
37287
37287
|
return new Promise((resolve2) => {
|
|
37288
|
+
var _a2;
|
|
37288
37289
|
const collected = emptyCollected();
|
|
37289
37290
|
let started2 = false;
|
|
37290
37291
|
let ended = false;
|
|
@@ -37304,6 +37305,18 @@ function __streamOverLoopback(port, method, url2, headers, body, onStart, onChun
|
|
|
37304
37305
|
(chunk) => onChunk(chunk)
|
|
37305
37306
|
);
|
|
37306
37307
|
const socket = netModule.connect({ port, host: "127.0.0.1" });
|
|
37308
|
+
(_a2 = flow == null ? void 0 : flow.control) == null ? void 0 : _a2.call(flow, () => socket.pause(), () => socket.resume());
|
|
37309
|
+
if (flow == null ? void 0 : flow.signal) {
|
|
37310
|
+
if (flow.signal.aborted) {
|
|
37311
|
+
socket.destroy();
|
|
37312
|
+
finish();
|
|
37313
|
+
return;
|
|
37314
|
+
}
|
|
37315
|
+
flow.signal.addEventListener("abort", () => {
|
|
37316
|
+
socket.destroy();
|
|
37317
|
+
finish();
|
|
37318
|
+
}, { once: true });
|
|
37319
|
+
}
|
|
37307
37320
|
socket.on("data", (chunk) => {
|
|
37308
37321
|
if (ended) return;
|
|
37309
37322
|
const read = parser.execute(chunk);
|
|
@@ -37380,6 +37393,9 @@ function __upgradeOverLoopback(port, method, url2, headers) {
|
|
|
37380
37393
|
});
|
|
37381
37394
|
}
|
|
37382
37395
|
const _encoder = new TextEncoder();
|
|
37396
|
+
const FLOW_MAX_REQUEST_BYTES = 64 * 1024 * 1024;
|
|
37397
|
+
const FLOW_MAX_CHUNK_BYTES = 65536;
|
|
37398
|
+
const FLOW_QUEUED_CHUNKS = 4;
|
|
37383
37399
|
function ownedBytes(view) {
|
|
37384
37400
|
const copy2 = new Uint8Array(view.byteLength);
|
|
37385
37401
|
copy2.set(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
|
|
@@ -37649,6 +37665,8 @@ const _ServerBridge = class _ServerBridge extends EventEmitter {
|
|
|
37649
37665
|
/** The server the service worker answers at the origin's root, the preview's; null for none. */
|
|
37650
37666
|
__publicField(this, "primaryPort", null);
|
|
37651
37667
|
__publicField(this, "ownDocuments");
|
|
37668
|
+
/** Flow-controlled requests in flight: a pull is one chunk's credit, a cancel ends the upstream. */
|
|
37669
|
+
__publicField(this, "controlledStreams", /* @__PURE__ */ new Map());
|
|
37652
37670
|
this.options = options;
|
|
37653
37671
|
if (typeof location !== "undefined") {
|
|
37654
37672
|
this.baseUrl = options.baseUrl || `${location.protocol}//${location.host}`;
|
|
@@ -37819,6 +37837,7 @@ const _ServerBridge = class _ServerBridge extends EventEmitter {
|
|
|
37819
37837
|
await controllerReady;
|
|
37820
37838
|
const reinit = () => {
|
|
37821
37839
|
if (navigator.serviceWorker.controller) {
|
|
37840
|
+
for (const stream of [...this.controlledStreams.values()]) stream.cancel();
|
|
37822
37841
|
this.messageChannel = new MessageChannel();
|
|
37823
37842
|
this.messageChannel.port1.onmessage = this.handleServiceWorkerMessage.bind(this);
|
|
37824
37843
|
navigator.serviceWorker.controller.postMessage(
|
|
@@ -37850,6 +37869,17 @@ const _ServerBridge = class _ServerBridge extends EventEmitter {
|
|
|
37850
37869
|
var _a2, _b2, _c2;
|
|
37851
37870
|
const { type: type2, id, data: data2 } = event.data;
|
|
37852
37871
|
_ServerBridge.DEBUG && console.log("[ServerBridge] SW message:", type2, id, data2 == null ? void 0 : data2.url);
|
|
37872
|
+
if (type2 === "stream-pull" || type2 === "stream-cancel") {
|
|
37873
|
+
const stream = this.controlledStreams.get(id);
|
|
37874
|
+
if (type2 === "stream-pull") stream == null ? void 0 : stream.pull();
|
|
37875
|
+
else stream == null ? void 0 : stream.cancel();
|
|
37876
|
+
return;
|
|
37877
|
+
}
|
|
37878
|
+
if (type2 === "request" && (data2 == null ? void 0 : data2.flowControl) === 1) {
|
|
37879
|
+
const { port, method, url: url2, headers, body } = data2;
|
|
37880
|
+
await this.controlledStream(id, port, method, url2, headers, body);
|
|
37881
|
+
return;
|
|
37882
|
+
}
|
|
37853
37883
|
if (type2 === "request") {
|
|
37854
37884
|
const { port, method, url: url2, headers, body, streaming } = data2;
|
|
37855
37885
|
_ServerBridge.DEBUG && console.log("[ServerBridge] Handling request:", port, method, url2, "streaming:", streaming);
|
|
@@ -37898,7 +37928,7 @@ const _ServerBridge = class _ServerBridge extends EventEmitter {
|
|
|
37898
37928
|
* it; a server the host registered answers through its own streaming
|
|
37899
37929
|
* method where it has one, else its buffered answer is delivered whole.
|
|
37900
37930
|
*/
|
|
37901
|
-
async handleStreamingRequest(port, method, url2, headers, body, callbacks) {
|
|
37931
|
+
async handleStreamingRequest(port, method, url2, headers, body, callbacks, flow) {
|
|
37902
37932
|
const virtualServer = this.servers.get(port);
|
|
37903
37933
|
if (!virtualServer) return false;
|
|
37904
37934
|
const bodyBuffer = body ? Buffer$9.from(new Uint8Array(body)) : void 0;
|
|
@@ -37913,7 +37943,8 @@ const _ServerBridge = class _ServerBridge extends EventEmitter {
|
|
|
37913
37943
|
bodyBuffer,
|
|
37914
37944
|
(statusCode, statusMessage, respHeaders) => callbacks.start(statusCode, statusMessage, respHeaders),
|
|
37915
37945
|
(chunk) => callbacks.chunk(ownedBytes(chunk)),
|
|
37916
|
-
() => callbacks.end()
|
|
37946
|
+
() => callbacks.end(),
|
|
37947
|
+
flow
|
|
37917
37948
|
);
|
|
37918
37949
|
return true;
|
|
37919
37950
|
}
|
|
@@ -37926,7 +37957,8 @@ const _ServerBridge = class _ServerBridge extends EventEmitter {
|
|
|
37926
37957
|
bodyBuffer,
|
|
37927
37958
|
(statusCode, statusMessage, respHeaders) => callbacks.start(statusCode, statusMessage, respHeaders),
|
|
37928
37959
|
(chunk) => callbacks.chunk(typeof chunk === "string" ? _encoder.encode(chunk) : ownedBytes(chunk)),
|
|
37929
|
-
() => callbacks.end()
|
|
37960
|
+
() => callbacks.end(),
|
|
37961
|
+
flow
|
|
37930
37962
|
);
|
|
37931
37963
|
return true;
|
|
37932
37964
|
}
|
|
@@ -38043,7 +38075,91 @@ const _ServerBridge = class _ServerBridge extends EventEmitter {
|
|
|
38043
38075
|
*/
|
|
38044
38076
|
notifyServiceWorker(type2, data2) {
|
|
38045
38077
|
if (this.serviceWorkerReady && this.messageChannel) {
|
|
38046
|
-
|
|
38078
|
+
const flow = type2 === "server-registered" ? { flowControl: 1, maxRequestBytes: FLOW_MAX_REQUEST_BYTES, maxChunkBytes: FLOW_MAX_CHUNK_BYTES } : {};
|
|
38079
|
+
this.messageChannel.port1.postMessage({ type: type2, data: { ...data2, ...flow } });
|
|
38080
|
+
}
|
|
38081
|
+
}
|
|
38082
|
+
/**
|
|
38083
|
+
* A request the worker sent under flow control: the response starts with
|
|
38084
|
+
* its head, then goes one chunk (at most FLOW_MAX_CHUNK_BYTES) for each
|
|
38085
|
+
* `stream-pull`, and ends with `stream-end` once the server finished and
|
|
38086
|
+
* every chunk went out. The upstream connection is paused while chunks wait
|
|
38087
|
+
* for credit, so a slow reader holds the server back; `stream-cancel` ends it.
|
|
38088
|
+
*/
|
|
38089
|
+
async controlledStream(id, port, method, url2, headers, body) {
|
|
38090
|
+
const channel = this.messageChannel;
|
|
38091
|
+
const post = (type2, data2) => {
|
|
38092
|
+
channel == null ? void 0 : channel.port1.postMessage({ type: type2, id, ...data2 === void 0 ? {} : { data: data2 } });
|
|
38093
|
+
};
|
|
38094
|
+
const queue = [];
|
|
38095
|
+
const abort = new AbortController();
|
|
38096
|
+
let credits = 0;
|
|
38097
|
+
let upstreamEnded = false;
|
|
38098
|
+
let finished = false;
|
|
38099
|
+
let paused = false;
|
|
38100
|
+
let pause = () => void 0;
|
|
38101
|
+
let resume = () => void 0;
|
|
38102
|
+
const finish = () => {
|
|
38103
|
+
finished = true;
|
|
38104
|
+
this.controlledStreams.delete(id);
|
|
38105
|
+
};
|
|
38106
|
+
const flush = () => {
|
|
38107
|
+
if (finished) return;
|
|
38108
|
+
while (credits > 0 && queue.length > 0) {
|
|
38109
|
+
credits -= 1;
|
|
38110
|
+
post("stream-chunk", { chunkBase64: uint8ToBase64(queue.shift()) });
|
|
38111
|
+
}
|
|
38112
|
+
if (upstreamEnded && queue.length === 0) {
|
|
38113
|
+
post("stream-end");
|
|
38114
|
+
finish();
|
|
38115
|
+
return;
|
|
38116
|
+
}
|
|
38117
|
+
if (queue.length >= FLOW_QUEUED_CHUNKS && !paused) {
|
|
38118
|
+
paused = true;
|
|
38119
|
+
pause();
|
|
38120
|
+
} else if (queue.length < FLOW_QUEUED_CHUNKS && paused) {
|
|
38121
|
+
paused = false;
|
|
38122
|
+
resume();
|
|
38123
|
+
}
|
|
38124
|
+
};
|
|
38125
|
+
this.controlledStreams.set(id, {
|
|
38126
|
+
pull: () => {
|
|
38127
|
+
credits += 1;
|
|
38128
|
+
flush();
|
|
38129
|
+
},
|
|
38130
|
+
cancel: () => {
|
|
38131
|
+
if (finished) return;
|
|
38132
|
+
finish();
|
|
38133
|
+
queue.length = 0;
|
|
38134
|
+
abort.abort();
|
|
38135
|
+
}
|
|
38136
|
+
});
|
|
38137
|
+
try {
|
|
38138
|
+
const answered = await this.handleStreamingRequest(port, method, url2, headers, body, {
|
|
38139
|
+
start: (statusCode, statusMessage, respHeaders) => post("stream-start", { statusCode, statusMessage, headers: respHeaders }),
|
|
38140
|
+
chunk: (chunk) => {
|
|
38141
|
+
if (finished) return;
|
|
38142
|
+
for (let at2 = 0; at2 < chunk.byteLength; at2 += FLOW_MAX_CHUNK_BYTES) queue.push(chunk.slice(at2, at2 + FLOW_MAX_CHUNK_BYTES));
|
|
38143
|
+
flush();
|
|
38144
|
+
},
|
|
38145
|
+
end: () => {
|
|
38146
|
+
upstreamEnded = true;
|
|
38147
|
+
flush();
|
|
38148
|
+
}
|
|
38149
|
+
}, { signal: abort.signal, control: (p, r) => {
|
|
38150
|
+
pause = p;
|
|
38151
|
+
resume = r;
|
|
38152
|
+
} });
|
|
38153
|
+
if (!answered) {
|
|
38154
|
+
post("stream-start", { statusCode: 503, statusMessage: "Service Unavailable", headers: {} });
|
|
38155
|
+
post("stream-end");
|
|
38156
|
+
finish();
|
|
38157
|
+
}
|
|
38158
|
+
} catch (error) {
|
|
38159
|
+
if (!finished) {
|
|
38160
|
+
post("stream-error", { message: error instanceof Error ? error.message : String(error) });
|
|
38161
|
+
finish();
|
|
38162
|
+
}
|
|
38047
38163
|
}
|
|
38048
38164
|
}
|
|
38049
38165
|
/**
|
|
@@ -22,7 +22,17 @@ export declare function __requestOverLoopback(port: number, method: string, url:
|
|
|
22
22
|
* the server writes, then the end. This is what the service worker streams
|
|
23
23
|
* to the page.
|
|
24
24
|
*/
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* How a caller paces a streamed answer: `signal` ends the connection (the
|
|
27
|
+
* reader went away), and `control` is handed the connection's pause and
|
|
28
|
+
* resume, so a reader that holds no credit holds the server back instead of
|
|
29
|
+
* a queue growing without bound.
|
|
30
|
+
*/
|
|
31
|
+
export interface LoopbackStreamFlow {
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
control?(pause: () => void, resume: () => void): void;
|
|
34
|
+
}
|
|
35
|
+
export declare function __streamOverLoopback(port: number, method: string, url: string, headers: Record<string, string>, body: Buffer | undefined, onStart: (statusCode: number, statusMessage: string, headers: Record<string, string>) => void, onChunk: (chunk: Uint8Array) => void, onEnd: () => void, flow?: LoopbackStreamFlow): Promise<void>;
|
|
26
36
|
/** What an upgrade answered with: the 101 and the socket the page now owns. */
|
|
27
37
|
export interface UpgradeResult {
|
|
28
38
|
statusCode: number;
|
package/dist/server-bridge.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Server Bridge
|
|
3
3
|
* Connects Service Worker requests to virtual HTTP servers
|
|
4
4
|
*/
|
|
5
|
-
import { type ResponseData } from './node-lib/http-bridge';
|
|
5
|
+
import { type ResponseData, type LoopbackStreamFlow } from './node-lib/http-bridge';
|
|
6
6
|
import { EventEmitter } from './node-lib/events-module';
|
|
7
7
|
/**
|
|
8
8
|
* Interface for virtual servers that can be registered with the bridge
|
|
@@ -125,12 +125,22 @@ export declare class ServerBridge extends EventEmitter {
|
|
|
125
125
|
start(statusCode: number, statusMessage: string, headers: Record<string, string>): void;
|
|
126
126
|
chunk(chunk: Uint8Array): void;
|
|
127
127
|
end(): void;
|
|
128
|
-
}): Promise<boolean>;
|
|
128
|
+
}, flow?: LoopbackStreamFlow): Promise<boolean>;
|
|
129
129
|
private streamToServiceWorker;
|
|
130
130
|
/**
|
|
131
131
|
* Send message to Service Worker
|
|
132
132
|
*/
|
|
133
133
|
private notifyServiceWorker;
|
|
134
|
+
/** Flow-controlled requests in flight: a pull is one chunk's credit, a cancel ends the upstream. */
|
|
135
|
+
private readonly controlledStreams;
|
|
136
|
+
/**
|
|
137
|
+
* A request the worker sent under flow control: the response starts with
|
|
138
|
+
* its head, then goes one chunk (at most FLOW_MAX_CHUNK_BYTES) for each
|
|
139
|
+
* `stream-pull`, and ends with `stream-end` once the server finished and
|
|
140
|
+
* every chunk went out. The upstream connection is paused while chunks wait
|
|
141
|
+
* for credit, so a slow reader holds the server back; `stream-cancel` ends it.
|
|
142
|
+
*/
|
|
143
|
+
private controlledStream;
|
|
134
144
|
/**
|
|
135
145
|
* Create a mock request handler for testing without Service Worker
|
|
136
146
|
*/
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -38,7 +38,7 @@ export { PackageManager, install } from './npm';
|
|
|
38
38
|
export { ServerBridge, getServerBridge, resetServerBridge } from './server-bridge';
|
|
39
39
|
export type { InitServiceWorkerOptions } from './server-bridge';
|
|
40
40
|
/** What a page's request is answered with, the shape the bridge answers. */
|
|
41
|
-
export type { ResponseData } from './node-lib/http-bridge';
|
|
41
|
+
export type { ResponseData, LoopbackStreamFlow } from './node-lib/http-bridge';
|
|
42
42
|
// The bundlers' own wasm builds, as doors: a host that builds outside a run
|
|
43
43
|
// (the substrate's rolldown pass) reaches them here rather than through a
|
|
44
44
|
// guest's `require`.
|
|
@@ -195,11 +195,23 @@ export function __requestOverLoopback(
|
|
|
195
195
|
* the server writes, then the end. This is what the service worker streams
|
|
196
196
|
* to the page.
|
|
197
197
|
*/
|
|
198
|
+
/**
|
|
199
|
+
* How a caller paces a streamed answer: `signal` ends the connection (the
|
|
200
|
+
* reader went away), and `control` is handed the connection's pause and
|
|
201
|
+
* resume, so a reader that holds no credit holds the server back instead of
|
|
202
|
+
* a queue growing without bound.
|
|
203
|
+
*/
|
|
204
|
+
export interface LoopbackStreamFlow {
|
|
205
|
+
signal?: AbortSignal;
|
|
206
|
+
control?(pause: () => void, resume: () => void): void;
|
|
207
|
+
}
|
|
208
|
+
|
|
198
209
|
export function __streamOverLoopback(
|
|
199
210
|
port: number, method: string, url: string, headers: Record<string, string>, body: Buffer | undefined,
|
|
200
211
|
onStart: (statusCode: number, statusMessage: string, headers: Record<string, string>) => void,
|
|
201
212
|
onChunk: (chunk: Uint8Array) => void,
|
|
202
213
|
onEnd: () => void,
|
|
214
|
+
flow?: LoopbackStreamFlow,
|
|
203
215
|
): Promise<void> {
|
|
204
216
|
if (!__listening(port)) {
|
|
205
217
|
onStart(503, 'Service Unavailable', { 'Content-Type': 'text/plain' });
|
|
@@ -217,6 +229,11 @@ export function __streamOverLoopback(
|
|
|
217
229
|
(chunk) => onChunk(chunk),
|
|
218
230
|
);
|
|
219
231
|
const socket = netModule.connect({ port, host: '127.0.0.1' }) as unknown as Socket;
|
|
232
|
+
flow?.control?.(() => socket.pause(), () => socket.resume());
|
|
233
|
+
if (flow?.signal) {
|
|
234
|
+
if (flow.signal.aborted) { socket.destroy(); finish(); return; }
|
|
235
|
+
flow.signal.addEventListener('abort', () => { socket.destroy(); finish(); }, { once: true });
|
|
236
|
+
}
|
|
220
237
|
socket.on('data', (chunk: Uint8Array) => {
|
|
221
238
|
if (ended) return;
|
|
222
239
|
const read = parser.execute(chunk);
|
package/src/server-bridge.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
__requestOverLoopback, __streamOverLoopback, __upgradeOverLoopback, __listening,
|
|
8
|
-
type ResponseData,
|
|
8
|
+
type ResponseData, type LoopbackStreamFlow,
|
|
9
9
|
} from './node-lib/http-bridge';
|
|
10
10
|
import { setPortWatchers } from './node-lib/net-module';
|
|
11
11
|
import { EventEmitter } from './node-lib/events-module';
|
|
@@ -15,6 +15,12 @@ import { Buffer } from './node-lib/buffer-module';
|
|
|
15
15
|
import { uint8ToBase64 } from './utils/binary-encoding';
|
|
16
16
|
|
|
17
17
|
const _encoder = new TextEncoder();
|
|
18
|
+
// The worker's flow-controlled stream: a request body up to this size, and a
|
|
19
|
+
// response in chunks of exactly the worker's own maximum, one per pull.
|
|
20
|
+
const FLOW_MAX_REQUEST_BYTES = 64 * 1024 * 1024;
|
|
21
|
+
const FLOW_MAX_CHUNK_BYTES = 65536;
|
|
22
|
+
// Chunks that may wait for the reader's credit before the server is paused.
|
|
23
|
+
const FLOW_QUEUED_CHUNKS = 4;
|
|
18
24
|
|
|
19
25
|
/** The bytes this view names, in a buffer of their own. A pooled Buffer is a window on 8 KB. */
|
|
20
26
|
function ownedBytes(view: Uint8Array): Uint8Array {
|
|
@@ -649,6 +655,7 @@ export class ServerBridge extends EventEmitter {
|
|
|
649
655
|
// to all clients when a request arrives but mainPort is null.
|
|
650
656
|
const reinit = () => {
|
|
651
657
|
if (navigator.serviceWorker.controller) {
|
|
658
|
+
for (const stream of [...this.controlledStreams.values()]) stream.cancel();
|
|
652
659
|
this.messageChannel = new MessageChannel();
|
|
653
660
|
this.messageChannel.port1.onmessage = this.handleServiceWorkerMessage.bind(this);
|
|
654
661
|
navigator.serviceWorker.controller.postMessage(
|
|
@@ -684,6 +691,16 @@ export class ServerBridge extends EventEmitter {
|
|
|
684
691
|
|
|
685
692
|
ServerBridge.DEBUG && console.log('[ServerBridge] SW message:', type, id, data?.url);
|
|
686
693
|
|
|
694
|
+
if (type === 'stream-pull' || type === 'stream-cancel') {
|
|
695
|
+
const stream = this.controlledStreams.get(id);
|
|
696
|
+
if (type === 'stream-pull') stream?.pull(); else stream?.cancel();
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (type === 'request' && data?.flowControl === 1) {
|
|
700
|
+
const { port, method, url, headers, body } = data;
|
|
701
|
+
await this.controlledStream(id, port, method, url, headers, body);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
687
704
|
if (type === 'request') {
|
|
688
705
|
const { port, method, url, headers, body, streaming } = data;
|
|
689
706
|
|
|
@@ -752,6 +769,7 @@ export class ServerBridge extends EventEmitter {
|
|
|
752
769
|
chunk(chunk: Uint8Array): void;
|
|
753
770
|
end(): void;
|
|
754
771
|
},
|
|
772
|
+
flow?: LoopbackStreamFlow,
|
|
755
773
|
): Promise<boolean> {
|
|
756
774
|
const virtualServer = this.servers.get(port);
|
|
757
775
|
if (!virtualServer) return false;
|
|
@@ -764,6 +782,7 @@ export class ServerBridge extends EventEmitter {
|
|
|
764
782
|
(statusCode, statusMessage, respHeaders) => callbacks.start(statusCode, statusMessage, respHeaders),
|
|
765
783
|
(chunk) => callbacks.chunk(ownedBytes(chunk)),
|
|
766
784
|
() => callbacks.end(),
|
|
785
|
+
flow,
|
|
767
786
|
);
|
|
768
787
|
return true;
|
|
769
788
|
}
|
|
@@ -773,14 +792,18 @@ export class ServerBridge extends EventEmitter {
|
|
|
773
792
|
onStart: (statusCode: number, statusMessage: string, headers: Record<string, string>) => void,
|
|
774
793
|
onChunk: (chunk: string | Uint8Array) => void,
|
|
775
794
|
onEnd: () => void,
|
|
795
|
+
flow?: LoopbackStreamFlow,
|
|
776
796
|
) => Promise<void>;
|
|
777
797
|
};
|
|
778
798
|
if (typeof server.handleStreamingRequest === 'function') {
|
|
799
|
+
// a registered server that streams is paced the same way: it is handed
|
|
800
|
+
// the reader's going away and the pause and resume of its own producer
|
|
779
801
|
await server.handleStreamingRequest(
|
|
780
802
|
method, url, headers, bodyBuffer,
|
|
781
803
|
(statusCode, statusMessage, respHeaders) => callbacks.start(statusCode, statusMessage, respHeaders),
|
|
782
804
|
(chunk) => callbacks.chunk(typeof chunk === 'string' ? _encoder.encode(chunk) : ownedBytes(chunk)),
|
|
783
805
|
() => callbacks.end(),
|
|
806
|
+
flow,
|
|
784
807
|
);
|
|
785
808
|
return true;
|
|
786
809
|
}
|
|
@@ -909,7 +932,71 @@ export class ServerBridge extends EventEmitter {
|
|
|
909
932
|
*/
|
|
910
933
|
private notifyServiceWorker(type: string, data: unknown): void {
|
|
911
934
|
if (this.serviceWorkerReady && this.messageChannel) {
|
|
912
|
-
this
|
|
935
|
+
// Every port this bridge answers speaks the worker's flow-controlled
|
|
936
|
+
// stream: a response is paced by the reader's pulls, one chunk a pull,
|
|
937
|
+
// and a reader that goes away ends the request (virtual HTTP flow
|
|
938
|
+
// control, the worker's side of which is in public/__sw__.js).
|
|
939
|
+
const flow = type === 'server-registered'
|
|
940
|
+
? { flowControl: 1, maxRequestBytes: FLOW_MAX_REQUEST_BYTES, maxChunkBytes: FLOW_MAX_CHUNK_BYTES } : {};
|
|
941
|
+
this.messageChannel.port1.postMessage({ type, data: { ...(data as object), ...flow } });
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** Flow-controlled requests in flight: a pull is one chunk's credit, a cancel ends the upstream. */
|
|
946
|
+
private readonly controlledStreams = new Map<number, { pull(): void; cancel(): void }>();
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* A request the worker sent under flow control: the response starts with
|
|
950
|
+
* its head, then goes one chunk (at most FLOW_MAX_CHUNK_BYTES) for each
|
|
951
|
+
* `stream-pull`, and ends with `stream-end` once the server finished and
|
|
952
|
+
* every chunk went out. The upstream connection is paused while chunks wait
|
|
953
|
+
* for credit, so a slow reader holds the server back; `stream-cancel` ends it.
|
|
954
|
+
*/
|
|
955
|
+
private async controlledStream(id: number, port: number, method: string, url: string, headers: Record<string, string>, body?: ArrayBuffer): Promise<void> {
|
|
956
|
+
// the stream belongs to the channel it arrived on; a new channel's worker failed it already
|
|
957
|
+
const channel = this.messageChannel;
|
|
958
|
+
const post = (type: string, data?: unknown): void => { channel?.port1.postMessage({ type, id, ...(data === undefined ? {} : { data }) }); };
|
|
959
|
+
const queue: Uint8Array[] = [];
|
|
960
|
+
const abort = new AbortController();
|
|
961
|
+
let credits = 0;
|
|
962
|
+
let upstreamEnded = false;
|
|
963
|
+
let finished = false;
|
|
964
|
+
let paused = false;
|
|
965
|
+
let pause = (): void => undefined;
|
|
966
|
+
let resume = (): void => undefined;
|
|
967
|
+
const finish = (): void => { finished = true; this.controlledStreams.delete(id); };
|
|
968
|
+
const flush = (): void => {
|
|
969
|
+
if (finished) return;
|
|
970
|
+
while (credits > 0 && queue.length > 0) {
|
|
971
|
+
credits -= 1;
|
|
972
|
+
post('stream-chunk', { chunkBase64: uint8ToBase64(queue.shift()!) });
|
|
973
|
+
}
|
|
974
|
+
if (upstreamEnded && queue.length === 0) { post('stream-end'); finish(); return; }
|
|
975
|
+
// a few chunks may wait for credit; more than that, and the server waits
|
|
976
|
+
if (queue.length >= FLOW_QUEUED_CHUNKS && !paused) { paused = true; pause(); }
|
|
977
|
+
else if (queue.length < FLOW_QUEUED_CHUNKS && paused) { paused = false; resume(); }
|
|
978
|
+
};
|
|
979
|
+
this.controlledStreams.set(id, {
|
|
980
|
+
pull: () => { credits += 1; flush(); },
|
|
981
|
+
cancel: () => { if (finished) return; finish(); queue.length = 0; abort.abort(); },
|
|
982
|
+
});
|
|
983
|
+
try {
|
|
984
|
+
const answered = await this.handleStreamingRequest(port, method, url, headers, body, {
|
|
985
|
+
start: (statusCode, statusMessage, respHeaders) => post('stream-start', { statusCode, statusMessage, headers: respHeaders }),
|
|
986
|
+
chunk: (chunk) => {
|
|
987
|
+
if (finished) return;
|
|
988
|
+
for (let at = 0; at < chunk.byteLength; at += FLOW_MAX_CHUNK_BYTES) queue.push(chunk.slice(at, at + FLOW_MAX_CHUNK_BYTES));
|
|
989
|
+
flush();
|
|
990
|
+
},
|
|
991
|
+
end: () => { upstreamEnded = true; flush(); },
|
|
992
|
+
}, { signal: abort.signal, control: (p, r) => { pause = p; resume = r; } });
|
|
993
|
+
if (!answered) {
|
|
994
|
+
post('stream-start', { statusCode: 503, statusMessage: 'Service Unavailable', headers: {} });
|
|
995
|
+
post('stream-end');
|
|
996
|
+
finish();
|
|
997
|
+
}
|
|
998
|
+
} catch (error) {
|
|
999
|
+
if (!finished) { post('stream-error', { message: error instanceof Error ? error.message : String(error) }); finish(); }
|
|
913
1000
|
}
|
|
914
1001
|
}
|
|
915
1002
|
|