@statewalker/webrun-http-browser 0.12.1 → 0.12.3

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.
@@ -196,14 +196,18 @@ export function trackedLink(
196
196
  finish();
197
197
  return response;
198
198
  }
199
- return new Response(
200
- guardBody(response.body, lost, () => lostReason, finish, request.signal),
201
- {
202
- status: response.status,
203
- statusText: response.statusText,
204
- headers: response.headers,
205
- },
206
- );
199
+ // From here the body fails with the link's reason when the link closes:
200
+ // `fail` becomes the body's, and `finish` removes whichever is current.
201
+ failers.delete(fail);
202
+ const body = guardBody(response.body, finish, request);
203
+ fail = body.fail;
204
+ if (closed) fail(lostReason as Error);
205
+ else if (!finished) failers.add(fail);
206
+ return new Response(body.stream, {
207
+ status: response.status,
208
+ statusText: response.statusText,
209
+ headers: response.headers,
210
+ });
207
211
  },
208
212
  async close(reason) {
209
213
  if (closed) return;
@@ -220,37 +224,43 @@ export function trackedLink(
220
224
  }
221
225
 
222
226
  /**
223
- * A response body that reports its end (done, cancelled, failed or aborted) and
224
- * fails with the link's reason when the link is lost mid-stream. An abort of the
225
- * request's signal ends it at once, read or not: the exchange is over (the layer
226
- * below has given its port back), so the request leaves `inflight` then.
227
+ * A response body that reports its end (done, cancelled, failed or aborted).
228
+ * `fail(error)` ends it with `error` at once — the link calls it when it is lost
229
+ * mid-stream. An abort of the request's signal does the same with the signal's
230
+ * reason, read or not: the exchange is over (the layer below has given its port
231
+ * back), so the request leaves `inflight` then. A read waits on the reader
232
+ * alone, so nothing outlives the chunk it brings.
227
233
  */
228
234
  function guardBody(
229
235
  body: ReadableStream<Uint8Array>,
230
- lost: Promise<never>,
231
- reason: () => Error | undefined,
232
236
  finish: () => void,
233
- signal: AbortSignal | undefined,
234
- ): ReadableStream<Uint8Array> {
237
+ request: Request,
238
+ ): { stream: ReadableStream<Uint8Array>; fail: (error: Error) => void } {
239
+ // Held until the body ends: the Request's signal follows the caller's signal only while the
240
+ // Request lives (Node's undici holds the Request's controller through a WeakRef), and a caller may keep only the Response.
241
+ const keep: { request?: Request } = { request };
242
+ const signal: AbortSignal | undefined = request.signal ?? undefined;
235
243
  const reader = body.getReader();
236
244
  let controller!: ReadableStreamDefaultController<Uint8Array>;
237
245
  let ended = false;
238
246
  const end = () => {
239
247
  ended = true;
240
248
  signal?.removeEventListener("abort", onAbort);
249
+ keep.request = undefined;
241
250
  finish();
242
251
  };
243
- const onAbort = () => {
252
+ const fail = (error: Error) => {
244
253
  if (ended) return;
245
254
  end();
246
255
  void reader.cancel().catch(() => {});
247
256
  try {
248
- controller.error(abortError(signal as AbortSignal));
257
+ controller.error(error);
249
258
  } catch {
250
259
  /* already closed or errored */
251
260
  }
252
261
  };
253
- return new ReadableStream<Uint8Array>({
262
+ const onAbort = () => fail(abortError(signal as AbortSignal));
263
+ const stream = new ReadableStream<Uint8Array>({
254
264
  start(c) {
255
265
  controller = c;
256
266
  if (signal?.aborted) onAbort();
@@ -258,7 +268,7 @@ function guardBody(
258
268
  },
259
269
  async pull(controller) {
260
270
  try {
261
- const { done, value } = await Promise.race([reader.read(), lost]);
271
+ const { done, value } = await reader.read();
262
272
  if (ended) return;
263
273
  if (done) {
264
274
  end();
@@ -267,17 +277,22 @@ function guardBody(
267
277
  }
268
278
  controller.enqueue(value);
269
279
  } catch (error) {
270
- if (ended) return;
271
- end();
272
- void reader.cancel().catch(() => {});
273
- controller.error(reason() ?? error);
280
+ fail(error as Error);
274
281
  }
275
282
  },
276
- async cancel(why) {
283
+ cancel(why) {
277
284
  end();
278
- await reader.cancel(why).catch(() => {});
285
+ // The body is cancelled a macrotask later, not at once. Between two of its pulls it has
286
+ // no read in flight (the stream holds the next `pull` until the last one settles, for a
287
+ // few microtasks), and a cancel landing there abandons the call even with its end on the
288
+ // way. A macrotask later it has either closed (its end came: the call is finished, and
289
+ // an upload the handler reads after answering goes on) or started its next read, which
290
+ // then decides as `fetchOverDuplex`'s body does. How many microtasks the gap lasts
291
+ // differs between engines (Node 22 and 24 do). `cancel()` does not wait for it.
292
+ setTimeout(() => void reader.cancel(why).catch(() => {}), 0);
279
293
  },
280
294
  });
295
+ return { stream, fail };
281
296
  }
282
297
 
283
298
  /** The signal's reason as an `Error` (`AbortError` when it gave none). */
@@ -127,10 +127,21 @@ export function serveHttpInWorker(
127
127
 
128
128
  return {
129
129
  fetch(input, init) {
130
+ // A Request built from another follows its signal only as long as the other lives (in Node,
131
+ // undici links them weakly): pass the signal on directly, so a caller who keeps neither
132
+ // can still abort.
130
133
  const request =
131
134
  input instanceof Request && init === undefined
132
135
  ? input
133
- : new Request(input instanceof Request ? input : new URL(`${input}`, baseUrl()), init);
136
+ : new Request(input instanceof Request ? input : new URL(`${input}`, baseUrl()), {
137
+ ...init,
138
+ signal:
139
+ init && "signal" in init
140
+ ? init.signal
141
+ : input instanceof Request
142
+ ? input.signal
143
+ : undefined,
144
+ });
134
145
  return endpoint.fetch(request);
135
146
  },
136
147
  connectWebSocket,
package/src/ws/frames.ts CHANGED
@@ -100,20 +100,26 @@ export function messageSize(data: string | ArrayBuffer): number {
100
100
  return typeof data === "string" ? utf8Length(data) : data.byteLength;
101
101
  }
102
102
 
103
- /** The UTF-8 length of `text`, without encoding it. */
103
+ const encoder = new TextEncoder();
104
+ /** Characters encoded per `encodeInto` call: at most 3 bytes each, so the scratch stays at 96 KiB. */
105
+ const CHUNK = 32_768;
106
+ const scratch = new Uint8Array(CHUNK * 3);
107
+
108
+ /**
109
+ * The UTF-8 length of `text`, as `TextEncoder` would encode it (a lone surrogate counts as the 3
110
+ * bytes of U+FFFD). Every message is measured on both ends (`bufferedAmount` and the byte acks), so
111
+ * this runs natively: `encodeInto` a reused buffer, a chunk at a time, never splitting a surrogate
112
+ * pair across two chunks. A loop over the characters in JavaScript took about 30 times as long.
113
+ */
104
114
  export function utf8Length(text: string): number {
105
115
  let length = 0;
106
- for (let i = 0; i < text.length; i++) {
107
- const code = text.charCodeAt(i);
108
- if (code < 0x80) length += 1;
109
- else if (code < 0x800) length += 2;
110
- else if (code >= 0xd800 && code <= 0xdbff && i + 1 < text.length) {
111
- const next = text.charCodeAt(i + 1);
112
- if (next >= 0xdc00 && next <= 0xdfff) {
113
- length += 4;
114
- i++;
115
- } else length += 3;
116
- } else length += 3;
116
+ for (let start = 0; start < text.length; ) {
117
+ let end = Math.min(start + CHUNK, text.length);
118
+ // A high surrogate ending the chunk goes with its low surrogate into the next one.
119
+ const last = text.charCodeAt(end - 1);
120
+ if (end < text.length && last >= 0xd800 && last <= 0xdbff) end--;
121
+ length += encoder.encodeInto(text.slice(start, end), scratch).written;
122
+ start = end;
117
123
  }
118
124
  return length;
119
125
  }