@statewalker/webrun-http-browser 0.7.1 → 0.10.0
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 +716 -84
- package/dist/relay-sw.js +647 -90
- package/dist/relay-worker.js +647 -90
- package/dist/sw/protocol.d.ts +2 -0
- package/dist/sw/protocol.d.ts.map +1 -1
- package/dist/sw/sw-http-adapter.d.ts +2 -0
- package/dist/sw/sw-http-adapter.d.ts.map +1 -1
- package/dist/sw/worker-service.d.ts.map +1 -1
- package/dist/sw-worker.js +695 -81
- package/dist/sw.js +709 -83
- package/dist/worker.js +704 -82
- package/package.json +3 -3
- package/src/link/http-link.ts +42 -6
- package/src/sw/protocol.ts +4 -3
- package/src/sw/sw-http-adapter.ts +7 -0
- package/src/sw/worker-service.ts +14 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/webrun-http-browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "ServiceWorker-based HTTP and WebSocket server for browsers: handlers in pages and Dedicated Workers, same-origin and relay modes, streams with backpressure",
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
],
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"idb-keyval": "^6.3.0",
|
|
61
|
+
"@statewalker/webrun-rpc": "^0.8.0",
|
|
61
62
|
"@statewalker/webrun-streams": "^0.2.3",
|
|
62
|
-
"@statewalker/webrun-
|
|
63
|
-
"@statewalker/webrun-http-streams": "^0.3.1"
|
|
63
|
+
"@statewalker/webrun-http-streams": "^0.4.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@biomejs/biome": "^2.5.15",
|
package/src/link/http-link.ts
CHANGED
|
@@ -181,7 +181,7 @@ export function trackedLink(
|
|
|
181
181
|
return response;
|
|
182
182
|
}
|
|
183
183
|
return new Response(
|
|
184
|
-
guardBody(response.body, lost, () => lostReason, finish),
|
|
184
|
+
guardBody(response.body, lost, () => lostReason, finish, request.signal),
|
|
185
185
|
{
|
|
186
186
|
status: response.status,
|
|
187
187
|
statusText: response.statusText,
|
|
@@ -204,35 +204,71 @@ export function trackedLink(
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
/**
|
|
207
|
-
* A response body that reports its end (done, cancelled or
|
|
208
|
-
* with the link's reason when the link is lost mid-stream.
|
|
207
|
+
* A response body that reports its end (done, cancelled, failed or aborted) and
|
|
208
|
+
* fails with the link's reason when the link is lost mid-stream. An abort of the
|
|
209
|
+
* request's signal ends it at once, read or not: the exchange is over (the layer
|
|
210
|
+
* below has given its port back), so the request leaves `inflight` then.
|
|
209
211
|
*/
|
|
210
212
|
function guardBody(
|
|
211
213
|
body: ReadableStream<Uint8Array>,
|
|
212
214
|
lost: Promise<never>,
|
|
213
215
|
reason: () => Error | undefined,
|
|
214
216
|
finish: () => void,
|
|
217
|
+
signal: AbortSignal | undefined,
|
|
215
218
|
): ReadableStream<Uint8Array> {
|
|
216
219
|
const reader = body.getReader();
|
|
220
|
+
let controller!: ReadableStreamDefaultController<Uint8Array>;
|
|
221
|
+
let ended = false;
|
|
222
|
+
const end = () => {
|
|
223
|
+
ended = true;
|
|
224
|
+
signal?.removeEventListener("abort", onAbort);
|
|
225
|
+
finish();
|
|
226
|
+
};
|
|
227
|
+
const onAbort = () => {
|
|
228
|
+
if (ended) return;
|
|
229
|
+
end();
|
|
230
|
+
void reader.cancel().catch(() => {});
|
|
231
|
+
try {
|
|
232
|
+
controller.error(abortError(signal as AbortSignal));
|
|
233
|
+
} catch {
|
|
234
|
+
/* already closed or errored */
|
|
235
|
+
}
|
|
236
|
+
};
|
|
217
237
|
return new ReadableStream<Uint8Array>({
|
|
238
|
+
start(c) {
|
|
239
|
+
controller = c;
|
|
240
|
+
if (signal?.aborted) onAbort();
|
|
241
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
242
|
+
},
|
|
218
243
|
async pull(controller) {
|
|
219
244
|
try {
|
|
220
245
|
const { done, value } = await Promise.race([reader.read(), lost]);
|
|
246
|
+
if (ended) return;
|
|
221
247
|
if (done) {
|
|
222
|
-
|
|
248
|
+
end();
|
|
223
249
|
controller.close();
|
|
224
250
|
return;
|
|
225
251
|
}
|
|
226
252
|
controller.enqueue(value);
|
|
227
253
|
} catch (error) {
|
|
228
|
-
|
|
254
|
+
if (ended) return;
|
|
255
|
+
end();
|
|
229
256
|
void reader.cancel().catch(() => {});
|
|
230
257
|
controller.error(reason() ?? error);
|
|
231
258
|
}
|
|
232
259
|
},
|
|
233
260
|
async cancel(why) {
|
|
234
|
-
|
|
261
|
+
end();
|
|
235
262
|
await reader.cancel(why).catch(() => {});
|
|
236
263
|
},
|
|
237
264
|
});
|
|
238
265
|
}
|
|
266
|
+
|
|
267
|
+
/** The signal's reason as an `Error` (`AbortError` when it gave none). */
|
|
268
|
+
function abortError(signal: AbortSignal): Error {
|
|
269
|
+
const why = (signal as unknown as { reason?: unknown }).reason;
|
|
270
|
+
if (why instanceof Error) return why;
|
|
271
|
+
const err = new Error(why === undefined ? "Aborted" : String(why));
|
|
272
|
+
err.name = "AbortError";
|
|
273
|
+
return err;
|
|
274
|
+
}
|
package/src/sw/protocol.ts
CHANGED
|
@@ -93,11 +93,12 @@ export const WORKER_HELLO = "webrun-http:worker-hello";
|
|
|
93
93
|
export type WorkerControlMessage =
|
|
94
94
|
// worker → page, once: the worker serves, and holds this owner lock.
|
|
95
95
|
| { type: "ready"; ownerLock?: string }
|
|
96
|
-
// page → worker: a fresh link (ports: down, ctl, up).
|
|
97
|
-
| { type: "link"; key: string; routerLock?: string }
|
|
96
|
+
// page → worker: a fresh link (ports: down, ctl, up), numbered per control port.
|
|
97
|
+
| { type: "link"; key: string; routerLock?: string; seq?: number }
|
|
98
98
|
// page → worker: drop the links of this key.
|
|
99
99
|
| { type: "unlink"; key: string }
|
|
100
100
|
// worker → page: my links are dead and a call waits; attach me again.
|
|
101
|
-
|
|
101
|
+
// `seen`: the `seq` of the last link received on this port (0: none yet).
|
|
102
|
+
| { type: "relink"; seen?: number }
|
|
102
103
|
// worker → page: a call is in flight; keep the ServiceWorker awake.
|
|
103
104
|
| { type: "keepalive" };
|
|
@@ -480,6 +480,8 @@ class WorkerLinkImpl implements WorkerLink {
|
|
|
480
480
|
private readonly _ownerLock?: string;
|
|
481
481
|
private _attaching?: Promise<void>;
|
|
482
482
|
private _detached = false;
|
|
483
|
+
/** The `seq` of the last link sent to the worker. */
|
|
484
|
+
private _sent = 0;
|
|
483
485
|
|
|
484
486
|
constructor(
|
|
485
487
|
private readonly _adapter: SwHttpAdapter,
|
|
@@ -494,6 +496,10 @@ class WorkerLinkImpl implements WorkerLink {
|
|
|
494
496
|
_control.onmessage = (event) => {
|
|
495
497
|
const message = event.data as WorkerControlMessage;
|
|
496
498
|
if (message?.type === "relink") {
|
|
499
|
+
// The worker has not received the latest link yet: it is on its way
|
|
500
|
+
// (a call made before it arrived asks too). Attaching again would
|
|
501
|
+
// replace the link a call is about to run on.
|
|
502
|
+
if (message.seen !== undefined && message.seen < this._sent) return;
|
|
497
503
|
// Starts the ServiceWorker if it was stopped; the attach follows.
|
|
498
504
|
this._adapter._poke(WAKE_MESSAGE);
|
|
499
505
|
void this.attach().catch(() => {});
|
|
@@ -555,6 +561,7 @@ class WorkerLinkImpl implements WorkerLink {
|
|
|
555
561
|
type: "link",
|
|
556
562
|
key: this.key,
|
|
557
563
|
routerLock: reply.routerLock,
|
|
564
|
+
seq: ++this._sent,
|
|
558
565
|
} satisfies WorkerControlMessage,
|
|
559
566
|
[down.port2, ctl.port2, up.port2],
|
|
560
567
|
);
|
package/src/sw/worker-service.ts
CHANGED
|
@@ -64,12 +64,23 @@ export function serveHttpInWorker(
|
|
|
64
64
|
const ownerLock = newLockName("webrun-http-owner");
|
|
65
65
|
const held = holdLock(ownerLock);
|
|
66
66
|
const controls = new Set<MessagePort>();
|
|
67
|
+
// Per control port, the `seq` of the last link received on it. A relink
|
|
68
|
+
// request carries it: the page attaches again only if this worker has its
|
|
69
|
+
// latest link, not while that link is still on its way.
|
|
70
|
+
const seen = new Map<MessagePort, number>();
|
|
67
71
|
const tellPages = (message: WorkerControlMessage) => {
|
|
68
72
|
for (const control of controls) control.postMessage(message);
|
|
69
73
|
};
|
|
70
74
|
const endpoint = new HandlerEndpoint(handler, {
|
|
71
75
|
...options,
|
|
72
|
-
requestRelink: () =>
|
|
76
|
+
requestRelink: () => {
|
|
77
|
+
for (const control of controls) {
|
|
78
|
+
control.postMessage({
|
|
79
|
+
type: "relink",
|
|
80
|
+
seen: seen.get(control) ?? 0,
|
|
81
|
+
} satisfies WorkerControlMessage);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
73
84
|
keepAlive: () => tellPages({ type: "keepalive" }),
|
|
74
85
|
});
|
|
75
86
|
|
|
@@ -82,6 +93,7 @@ export function serveHttpInWorker(
|
|
|
82
93
|
control.onmessage = (message) => {
|
|
83
94
|
const msg = message.data as WorkerControlMessage;
|
|
84
95
|
if (msg?.type === "link") {
|
|
96
|
+
if (msg.seq !== undefined) seen.set(control, msg.seq);
|
|
85
97
|
const [down, ctl, up] = message.ports;
|
|
86
98
|
if (down) void endpoint.link(msg.key, { down, ctl, up }, msg.routerLock);
|
|
87
99
|
} else if (msg?.type === "unlink") {
|
|
@@ -127,6 +139,7 @@ export function serveHttpInWorker(
|
|
|
127
139
|
await endpoint.close();
|
|
128
140
|
for (const control of controls) control.close();
|
|
129
141
|
controls.clear();
|
|
142
|
+
seen.clear();
|
|
130
143
|
(await held)();
|
|
131
144
|
},
|
|
132
145
|
};
|