@statewalker/webrun-http-browser 0.3.4 → 0.4.2
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/README.md +87 -2
- package/dist/core/data-channels.d.ts.map +1 -1
- package/dist/core/message-target.d.ts +1 -15
- package/dist/core/message-target.d.ts.map +1 -1
- package/dist/http/http-send-recieve.d.ts +27 -14
- package/dist/http/http-send-recieve.d.ts.map +1 -1
- package/dist/index.js +250 -121
- package/dist/relay-sw.js +848 -66
- package/dist/sw/sw-dispatcher.d.ts.map +1 -1
- package/dist/sw-worker.js +848 -64
- package/dist/sw.js +879 -72
- package/package.json +4 -3
- package/src/core/data-channels.ts +42 -4
- package/src/core/message-target.ts +8 -18
- package/src/http/http-send-recieve.ts +27 -14
- package/src/sw/sw-dispatcher.ts +26 -1
- package/LICENSE +0 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/webrun-http-browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "ServiceWorker-based HTTP server for browsers, with relay and same-origin dispatch modes",
|
|
@@ -42,8 +42,9 @@
|
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"idb-keyval": "^6.3.0",
|
|
45
|
-
"@statewalker/webrun-streams": "0.
|
|
46
|
-
"@statewalker/webrun-
|
|
45
|
+
"@statewalker/webrun-http-streams": "0.2.2",
|
|
46
|
+
"@statewalker/webrun-rpc": "0.4.0",
|
|
47
|
+
"@statewalker/webrun-streams": "0.2.0"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@types/node": "^26.2.0",
|
|
@@ -125,11 +125,18 @@ export async function* sendStream<T>(
|
|
|
125
125
|
communicationPort.postMessage({ type: "START_CALL", params }, [messageChannel.port2]);
|
|
126
126
|
|
|
127
127
|
const channel = newStreamChannel<T>(messageChannel.port1);
|
|
128
|
+
let drained = false;
|
|
128
129
|
try {
|
|
129
130
|
await channel.start();
|
|
130
131
|
void channel.sendAll(input);
|
|
131
132
|
yield* channel.recieveAll();
|
|
133
|
+
drained = true;
|
|
132
134
|
} finally {
|
|
135
|
+
// A caller that stops early — an aborted `fetch`, a cancelled response
|
|
136
|
+
// body, a `break` — has to TELL the peer. Closing a `MessagePort` does not
|
|
137
|
+
// notify the other end, so without this the handler on the far side keeps
|
|
138
|
+
// producing into a port nobody reads, for the life of the page.
|
|
139
|
+
if (!drained) channel.cancel();
|
|
133
140
|
await channel.close();
|
|
134
141
|
}
|
|
135
142
|
}
|
|
@@ -168,26 +175,57 @@ export function handleStreams<T>(
|
|
|
168
175
|
interface StreamChannel<T> {
|
|
169
176
|
start(): Promise<void>;
|
|
170
177
|
close(): Promise<void>;
|
|
178
|
+
/** Tell the peer we have stopped reading, so it can release its producer. */
|
|
179
|
+
cancel(): void;
|
|
171
180
|
recieveAll(): AsyncGenerator<T, void, unknown>;
|
|
172
181
|
sendAll(it: AsyncIterable<T>): Promise<void>;
|
|
173
182
|
}
|
|
174
183
|
|
|
184
|
+
/** A data chunk, or — with `cancel` — the peer saying it has stopped reading. */
|
|
185
|
+
type StreamMessage<T> = { done?: boolean; value?: T; error?: unknown; cancel?: boolean };
|
|
186
|
+
|
|
175
187
|
function newStreamChannel<T>(port: MessageTarget): StreamChannel<T> {
|
|
176
|
-
type DataListener = (msg:
|
|
188
|
+
type DataListener = (msg: StreamMessage<T>) => Promise<boolean>;
|
|
177
189
|
let listeners: DataListener[] = [];
|
|
178
190
|
let iterators: AsyncIterable<T>[] = [];
|
|
179
191
|
|
|
180
|
-
const notifyAll = async (data:
|
|
192
|
+
const notifyAll = async (data: StreamMessage<T>) => {
|
|
181
193
|
for (const listener of listeners) await listener(data);
|
|
182
194
|
};
|
|
183
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Release whatever we are sending. NOT awaited: `.return()` on an async
|
|
198
|
+
* generator parked awaiting its own source is queued behind that pending
|
|
199
|
+
* `next()`, so awaiting it here would block the message handler — and the
|
|
200
|
+
* chunk that would unblock it can only arrive through that same handler.
|
|
201
|
+
*/
|
|
202
|
+
const cancelOutgoing = (): void => {
|
|
203
|
+
for (const it of [...iterators]) {
|
|
204
|
+
const iterable = it as AsyncIterable<T> & { return?: () => unknown };
|
|
205
|
+
void Promise.resolve(iterable.return?.()).catch(() => {});
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
184
209
|
const channel = newInvokationChannel({
|
|
185
210
|
port,
|
|
186
|
-
handler: (data: unknown) =>
|
|
211
|
+
handler: (data: unknown) => {
|
|
212
|
+
const message = data as StreamMessage<T>;
|
|
213
|
+
if (message?.cancel) {
|
|
214
|
+
cancelOutgoing();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
return notifyAll(message);
|
|
218
|
+
},
|
|
187
219
|
});
|
|
188
220
|
|
|
189
221
|
const start = () => channel.start();
|
|
190
222
|
|
|
223
|
+
const cancel = (): void => {
|
|
224
|
+
// Best effort by design: the port may already be gone, and a peer that
|
|
225
|
+
// never hears this is no worse off than before the message existed.
|
|
226
|
+
void channel.invoke({ cancel: true }).catch(() => {});
|
|
227
|
+
};
|
|
228
|
+
|
|
191
229
|
const close = async () => {
|
|
192
230
|
await notifyAll({ done: true });
|
|
193
231
|
for (const it of [...iterators]) {
|
|
@@ -220,5 +258,5 @@ function newStreamChannel<T>(port: MessageTarget): StreamChannel<T> {
|
|
|
220
258
|
);
|
|
221
259
|
}
|
|
222
260
|
|
|
223
|
-
return { start, close, recieveAll, sendAll };
|
|
261
|
+
return { start, close, cancel, recieveAll, sendAll };
|
|
224
262
|
}
|
|
@@ -1,18 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** An object we can post messages to (with optional transferable list). */
|
|
11
|
-
export interface MessageSink {
|
|
12
|
-
postMessage(message: unknown, transfer?: Transferable[]): void;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Full-duplex message target: both sends and receives. */
|
|
16
|
-
export interface MessageTarget extends MessageSource, MessageSink {
|
|
17
|
-
close?(): void | Promise<void>;
|
|
18
|
-
}
|
|
1
|
+
// Re-exported from @statewalker/webrun-rpc, which owns the port layer, so this
|
|
2
|
+
// package's five internal importers and its public API are unchanged.
|
|
3
|
+
export type {
|
|
4
|
+
MessageListener,
|
|
5
|
+
MessageSink,
|
|
6
|
+
MessageSource,
|
|
7
|
+
MessageTarget,
|
|
8
|
+
} from "@statewalker/webrun-rpc";
|
|
@@ -39,13 +39,21 @@ async function httpFromIterator<Options>(
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* `
|
|
48
|
-
* `
|
|
42
|
+
* Serve an `HttpHandler` over a `MessageTarget`, using this package's own
|
|
43
|
+
* `handleStreams` transport.
|
|
44
|
+
*
|
|
45
|
+
* @deprecated Prefer the port stack in `@statewalker/webrun-rpc`: open a port
|
|
46
|
+
* (`multiplexPort` over one pipe, or `transferPortMux` where the platform can
|
|
47
|
+
* transfer a real `MessagePort`), turn it into a `Duplex` with
|
|
48
|
+
* `serveDuplexOverPort`, and serve HTTP on that with
|
|
49
|
+
* `httpServe(handler, options)` from `@statewalker/webrun-http-streams`.
|
|
50
|
+
*
|
|
51
|
+
* That path has backpressure; **this one does not.** `sendStream`'s chunk
|
|
52
|
+
* sender discards the promise it is given, so a fast producer over a slow
|
|
53
|
+
* consumer accumulates without bound. It also has no per-stream timeout and no
|
|
54
|
+
* chunking to a transport's message ceiling.
|
|
55
|
+
*
|
|
56
|
+
* Kept for existing ServiceWorker setups built on the `MessageTarget` surface.
|
|
49
57
|
*/
|
|
50
58
|
export function handleHttpRequests(
|
|
51
59
|
communicationPort: MessageTarget,
|
|
@@ -60,13 +68,18 @@ export function handleHttpRequests(
|
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* `
|
|
71
|
+
* Ship a `Request` over a `MessageTarget` and await the `Response`, using this
|
|
72
|
+
* package's own `sendStream` transport.
|
|
73
|
+
*
|
|
74
|
+
* @deprecated Prefer the port stack in `@statewalker/webrun-rpc`: open a port
|
|
75
|
+
* (`multiplexPort` over one pipe, or `transferPortMux` where the platform can
|
|
76
|
+
* transfer a real `MessagePort`), turn it into a `Duplex` with
|
|
77
|
+
* `duplexOverPort`, and drive HTTP over it with `httpFetch` from
|
|
78
|
+
* `@statewalker/webrun-http-streams`.
|
|
79
|
+
*
|
|
80
|
+
* Same caveat as {@link handleHttpRequests}: the transport underneath this
|
|
81
|
+
* helper has no backpressure, no per-stream timeout, and no chunking to a
|
|
82
|
+
* transport's message ceiling. Kept for existing ServiceWorker setups.
|
|
70
83
|
*/
|
|
71
84
|
export async function sendHttpRequest(
|
|
72
85
|
communicationPort: MessageTarget,
|
package/src/sw/sw-dispatcher.ts
CHANGED
|
@@ -47,7 +47,12 @@ export class SwPortHandler {
|
|
|
47
47
|
get serviceWorkerUrl(): string {
|
|
48
48
|
if (!this._serviceWorkerUrl) {
|
|
49
49
|
const url = this.options.serviceWorkerUrl
|
|
50
|
-
?
|
|
50
|
+
? // A worker url is relative to the document that registers it, so it
|
|
51
|
+
// is resolved against `location.href` like any other url a page
|
|
52
|
+
// writes. Without the base, the root-relative form callers actually
|
|
53
|
+
// use — `"/sw-worker.js"` — threw a bare `Invalid URL` naming
|
|
54
|
+
// neither the option nor the value.
|
|
55
|
+
resolveWorkerUrl(this.options.serviceWorkerUrl)
|
|
51
56
|
: new URL("./index-sw.js", this.rootUrl);
|
|
52
57
|
this._serviceWorkerUrl = `${url}`;
|
|
53
58
|
}
|
|
@@ -305,3 +310,23 @@ export class SwPortDispatcher {
|
|
|
305
310
|
return index;
|
|
306
311
|
}
|
|
307
312
|
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Resolve a `serviceWorkerUrl` option the way a page would: relative to the
|
|
316
|
+
* current document. Absolute urls pass through untouched. A url that cannot
|
|
317
|
+
* be resolved is reported with the option name and the offending value,
|
|
318
|
+
* because the raw `TypeError: Failed to construct 'URL': Invalid URL` says
|
|
319
|
+
* neither.
|
|
320
|
+
*/
|
|
321
|
+
function resolveWorkerUrl(serviceWorkerUrl: string): URL {
|
|
322
|
+
const base = globalThis.location?.href;
|
|
323
|
+
try {
|
|
324
|
+
return new URL(serviceWorkerUrl, base);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
`Invalid serviceWorkerUrl: ${JSON.stringify(serviceWorkerUrl)}` +
|
|
328
|
+
(base ? ` (relative to ${base})` : " (no document to resolve it against)"),
|
|
329
|
+
{ cause: error },
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2022-2026 statewalker
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|