@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 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
- this.messageChannel.port1.postMessage({ type: type2, data: data2 });
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
- 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): Promise<void>;
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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/tabnode",
3
- "version": "0.5.12",
3
+ "version": "0.5.14",
4
4
  "description": "Node's own library, in a browser tab.",
5
5
  "type": "module",
6
6
  "license": "MIT",
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);
@@ -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.messageChannel.port1.postMessage({ type, data });
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