@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.
- package/dist/index.js +98 -64
- package/dist/link/http-link.d.ts.map +1 -1
- package/dist/relay-sw.js +72 -45
- package/dist/relay-worker.js +72 -45
- package/dist/sw/worker-service.d.ts.map +1 -1
- package/dist/sw-worker.js +72 -45
- package/dist/sw.js +98 -64
- package/dist/worker.js +98 -64
- package/dist/ws/frames.d.ts +6 -1
- package/dist/ws/frames.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/link/http-link.ts +41 -26
- package/src/sw/worker-service.ts +12 -1
- package/src/ws/frames.ts +18 -12
package/src/link/http-link.ts
CHANGED
|
@@ -196,14 +196,18 @@ export function trackedLink(
|
|
|
196
196
|
finish();
|
|
197
197
|
return response;
|
|
198
198
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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)
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
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
|
-
|
|
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
|
|
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(
|
|
257
|
+
controller.error(error);
|
|
249
258
|
} catch {
|
|
250
259
|
/* already closed or errored */
|
|
251
260
|
}
|
|
252
261
|
};
|
|
253
|
-
|
|
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
|
|
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
|
-
|
|
271
|
-
end();
|
|
272
|
-
void reader.cancel().catch(() => {});
|
|
273
|
-
controller.error(reason() ?? error);
|
|
280
|
+
fail(error as Error);
|
|
274
281
|
}
|
|
275
282
|
},
|
|
276
|
-
|
|
283
|
+
cancel(why) {
|
|
277
284
|
end();
|
|
278
|
-
|
|
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). */
|
package/src/sw/worker-service.ts
CHANGED
|
@@ -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()),
|
|
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
|
-
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
}
|