@statewalker/webrun-http-browser 0.3.2 → 0.3.4

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.
Files changed (92) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +321 -50
  3. package/demo/demo-1.html +109 -194
  4. package/demo/demo-2.html +119 -580
  5. package/dist/core/data-calls.d.ts +5 -0
  6. package/dist/core/data-calls.d.ts.map +1 -0
  7. package/dist/core/data-channels.d.ts +17 -0
  8. package/dist/core/data-channels.d.ts.map +1 -0
  9. package/dist/core/index.d.ts +6 -0
  10. package/dist/core/index.d.ts.map +1 -0
  11. package/dist/core/message-target.d.ts +16 -0
  12. package/dist/core/message-target.d.ts.map +1 -0
  13. package/dist/core/registry.d.ts +18 -0
  14. package/dist/core/registry.d.ts.map +1 -0
  15. package/dist/http/http-send-recieve.d.ts +23 -0
  16. package/dist/http/http-send-recieve.d.ts.map +1 -0
  17. package/dist/http/index.d.ts +4 -0
  18. package/dist/http/index.d.ts.map +1 -0
  19. package/dist/index.d.ts +4 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +2541 -807
  22. package/dist/relay/index-sw.d.ts +7 -0
  23. package/dist/relay/index-sw.d.ts.map +1 -0
  24. package/dist/relay/index.d.ts +69 -0
  25. package/dist/relay/index.d.ts.map +1 -0
  26. package/dist/relay/split-service-url.d.ts +13 -0
  27. package/dist/relay/split-service-url.d.ts.map +1 -0
  28. package/dist/relay-sw.d.ts +2 -0
  29. package/dist/relay-sw.d.ts.map +1 -0
  30. package/dist/relay-sw.js +878 -0
  31. package/dist/sw/http-sw-dispatcher.d.ts +28 -0
  32. package/dist/sw/http-sw-dispatcher.d.ts.map +1 -0
  33. package/dist/sw/index.d.ts +3 -0
  34. package/dist/sw/index.d.ts.map +1 -0
  35. package/dist/sw/sw-dispatcher.d.ts +74 -0
  36. package/dist/sw/sw-dispatcher.d.ts.map +1 -0
  37. package/dist/sw-worker.d.ts +2 -0
  38. package/dist/sw-worker.d.ts.map +1 -0
  39. package/dist/sw-worker.js +813 -0
  40. package/dist/sw.d.ts +2 -0
  41. package/dist/sw.d.ts.map +1 -0
  42. package/dist/sw.js +1076 -0
  43. package/package.json +50 -37
  44. package/public/index.html +61 -97
  45. package/public/index.js +7 -4
  46. package/public/sw-worker.js +4 -0
  47. package/public-relay/heartbeat.js +4 -3
  48. package/public-relay/relay-sw.js +4 -6
  49. package/public-relay/relay.html +16 -11
  50. package/src/core/data-calls.ts +46 -0
  51. package/src/core/data-channels.ts +224 -0
  52. package/src/core/index.ts +9 -0
  53. package/src/core/message-target.ts +18 -0
  54. package/src/core/registry.ts +54 -0
  55. package/src/http/http-send-recieve.ts +81 -0
  56. package/src/http/index.ts +7 -0
  57. package/src/index.ts +3 -0
  58. package/src/relay/index-sw.ts +145 -0
  59. package/src/relay/index.ts +261 -0
  60. package/src/relay/split-service-url.ts +30 -0
  61. package/src/relay-sw.ts +7 -0
  62. package/src/sw/http-sw-dispatcher.ts +122 -0
  63. package/src/sw/index.ts +2 -0
  64. package/src/sw/sw-dispatcher.ts +307 -0
  65. package/src/sw-worker.ts +7 -0
  66. package/src/sw.ts +1 -0
  67. package/CHANGELOG.md +0 -1
  68. package/demo/demo-3.html +0 -167
  69. package/dist/index-sw-umd.js +0 -798
  70. package/dist/index-sw-umd.min.js +0 -2
  71. package/dist/index-sw.js +0 -791
  72. package/dist/index-sw.min.js +0 -2
  73. package/dist/index-umd.js +0 -853
  74. package/dist/index-umd.min.js +0 -2
  75. package/dist/index.min.js +0 -2
  76. package/index.js +0 -1
  77. package/src/core/data-calls.js +0 -36
  78. package/src/core/data-channels.js +0 -192
  79. package/src/core/data-send-recieve.js +0 -28
  80. package/src/core/errors.js +0 -13
  81. package/src/core/index.js +0 -4
  82. package/src/http/HttpError.js +0 -59
  83. package/src/http/http-send-recieve.js +0 -40
  84. package/src/http/http-stubs.js +0 -135
  85. package/src/http/index.js +0 -3
  86. package/src/http/readable-streams.js +0 -33
  87. package/src/index.js +0 -7
  88. package/src/relay/index-sw.js +0 -216
  89. package/src/relay/index.js +0 -263
  90. package/src/relay/splitServiceUrl.js +0 -24
  91. package/src/sw/http-sw-dispatcher.js +0 -128
  92. package/src/sw/sw-dispatcher.js +0 -264
package/dist/index.js CHANGED
@@ -1,820 +1,2554 @@
1
- // @statewalker/webrun-http-browser v0.3.2 https://github.com/statewalker/statewalker-webrun-http-browser Copyright 2023 Mikhail Kotelnikov
1
+ //#region ../webrun-streams/src/collect.ts
2
+ /** Collect all items from an async iterable into an array. */
3
+ async function collect(input) {
4
+ const items = [];
5
+ for await (const item of input) items.push(item);
6
+ return items;
7
+ }
8
+ /** Concatenate all Uint8Array chunks into a single Uint8Array. */
9
+ async function collectBytes(input) {
10
+ const chunks = [];
11
+ let total = 0;
12
+ for await (const chunk of input) {
13
+ chunks.push(chunk);
14
+ total += chunk.length;
15
+ }
16
+ if (chunks.length === 1) return chunks[0] ?? /* @__PURE__ */ new Uint8Array(0);
17
+ const result = new Uint8Array(total);
18
+ let offset = 0;
19
+ for (const chunk of chunks) {
20
+ result.set(chunk, offset);
21
+ offset += chunk.length;
22
+ }
23
+ return result;
24
+ }
25
+ /** Concatenate all string chunks into a single string. */
26
+ async function collectString(input) {
27
+ let result = "";
28
+ for await (const chunk of input) result += chunk;
29
+ return result;
30
+ }
31
+ //#endregion
32
+ //#region ../webrun-streams/src/errors.ts
2
33
  function serializeError(error) {
3
- return Object.assign({
4
- message: error,
5
- stack: error.stack,
6
- }, error);
34
+ if (error instanceof Error) {
35
+ const out = {
36
+ message: error.message,
37
+ stack: error.stack
38
+ };
39
+ const bag = error;
40
+ for (const key of Object.keys(bag)) out[key] = bag[key];
41
+ return out;
42
+ }
43
+ if (typeof error === "object" && error !== null) {
44
+ const bag = error;
45
+ return {
46
+ message: String(bag.message ?? error),
47
+ ...bag
48
+ };
49
+ }
50
+ return { message: String(error) };
7
51
  }
8
-
9
52
  function deserializeError(error) {
10
- if (typeof error === "string") {
11
- error = { message: error };
12
- }
13
- return Object.assign(new Error(error.message), error);
14
- }
15
-
16
- async function callChannel(mainChannel, callType, params, ...transfers) {
17
- return new Promise((resolve, reject) => {
18
- const channel = new MessageChannel();
19
- channel.port1.onmessage = (ev) => {
20
- const { result, error } = ev.data;
21
- if (error) reject(deserializeError(error));
22
- else resolve(result);
23
- };
24
- mainChannel.postMessage({ type: callType, params }, [
25
- channel.port2,
26
- ...transfers,
27
- ]);
28
- });
29
- }
30
-
31
- function handleChannelCalls(mainChannel, callType, handler) {
32
- const messageListener = async (event) => {
33
- if (!event.data || event.data.type !== callType) return;
34
- const [port, ...transfers] = event.ports;
35
- let response = {};
36
- try {
37
- const params = event.data.params;
38
- response.result = await handler(event, params, ...transfers);
39
- } catch (error) {
40
- response.error = serializeError(error);
41
- }
42
- port.postMessage(response);
43
- };
44
- mainChannel.addEventListener("message", messageListener);
45
- try {
46
- mainChannel.start && mainChannel.start();
47
- } catch (e) {}
48
- return () => mainChannel.removeEventListener("message", messageListener);
49
- }
50
-
51
- // See https://github.com/agenjs/agen-utils/blob/main/src/iterator.js
52
- async function* iterate(init, queue = []) {
53
- let promise, notify, push = async (error, value, done) => {
54
- const slot = { error, value, done };
55
- slot.promise = new Promise(n => slot.notify = n);
56
- await queue.push(slot);
57
- notify && notify();
58
- notify = null;
59
- return slot.promise;
60
- };
61
- const next = (value) => push(undefined, value, false);
62
- const complete = () => push(undefined, undefined, true);
63
- const error = err => push(err, undefined, true);
64
- const unsubscribe = init(Object.assign([next, complete, error], { next, complete, error }));
65
- let slot;
66
- try {
67
- while (true) {
68
- slot = await queue.shift();
69
- if (slot) {
70
- try {
71
- if (slot.error) { throw slot.error; }
72
- else if (slot.done) { break; }
73
- else { yield slot.value; }
74
- } finally {
75
- slot.notify(true);
76
- }
77
- } else {
78
- await (promise = notify ? promise : new Promise(n => notify = n));
79
- }
80
- }
81
- } finally {
82
- notify && notify();
83
- push = () => false; // Stop pushing in the queue...
84
- ((typeof unsubscribe === 'function') && (await unsubscribe()));
85
- while (slot = await queue.shift()) { slot.notify(false); }
86
- }
87
- }
88
-
89
- function newRegistry(onError = console.error) {
90
- let counter = 0;
91
- const registrations = {};
92
- const register = (action) => {
93
- const id = counter++;
94
- return (registrations[id] = Object.assign(
95
- function (skip) {
96
- try {
97
- delete registrations[id];
98
- return !skip && action && action();
99
- } catch (error) {
100
- onError(error);
101
- }
102
- },
103
- { action }
104
- ));
105
- };
106
- const unregister = (action) =>
107
- Object.values(registrations).forEach((r) => r.action === action && r(true));
108
- const clear = () => Object.values(registrations).forEach((r) => r());
109
- return Object.assign([register, clear, unregister], {
110
- register,
111
- clear,
112
- unregister
113
- });
114
- }
115
-
116
- async function* recieveData(onMessage) {
117
- yield* iterate((iterator) => {
118
- return onMessage(async ({ done = true, value, error } = {}) => {
119
- if (error) {
120
- await iterator.error(error);
121
- } else if (done) {
122
- await iterator.complete();
123
- } else {
124
- await iterator.next(value);
125
- }
126
- });
127
- });
128
- }
129
-
130
- async function sendData(send, it) {
131
- let error;
132
- try {
133
- for await (let value of it) {
134
- await send({ done: false, value });
135
- }
136
- } catch (err) {
137
- error = err;
138
- } finally {
139
- await send({ done: true, error });
140
- }
141
- }
142
-
143
- async function* sendStream(communicationPort, input, params = {}) {
144
- const messageChannel = new MessageChannel();
145
- communicationPort.postMessage({ type: "START_CALL", params }, [
146
- messageChannel.port2,
147
- ]);
148
-
149
- const channel = newChannel(messageChannel.port1);
150
- try {
151
- await channel.start();
152
- channel.sendAll(input);
153
- yield* channel.recieveAll();
154
- } finally {
155
- await channel.close();
156
- }
157
- }
158
-
159
- function handleStreams(communicationPort, handler) {
160
- const listener = async (event) => {
161
- const { type, params } = event.data || {};
162
- if (type !== "START_CALL") return;
163
- const port = event.ports[0];
164
- const channel = newChannel(port);
165
- try {
166
- await channel.start();
167
- const input = channel.recieveAll();
168
- const response = await handler(input, params);
169
- await channel.sendAll(response);
170
- } finally {
171
- await channel.close();
172
- }
173
- };
174
- communicationPort.addEventListener("message", listener);
175
- try {
176
- communicationPort.start && communicationPort.start();
177
- } catch (e) {}
178
- return () => communicationPort.removeEventListener("message", listener);
179
- }
180
-
181
- /**
182
- * This method creates and returns methods allowing to send and recieve data over the specified port.
183
- * The main characteristics of the returned methods is that they properly manage backpressure - sender
184
- * iterators are waiting to send the next value until the previous one is consumed on the other side.
185
- * @param {*} port
186
- * @returns an object with the following methods:
187
- * * `start` - starts the connection over the specified port
188
- * * `close`- stops all iterators and closes the port
189
- * * `recieveAll() : AsyncIterator` - returns an AsyncIterator over all values send by the peer
190
- * * `sendAll(it : AsyncIterator) : Promise<any>` - sends all values defined by the given
191
- * AsyncIterator to the peer
192
- */
193
- function newChannel(port) {
194
- let listeners = [];
195
- let iterators = [];
196
-
197
- const notifyAll = async (data) => {
198
- for (let listener of listeners) {
199
- await listener(data);
200
- }
201
- };
202
- const channel = newInvokationChannel({
203
- port,
204
- handler: notifyAll,
205
- });
206
-
207
- const start = () => channel.start();
208
-
209
- const close = async () => {
210
- await notifyAll({ done: true });
211
- [...iterators].forEach((it) => it.return && it.return());
212
- await channel.close();
213
- };
214
-
215
- async function* recieveAll() {
216
- yield* recieveData((listener) => {
217
- listeners.push(listener);
218
- return () => listeners = listeners.filter((l) => l !== listener);
219
- });
220
- }
221
-
222
- async function sendAll(it) {
223
- await sendData(
224
- channel.invoke,
225
- (async function* () {
226
- try {
227
- iterators.push(it);
228
- yield* it;
229
- } finally {
230
- iterators = iterators.filter((i) => i !== it);
231
- }
232
- })(),
233
- );
234
- }
235
- return {
236
- start,
237
- close,
238
- recieveAll,
239
- sendAll,
240
- };
241
- }
242
-
243
- function newInvokationChannel({
244
- port,
245
- handler = () => {
246
- throw new Error("Handler not implemented");
247
- },
248
- onError = console.error,
249
- newCallId = () =>
250
- newInvokationChannel.__counter = (newInvokationChannel.__counter || 0) + 1,
251
- }) {
252
- const MESSAGE_TYPE_REQUEST = "REQUEST";
253
- const MESSAGE_TYPE_RESPONSE = "RESPONSE";
254
- const EVENT_MESSAGE = "message";
255
-
256
- const requests = {};
257
- const listener = async (event) => {
258
- const data = event.data || {};
259
- const { type, callId } = data;
260
- if (type === MESSAGE_TYPE_REQUEST) {
261
- try {
262
- const { request } = data;
263
- let result = await handler(request, ...(event.ports || []));
264
- const [response, ...transfers] = Array.isArray(result)
265
- ? result
266
- : result !== undefined
267
- ? [result]
268
- : [];
269
- port.postMessage(
270
- { type: MESSAGE_TYPE_RESPONSE, callId, response },
271
- transfers,
272
- );
273
- } catch (error) {
274
- port.postMessage({
275
- type: MESSAGE_TYPE_RESPONSE,
276
- callId,
277
- error: serializeError(error),
278
- });
279
- }
280
- } else if (type === MESSAGE_TYPE_RESPONSE) {
281
- const { callId, response, error } = data;
282
- const request = requests[callId];
283
- delete requests[callId];
284
- if (request) {
285
- error
286
- ? request.reject(deserializeError(error))
287
- : request.resolve(response);
288
- }
289
- }
290
- };
291
-
292
- const start = async () => {
293
- try {
294
- port.addEventListener(EVENT_MESSAGE, listener);
295
- port.start && await port.start();
296
- } catch (e) {
297
- onError(e);
298
- }
299
- };
300
-
301
- const close = async () => {
302
- try {
303
- port.removeEventListener(EVENT_MESSAGE, listener);
304
- port.close && await port.close();
305
- } catch (e) {
306
- onError(e);
307
- }
308
- };
309
-
310
- const invoke = async function (request = {}, ...transfers) {
311
- const callId = newCallId();
312
- return new Promise((resolve, reject) => {
313
- try {
314
- requests[callId] = { resolve, reject };
315
- port.postMessage(
316
- { type: MESSAGE_TYPE_REQUEST, callId, request },
317
- transfers,
318
- );
319
- } catch (error) {
320
- reject(error);
321
- delete request[callId];
322
- }
323
- });
324
- };
325
-
326
- return {
327
- start,
328
- close,
329
- invoke,
330
- };
331
- }
332
-
53
+ const payload = typeof error === "string" ? { message: error } : error;
54
+ return Object.assign(new Error(payload.message), payload);
55
+ }
56
+ //#endregion
57
+ //#region ../webrun-streams/src/emulate-mux.ts
58
+ /**
59
+ * Thrown by `emulateMux` and adapters when the underlying transport closes
60
+ * while one or more `Duplex` calls are in flight. Consumers can catch by
61
+ * `instanceof TransportClosedError` or by checking `error.name`.
62
+ */
63
+ var TransportClosedError = class extends Error {
64
+ name = "TransportClosedError";
65
+ constructor(message = "transport closed") {
66
+ super(message);
67
+ }
68
+ };
69
+ const TYPE_OPEN = 1;
70
+ const TYPE_DATA = 2;
71
+ const TYPE_ACK = 3;
72
+ const TYPE_END = 4;
73
+ const TYPE_ERROR = 5;
74
+ const TYPE_CLOSE = 6;
75
+ const DEFAULT_MAX_STREAMS = 256;
76
+ const DEFAULT_MTU = 65536;
77
+ const DEFAULT_MAX_STREAM_BUFFER = 8388608;
78
+ const textEncoder = new TextEncoder();
79
+ const textDecoder = new TextDecoder();
80
+ function emulateMux(channel, opts = {}) {
81
+ const maxStreams = opts.maxStreams ?? DEFAULT_MAX_STREAMS;
82
+ const mtu = opts.mtu ?? DEFAULT_MTU;
83
+ const maxStreamBuffer = opts.maxStreamBuffer ?? DEFAULT_MAX_STREAM_BUFFER;
84
+ const side = opts.side ?? "initiator";
85
+ const streams = /* @__PURE__ */ new Map();
86
+ let nextLocalId = side === "initiator" ? 2 : 1;
87
+ let handler = null;
88
+ let muxClosed = false;
89
+ const sendFrame = (id, type, payload) => {
90
+ if (muxClosed) return;
91
+ const idEnc = encodeVarint(id);
92
+ const total = idEnc.length + 1 + (payload?.byteLength ?? 0);
93
+ const frame = new Uint8Array(total);
94
+ frame.set(idEnc, 0);
95
+ frame[idEnc.length] = type;
96
+ if (payload && payload.byteLength > 0) frame.set(payload, idEnc.length + 1);
97
+ try {
98
+ channel.send(frame);
99
+ } catch {}
100
+ };
101
+ const teardownStream = (s, err) => {
102
+ if (s.closed) return;
103
+ s.closed = true;
104
+ const resolve = s.resolveAck;
105
+ const reject = s.rejectAck;
106
+ s.resolveAck = null;
107
+ s.rejectAck = null;
108
+ if (reject && err) reject(err);
109
+ else resolve?.();
110
+ s.doneIn(err);
111
+ streams.delete(s.id);
112
+ };
113
+ /**
114
+ * Graceful counterpart to `teardownStream`. A stream occupies a slot in
115
+ * `streams` until BOTH directions finish; releasing on inbound END alone
116
+ * would set `closed` while our own `pumpOutbound` is still sending, and it
117
+ * checks that flag to decide whether to keep going.
118
+ *
119
+ * Without this, only abnormal endings (cancel, ERROR, CLOSE, transport
120
+ * failure) ever freed a slot, so every normally-completed call leaked one
121
+ * until `maxStreams` began rejecting new calls.
122
+ */
123
+ const releaseIfComplete = (s) => {
124
+ if (s.closed || !s.inDone || !s.outDone) return;
125
+ s.closed = true;
126
+ streams.delete(s.id);
127
+ };
128
+ const failAll = (err) => {
129
+ if (muxClosed) return;
130
+ muxClosed = true;
131
+ for (const s of [...streams.values()]) teardownStream(s, err);
132
+ streams.clear();
133
+ };
134
+ const createStream = (id) => {
135
+ const queue = makeInboundQueue();
136
+ const state = {
137
+ id,
138
+ inbound: queue.generator(() => {
139
+ if (!state.closed && !muxClosed) sendFrame(state.id, TYPE_CLOSE);
140
+ teardownStream(state);
141
+ }),
142
+ pushIn: queue.push,
143
+ doneIn: queue.done,
144
+ resolveAck: null,
145
+ rejectAck: null,
146
+ closed: false,
147
+ inDone: false,
148
+ outDone: false,
149
+ queuedBytes: 0
150
+ };
151
+ return state;
152
+ };
153
+ const pumpOutbound = async (s, input) => {
154
+ try {
155
+ for await (const chunk of input) {
156
+ if (s.closed || muxClosed) return;
157
+ if (chunk.byteLength === 0) continue;
158
+ let off = 0;
159
+ while (off < chunk.byteLength) {
160
+ if (s.closed || muxClosed) return;
161
+ const end = Math.min(off + mtu, chunk.byteLength);
162
+ const piece = chunk.subarray(off, end);
163
+ sendFrame(s.id, TYPE_DATA, piece);
164
+ await new Promise((resolve, reject) => {
165
+ s.resolveAck = resolve;
166
+ s.rejectAck = reject;
167
+ });
168
+ off = end;
169
+ }
170
+ }
171
+ if (!s.closed && !muxClosed) {
172
+ sendFrame(s.id, TYPE_END);
173
+ s.outDone = true;
174
+ releaseIfComplete(s);
175
+ }
176
+ } catch (err) {
177
+ if (!s.closed && !muxClosed) {
178
+ const e = err instanceof Error ? err : new Error(String(err));
179
+ sendFrame(s.id, TYPE_ERROR, encodeError(e));
180
+ teardownStream(s, e);
181
+ }
182
+ }
183
+ };
184
+ const runHandler = async (s, h) => {
185
+ let outbound;
186
+ try {
187
+ outbound = h(s.inbound);
188
+ } catch (err) {
189
+ const e = err instanceof Error ? err : new Error(String(err));
190
+ sendFrame(s.id, TYPE_ERROR, encodeError(e));
191
+ teardownStream(s, e);
192
+ return;
193
+ }
194
+ await pumpOutbound(s, outbound);
195
+ if (!s.inDone && !s.closed && !muxClosed) {
196
+ sendFrame(s.id, TYPE_CLOSE);
197
+ teardownStream(s);
198
+ }
199
+ };
200
+ const handleFrame = (frame) => {
201
+ if (muxClosed) return;
202
+ if (frame.byteLength < 2) return;
203
+ let id;
204
+ let offset;
205
+ try {
206
+ ({value: id, offset} = decodeVarint(frame, 0));
207
+ } catch {
208
+ return;
209
+ }
210
+ if (offset >= frame.byteLength) return;
211
+ const type = frame[offset];
212
+ const payload = frame.subarray(offset + 1);
213
+ if (type === TYPE_OPEN) {
214
+ if (streams.has(id)) return;
215
+ if (streams.size >= maxStreams) {
216
+ sendFrame(id, TYPE_ERROR, encodeError(/* @__PURE__ */ new RangeError(`emulateMux: maxStreams=${maxStreams} exceeded`)));
217
+ return;
218
+ }
219
+ if (!handler) {
220
+ sendFrame(id, TYPE_ERROR, encodeError(/* @__PURE__ */ new Error("emulateMux: no handler registered")));
221
+ return;
222
+ }
223
+ const s = createStream(id);
224
+ streams.set(id, s);
225
+ runHandler(s, handler);
226
+ return;
227
+ }
228
+ const s = streams.get(id);
229
+ if (!s) return;
230
+ switch (type) {
231
+ case TYPE_DATA: {
232
+ s.queuedBytes += payload.byteLength;
233
+ if (s.queuedBytes > maxStreamBuffer) {
234
+ const err = /* @__PURE__ */ new RangeError(`emulateMux: stream ${id} buffered ${s.queuedBytes} bytes past maxStreamBuffer=${maxStreamBuffer} without being drained`);
235
+ sendFrame(id, TYPE_ERROR, encodeError(err));
236
+ teardownStream(s, err);
237
+ return;
238
+ }
239
+ const copy = payload.byteLength === 0 ? payload : new Uint8Array(payload);
240
+ s.pushIn(copy).then((handled) => {
241
+ s.queuedBytes -= copy.byteLength;
242
+ if (handled && !s.closed && !muxClosed) sendFrame(id, TYPE_ACK);
243
+ });
244
+ return;
245
+ }
246
+ case TYPE_ACK: {
247
+ const r = s.resolveAck;
248
+ s.resolveAck = null;
249
+ s.rejectAck = null;
250
+ r?.();
251
+ return;
252
+ }
253
+ case TYPE_END:
254
+ s.inDone = true;
255
+ s.doneIn();
256
+ releaseIfComplete(s);
257
+ return;
258
+ case TYPE_ERROR:
259
+ teardownStream(s, decodeError(payload));
260
+ return;
261
+ case TYPE_CLOSE:
262
+ teardownStream(s);
263
+ return;
264
+ default: return;
265
+ }
266
+ };
267
+ (async () => {
268
+ try {
269
+ for await (const frame of channel.recv) {
270
+ if (muxClosed) break;
271
+ handleFrame(frame);
272
+ }
273
+ } catch (err) {
274
+ failAll(err instanceof Error ? err : new Error(String(err)));
275
+ return;
276
+ }
277
+ failAll(new TransportClosedError());
278
+ })();
279
+ channel.closed.then(() => failAll(new TransportClosedError())).catch(() => {});
280
+ const call = (input) => {
281
+ if (muxClosed) return failedGenerator(new TransportClosedError());
282
+ if (streams.size >= maxStreams) return failedGenerator(/* @__PURE__ */ new RangeError(`emulateMux: maxStreams=${maxStreams} exceeded`));
283
+ const id = nextLocalId;
284
+ nextLocalId += 2;
285
+ const s = createStream(id);
286
+ streams.set(id, s);
287
+ sendFrame(id, TYPE_OPEN);
288
+ pumpOutbound(s, input);
289
+ return s.inbound;
290
+ };
291
+ const serve = (h) => {
292
+ handler = h;
293
+ let torn = false;
294
+ return async () => {
295
+ if (torn) return;
296
+ torn = true;
297
+ if (handler === h) handler = null;
298
+ };
299
+ };
300
+ const close = async () => {
301
+ if (muxClosed) return;
302
+ failAll(new TransportClosedError());
303
+ try {
304
+ channel.close();
305
+ } catch {}
306
+ };
307
+ return {
308
+ call,
309
+ serve,
310
+ close
311
+ };
312
+ }
313
+ function failedGenerator(err) {
314
+ return (async function* () {
315
+ throw err;
316
+ })();
317
+ }
318
+ /**
319
+ * Push/pull queue with eager push/done handles. Unlike `newAsyncGenerator`, the
320
+ * push and done functions are usable *before* the consumer begins iterating —
321
+ * `emulateMux` needs to enqueue frames from inbound traffic regardless of when
322
+ * (or whether) the consumer pulls them.
323
+ *
324
+ * `onCancel` fires only when the consumer terminates the generator before
325
+ * `done()` was called — i.e., a unilateral cancellation. If `done()` is
326
+ * called first (peer END/ERROR/CLOSE), the generator ends naturally and
327
+ * `onCancel` does not fire.
328
+ */
329
+ function makeInboundQueue() {
330
+ const slots = [];
331
+ let wake = null;
332
+ let queueClosed = false;
333
+ let doneCalled = false;
334
+ const push = (chunk) => {
335
+ if (queueClosed) return Promise.resolve(false);
336
+ return new Promise((resolve) => {
337
+ slots.push({
338
+ type: "value",
339
+ value: chunk,
340
+ resolve
341
+ });
342
+ wake?.();
343
+ });
344
+ };
345
+ const done = (err) => {
346
+ if (queueClosed || doneCalled) return Promise.resolve(false);
347
+ doneCalled = true;
348
+ return new Promise((resolve) => {
349
+ slots.push({
350
+ type: "done",
351
+ err,
352
+ resolve
353
+ });
354
+ wake?.();
355
+ });
356
+ };
357
+ async function* generator(onCancel) {
358
+ try {
359
+ while (true) {
360
+ if (slots.length === 0) {
361
+ await new Promise((r) => {
362
+ wake = r;
363
+ });
364
+ wake = null;
365
+ continue;
366
+ }
367
+ const slot = slots.shift();
368
+ if (slot.type === "done") {
369
+ slot.resolve(true);
370
+ if (slot.err) throw slot.err;
371
+ return;
372
+ }
373
+ yield slot.value;
374
+ slot.resolve(true);
375
+ }
376
+ } finally {
377
+ queueClosed = true;
378
+ for (const s of slots) s.resolve(false);
379
+ slots.length = 0;
380
+ if (!doneCalled) onCancel();
381
+ }
382
+ }
383
+ return {
384
+ generator,
385
+ push,
386
+ done
387
+ };
388
+ }
389
+ function encodeVarint(value) {
390
+ if (!Number.isInteger(value) || value < 0) throw new RangeError(`encodeVarint: ${value} is not a non-negative integer`);
391
+ const out = [];
392
+ let v = value;
393
+ while (v >= 128) {
394
+ out.push(v & 127 | 128);
395
+ v >>>= 7;
396
+ }
397
+ out.push(v & 127);
398
+ return new Uint8Array(out);
399
+ }
400
+ function decodeVarint(buf, start) {
401
+ let value = 0;
402
+ let shift = 0;
403
+ let i = start;
404
+ while (i < buf.length) {
405
+ const b = buf[i++];
406
+ value |= (b & 127) << shift;
407
+ if ((b & 128) === 0) return {
408
+ value: value >>> 0,
409
+ offset: i
410
+ };
411
+ shift += 7;
412
+ if (shift > 28) throw new Error("decodeVarint: too long");
413
+ }
414
+ throw new Error("decodeVarint: truncated");
415
+ }
416
+ function encodeError(err) {
417
+ return textEncoder.encode(JSON.stringify(serializeError(err)));
418
+ }
419
+ function decodeError(buf) {
420
+ if (buf.byteLength === 0) return /* @__PURE__ */ new Error("unknown stream error");
421
+ try {
422
+ return deserializeError(JSON.parse(textDecoder.decode(buf)));
423
+ } catch {
424
+ return new Error(textDecoder.decode(buf));
425
+ }
426
+ }
427
+ //#endregion
428
+ //#region ../webrun-streams/src/lines.ts
429
+ /** Split a stream of string chunks into individual lines (delimited by \n). */
430
+ async function* splitLines(input) {
431
+ let buffer = "";
432
+ for await (const chunk of input) {
433
+ buffer += chunk;
434
+ let idx = buffer.indexOf("\n");
435
+ while (idx !== -1) {
436
+ yield buffer.slice(0, idx);
437
+ buffer = buffer.slice(idx + 1);
438
+ idx = buffer.indexOf("\n");
439
+ }
440
+ }
441
+ if (buffer) yield buffer;
442
+ }
443
+ /** Append \n to each string in the stream. */
444
+ async function* joinLines(input) {
445
+ for await (const line of input) yield `${line}\n`;
446
+ }
447
+ //#endregion
448
+ //#region ../webrun-streams/src/map.ts
449
+ /** Apply a sync or async function to each item in a stream. */
450
+ async function* map(input, fn) {
451
+ for await (const item of input) yield await fn(item);
452
+ }
453
+ //#endregion
454
+ //#region ../webrun-streams/src/jsonl.ts
455
+ /** Encode each value as a JSON line (terminated by \n). */
456
+ async function* encodeJsonl(input) {
457
+ yield* map(input, (item) => `${JSON.stringify(item)}\n`);
458
+ }
459
+ /** Decode each line as a JSON value. Skips empty lines. */
460
+ async function* decodeJsonl(input) {
461
+ for await (const line of splitLines(input)) {
462
+ const trimmed = line.trim();
463
+ if (trimmed) yield JSON.parse(trimmed);
464
+ }
465
+ }
466
+ //#endregion
467
+ //#region ../webrun-streams/src/new-async-generator.ts
468
+ /**
469
+ * The newAsyncGenerator function creates async generators from callback-based initialization
470
+ * functions, providing a bridge between imperative event handling and declarative async
471
+ * iteration patterns.
472
+ *
473
+ * The initialization function receives two callback functions that control the generator's
474
+ * behavior. The `next` function yields values to consumers, while the `done` function
475
+ * signals completion or error conditions. The initialization function can return a cleanup
476
+ * function that will be called when the generator is closed or an error occurs.
477
+ *
478
+ * The generator manages an internal queue of values and completion signals, ensuring that
479
+ * producers can yield values without overwhelming consumers. Proper backpressure is implemented
480
+ * by having the `next` and `done` functions return promises that resolve only after the
481
+ * consumer has processed the values. This prevents memory leaks and ensures that producers
482
+ * are aware of whether their values were successfully handled.
483
+ * The generator also handles cleanup by draining any remaining items in the queue and notifying
484
+ * producers that their values were not processed if the generator is closed early. This ensures
485
+ * that resources are properly managed and that no memory leaks occur.
486
+ *
487
+ * Example usage:
488
+ * ```typescript
489
+ * const asyncGen = newAsyncGenerator<number>((next, done) => {
490
+ * let count = 0;
491
+ * const interval = setInterval(() => {
492
+ * if (count < 5) {
493
+ * next(count++);
494
+ * } else {
495
+ * done();
496
+ * clearInterval(interval);
497
+ * }
498
+ * }, 1000);
499
+ * return () => clearInterval(interval); // Cleanup function
500
+ * });
501
+ * (async () => {
502
+ * for await (const num of asyncGen) {
503
+ * console.log(num); // Logs numbers 0 to 4 at 1 second intervals
504
+ * }
505
+ * console.log("Completed");
506
+ * })();
507
+ * ```
508
+ * @template T The type of values yielded by the generator
509
+ * @template E The type of errors that can be thrown; defaults to Error
510
+ * @param init Initialization function that sets up the generator behavior with next/done callbacks;
511
+ * The first parameter - `next` function - is used to yield values to consumers;
512
+ * It returns a Promise<boolean> indicating whether the value was successfully handled;
513
+ * The second parameter - `done` function - is used to signal completion or error;
514
+ * It returns a Promise<boolean> indicating whether the completion was successfully handled;
515
+ * The initialization function can optionally return a cleanup function that will be called
516
+ * when the generator is closed or an error occurs;
517
+ * @param skipValues If true, only the most recent value is kept in the queue,
518
+ * skipping intermediate values (not consumed values are considered skipped);
519
+ * This is useful for scenarios where only the latest value matters, such as UI updates.
520
+ * Defaults to false, meaning all values are queued and processed in order.
521
+ * @returns AsyncGenerator that properly manages backpressure and resource cleanup
522
+ */
523
+ async function* newAsyncGenerator(init, skipValues = false) {
524
+ let head;
525
+ let tail;
526
+ /** Flag to prevent new values from being queued after generator closes */
527
+ let closed = false;
528
+ /** Wake-up function to notify the generator loop of new items */
529
+ let wakeUp;
530
+ /**
531
+ * Drains the internal queue, notifying all pending producers that their values
532
+ * were not processed due to generator closure. This prevents memory leaks and
533
+ * ensures proper backpressure signaling.
534
+ */
535
+ const drainQueue = () => {
536
+ for (; head; head = head.next) {
537
+ closed = closed || head.done;
538
+ head.resolve(false);
539
+ }
540
+ tail = void 0;
541
+ };
542
+ /**
543
+ * Enqueues a value or completion signal with a promise that resolves when the item
544
+ * is processed. This enables backpressure by allowing producers to know when their
545
+ * values have been consumed.
546
+ */
547
+ const enqueue = (params) => {
548
+ if (skipValues) drainQueue();
549
+ return !closed ? new Promise((resolve) => {
550
+ const next = {
551
+ ...params,
552
+ next: void 0,
553
+ resolve
554
+ };
555
+ if (tail) tail.next = next;
556
+ tail = next;
557
+ if (!head) head = tail;
558
+ wakeUp?.();
559
+ }) : Promise.resolve(false);
560
+ };
561
+ /**
562
+ * Producer function to yield a value to consumers. Returns a promise that resolves
563
+ * to true if the value was successfully processed, false if the generator is closed
564
+ * or the value was skipped due to backpressure.
565
+ */
566
+ const next = (value) => enqueue({
567
+ done: false,
568
+ value
569
+ });
570
+ /**
571
+ * Producer function to signal completion or error. Optional error parameter will
572
+ * cause the generator to throw that error to consumers.
573
+ */
574
+ const done = (error) => enqueue({
575
+ done: true,
576
+ error
577
+ });
578
+ const unsubscribe = init(next, done);
579
+ try {
580
+ while (!closed) {
581
+ if (!head) {
582
+ await new Promise((resolve) => {
583
+ wakeUp = resolve;
584
+ }).then(() => {
585
+ wakeUp = void 0;
586
+ });
587
+ continue;
588
+ }
589
+ const slot = head;
590
+ head = head.next;
591
+ if (tail === slot) tail = head;
592
+ try {
593
+ if (slot.done) {
594
+ closed = true;
595
+ if (slot.error !== void 0) throw slot.error;
596
+ break;
597
+ }
598
+ yield slot.value;
599
+ } finally {
600
+ slot.resolve(true);
601
+ }
602
+ }
603
+ } finally {
604
+ /**
605
+ * Cleanup phase - ensures proper resource management and notification of any
606
+ * remaining producers. This runs whether the generator completes normally,
607
+ * encounters an error, or is closed early by the consumer.
608
+ */
609
+ closed = true;
610
+ wakeUp?.();
611
+ if (typeof unsubscribe === "function") await unsubscribe();
612
+ /**
613
+ * Drain any remaining items in the queue and notify their producers that
614
+ * the values were not processed. This prevents memory leaks and ensures
615
+ * proper backpressure signaling.
616
+ */
617
+ drainQueue();
618
+ }
619
+ }
620
+ //#endregion
621
+ //#region ../webrun-streams/src/normalize.ts
622
+ const utf8 = new TextEncoder();
623
+ /**
624
+ * Coerce common byte-like inputs into a `Uint8Array`. Strings encode as UTF-8.
625
+ * Blobs return a `Promise<Uint8Array>`; every other input returns synchronously.
626
+ * Throws `TypeError` for anything else.
627
+ */
628
+ function normalizeToUint8Array(data) {
629
+ if (data instanceof Uint8Array) return data;
630
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
631
+ if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
632
+ if (typeof Blob !== "undefined" && data instanceof Blob) return data.arrayBuffer().then((buf) => new Uint8Array(buf));
633
+ if (typeof data === "string") return utf8.encode(data);
634
+ throw new TypeError(`normalizeToUint8Array: unsupported input ${describe(data)}; expected Uint8Array, ArrayBuffer, ArrayBufferView, Blob, or string`);
635
+ }
636
+ function describe(value) {
637
+ if (value === null) return "null";
638
+ const t = typeof value;
639
+ if (t !== "object") return t;
640
+ return value.constructor?.name ?? "object";
641
+ }
642
+ //#endregion
643
+ //#region ../webrun-streams/src/readable-streams.ts
333
644
  function toReadableStream(it) {
334
- return new ReadableStream({
335
- type: "bytes",
336
- async pull(controller) {
337
- let handled = false;
338
- try {
339
- while (true) {
340
- const slot = await it.next();
341
- if (!slot || slot.done) {
342
- break;
343
- }
344
- controller.enqueue(await slot.value);
345
- }
346
- } catch (error) {
347
- handled = true;
348
- controller.error(error);
349
- } finally {
350
- !handled && controller.close();
351
- }
352
- },
353
- });
354
- }
355
-
645
+ return new ReadableStream({ async pull(controller) {
646
+ let handled = false;
647
+ try {
648
+ while (true) {
649
+ const slot = await it.next();
650
+ if (!slot || slot.done) break;
651
+ const value = await slot.value;
652
+ controller.enqueue(value);
653
+ }
654
+ } catch (error) {
655
+ handled = true;
656
+ controller.error(error);
657
+ } finally {
658
+ if (!handled) controller.close();
659
+ }
660
+ } });
661
+ }
356
662
  async function* fromReadableStream(stream) {
357
- const reader = await stream.getReader();
358
- let done, value;
359
- while (({ done, value } = await reader.read())) {
360
- if (done) {
361
- break;
362
- }
363
- yield await value;
364
- }
365
- }
366
-
367
- /**
368
- * This method returns an HTTP handlers transforming
369
- * HTTP Request instances to binary representations and
370
- * sending them to a transport layer.
371
- * Results of these transport ivokations are deserialized
372
- * as meaningful Response instances and returned back to the caller.
373
- *
374
- * The specified `send` method is used to dispatch serialized
375
- * requests to return the serialized remote results.
376
- * This transport method accepts and returns objects with
377
- * the same structure:
378
- * * `options` - contains serialized request or response parameters
379
- * * `content` - an async iterator over binary blocks for
380
- * the request or response
381
- *
382
- * @param {*} send transport function accepting serialized requests
383
- * and returning back serialized responses
384
- * @returns an async handler accepting standard
385
- * HTTP `Request` instances and returning `Response` instances
386
- */
663
+ const reader = stream.getReader();
664
+ while (true) {
665
+ const { done, value } = await reader.read();
666
+ if (done) break;
667
+ if (value !== void 0) yield value;
668
+ }
669
+ }
670
+ //#endregion
671
+ //#region ../webrun-streams/src/recieve-iterator.ts
672
+ /**
673
+ * Inverse of {@link sendIterator}: turns a sequence of `{done, value, error}`
674
+ * chunks (delivered to the supplied callback by `installer`) into an async
675
+ * generator.
676
+ *
677
+ * The `installer` is given a `deliver` function. It should call `deliver`
678
+ * once for each incoming chunk and may return a cleanup callback that
679
+ * runs when the consumer stops iterating.
680
+ */
681
+ function recieveIterator(installer) {
682
+ return newAsyncGenerator((next, done) => {
683
+ const cleanup = installer(async (chunk = { done: true }) => {
684
+ const { done: isDone = true, value, error } = chunk;
685
+ if (error) return await done(error);
686
+ if (isDone) return await done();
687
+ return await next(value);
688
+ });
689
+ if (cleanup) return async () => {
690
+ await cleanup();
691
+ };
692
+ });
693
+ }
694
+ //#endregion
695
+ //#region ../webrun-streams/src/send-iterator.ts
696
+ /**
697
+ * Drain an async iterator into a sink that consumes one chunk at a time.
698
+ *
699
+ * Each yielded value becomes `{ done: false, value }`. Completion emits
700
+ * `{ done: true }`. If the iterator throws, the error is caught and the
701
+ * final `done` chunk carries it so the peer can rethrow on its side.
702
+ */
703
+ async function sendIterator(send, it) {
704
+ let error;
705
+ try {
706
+ for await (const value of it) await send({
707
+ done: false,
708
+ value
709
+ });
710
+ } catch (err) {
711
+ error = err;
712
+ } finally {
713
+ await send({
714
+ done: true,
715
+ error
716
+ });
717
+ }
718
+ }
719
+ //#endregion
720
+ //#region ../webrun-streams/src/text.ts
721
+ /** Decode Uint8Array chunks to string chunks via TextDecoder, handling split multi-byte characters. */
722
+ async function* decodeText(input) {
723
+ const decoder = new TextDecoder("utf-8", { fatal: false });
724
+ for await (const chunk of input) {
725
+ const text = decoder.decode(chunk, { stream: true });
726
+ if (text) yield text;
727
+ }
728
+ const tail = decoder.decode();
729
+ if (tail) yield tail;
730
+ }
731
+ /** Encode string chunks to Uint8Array chunks via TextEncoder. */
732
+ async function* encodeText(input) {
733
+ const encoder = new TextEncoder();
734
+ for await (const str of input) yield encoder.encode(str);
735
+ }
736
+ //#endregion
737
+ //#region ../webrun-streams/src/to-chunks.ts
738
+ const DEFAULT_CHUNK_SIZE = 16384;
739
+ /**
740
+ * Curried `Duplex`-shaped transformer that splits incoming `Uint8Array`s into
741
+ * chunks no larger than `size` bytes. Empty input chunks are skipped; small
742
+ * input chunks pass through unchanged (zero-copy `subarray` views are used
743
+ * when splitting, so no allocation per chunk).
744
+ *
745
+ * Use to respect transport MTUs before reaching `channel.send`. Reassembly on
746
+ * the receive side is not needed — consumers iterate a continuous byte stream.
747
+ */
748
+ function toChunks(size = DEFAULT_CHUNK_SIZE) {
749
+ if (!(Number.isInteger(size) && size > 0)) throw new RangeError(`toChunks: size must be a positive integer, got ${size}`);
750
+ return async function* (input) {
751
+ for await (const block of input) {
752
+ if (block.byteLength === 0) continue;
753
+ if (block.byteLength <= size) {
754
+ yield block;
755
+ continue;
756
+ }
757
+ let offset = 0;
758
+ while (offset < block.byteLength) {
759
+ const end = Math.min(offset + size, block.byteLength);
760
+ yield block.subarray(offset, end);
761
+ offset = end;
762
+ }
763
+ }
764
+ };
765
+ }
766
+ //#endregion
767
+ //#region src/core/data-calls.ts
768
+ function callChannel(target, callType, params, ...transfers) {
769
+ return new Promise((resolve, reject) => {
770
+ const channel = new MessageChannel();
771
+ channel.port1.onmessage = (ev) => {
772
+ const { result, error } = ev.data;
773
+ if (error) reject(deserializeError(error));
774
+ else resolve(result);
775
+ };
776
+ target.postMessage({
777
+ type: callType,
778
+ params
779
+ }, [channel.port2, ...transfers]);
780
+ });
781
+ }
782
+ function handleChannelCalls(target, callType, handler) {
783
+ const listener = async (event) => {
784
+ if (!event.data || event.data.type !== callType) return;
785
+ const [port, ...transfers] = event.ports ?? [];
786
+ const response = {};
787
+ try {
788
+ response.result = await handler(event, event.data.params, ...transfers);
789
+ } catch (error) {
790
+ response.error = serializeError(error);
791
+ }
792
+ port?.postMessage(response);
793
+ };
794
+ target.addEventListener("message", listener);
795
+ target.start?.();
796
+ return () => target.removeEventListener("message", listener);
797
+ }
798
+ //#endregion
799
+ //#region src/core/data-channels.ts
800
+ const MESSAGE_TYPE_REQUEST = "REQUEST";
801
+ const MESSAGE_TYPE_RESPONSE = "RESPONSE";
802
+ let __invocationCounter = 0;
803
+ function newInvokationChannel({ port, handler = () => {
804
+ throw new Error("Handler not implemented");
805
+ }, onError = console.error, newCallId = () => ++__invocationCounter }) {
806
+ const requests = {};
807
+ const listener = async (event) => {
808
+ const data = event.data ?? {};
809
+ if (data.type === MESSAGE_TYPE_REQUEST) try {
810
+ const result = await handler(data.request, ...event.ports);
811
+ const [response, ...transfers] = Array.isArray(result) ? result : result !== void 0 ? [result] : [];
812
+ port.postMessage({
813
+ type: MESSAGE_TYPE_RESPONSE,
814
+ callId: data.callId,
815
+ response
816
+ }, transfers);
817
+ } catch (error) {
818
+ port.postMessage({
819
+ type: MESSAGE_TYPE_RESPONSE,
820
+ callId: data.callId,
821
+ error: serializeError(error)
822
+ });
823
+ }
824
+ else if (data.type === MESSAGE_TYPE_RESPONSE) {
825
+ const pending = requests[data.callId];
826
+ delete requests[data.callId];
827
+ if (!pending) return;
828
+ if (data.error) pending.reject(deserializeError(data.error));
829
+ else pending.resolve(data.response);
830
+ }
831
+ };
832
+ const start = async () => {
833
+ try {
834
+ port.addEventListener("message", listener);
835
+ await port.start?.();
836
+ } catch (e) {
837
+ onError(e);
838
+ }
839
+ };
840
+ const close = async () => {
841
+ try {
842
+ port.removeEventListener("message", listener);
843
+ await port.close?.();
844
+ } catch (e) {
845
+ onError(e);
846
+ }
847
+ };
848
+ const invoke = (request = {}, ...transfers) => {
849
+ const callId = newCallId();
850
+ return new Promise((resolve, reject) => {
851
+ try {
852
+ requests[callId] = {
853
+ resolve,
854
+ reject
855
+ };
856
+ port.postMessage({
857
+ type: MESSAGE_TYPE_REQUEST,
858
+ callId,
859
+ request
860
+ }, transfers);
861
+ } catch (error) {
862
+ delete requests[callId];
863
+ reject(error);
864
+ }
865
+ });
866
+ };
867
+ return {
868
+ start,
869
+ close,
870
+ invoke
871
+ };
872
+ }
873
+ async function* sendStream(communicationPort, input, params = {}) {
874
+ const messageChannel = new MessageChannel();
875
+ communicationPort.postMessage({
876
+ type: "START_CALL",
877
+ params
878
+ }, [messageChannel.port2]);
879
+ const channel = newStreamChannel(messageChannel.port1);
880
+ try {
881
+ await channel.start();
882
+ channel.sendAll(input);
883
+ yield* channel.recieveAll();
884
+ } finally {
885
+ await channel.close();
886
+ }
887
+ }
888
+ function handleStreams(communicationPort, handler) {
889
+ const listener = async (event) => {
890
+ const { type, params } = event.data ?? {};
891
+ if (type !== "START_CALL") return;
892
+ const port = event.ports[0];
893
+ const channel = newStreamChannel(port);
894
+ try {
895
+ await channel.start();
896
+ const response = await handler(channel.recieveAll(), params ?? {});
897
+ await channel.sendAll(response);
898
+ } finally {
899
+ await channel.close();
900
+ }
901
+ };
902
+ communicationPort.addEventListener("message", listener);
903
+ communicationPort.start?.();
904
+ return () => communicationPort.removeEventListener("message", listener);
905
+ }
906
+ function newStreamChannel(port) {
907
+ let listeners = [];
908
+ let iterators = [];
909
+ const notifyAll = async (data) => {
910
+ for (const listener of listeners) await listener(data);
911
+ };
912
+ const channel = newInvokationChannel({
913
+ port,
914
+ handler: (data) => notifyAll(data)
915
+ });
916
+ const start = () => channel.start();
917
+ const close = async () => {
918
+ await notifyAll({ done: true });
919
+ for (const it of [...iterators]) await it.return?.();
920
+ await channel.close();
921
+ };
922
+ async function* recieveAll() {
923
+ yield* recieveIterator((deliver) => {
924
+ listeners.push(deliver);
925
+ return () => {
926
+ listeners = listeners.filter((l) => l !== deliver);
927
+ };
928
+ });
929
+ }
930
+ async function sendAll(it) {
931
+ await sendIterator((chunk) => void channel.invoke(chunk), (async function* () {
932
+ try {
933
+ iterators.push(it);
934
+ yield* it;
935
+ } finally {
936
+ iterators = iterators.filter((i) => i !== it);
937
+ }
938
+ })());
939
+ }
940
+ return {
941
+ start,
942
+ close,
943
+ recieveAll,
944
+ sendAll
945
+ };
946
+ }
947
+ //#endregion
948
+ //#region src/core/registry.ts
949
+ function newRegistry(onError = console.error) {
950
+ let counter = 0;
951
+ const registrations = {};
952
+ const register = (action) => {
953
+ const id = counter++;
954
+ const fn = Object.assign((skip) => {
955
+ try {
956
+ delete registrations[id];
957
+ return skip ? void 0 : action();
958
+ } catch (error) {
959
+ onError(error);
960
+ }
961
+ }, { action });
962
+ registrations[id] = fn;
963
+ return fn;
964
+ };
965
+ const unregister = (action) => {
966
+ for (const r of Object.values(registrations)) if (r.action === action) r(true);
967
+ };
968
+ const clear = () => {
969
+ for (const r of Object.values(registrations)) r();
970
+ };
971
+ return Object.assign([
972
+ register,
973
+ clear,
974
+ unregister
975
+ ], {
976
+ register,
977
+ clear,
978
+ unregister
979
+ });
980
+ }
981
+ //#endregion
982
+ //#region ../webrun-http-streams/src/bytes.ts
983
+ const CR = 13;
984
+ const LF = 10;
985
+ const EMPTY = /* @__PURE__ */ new Uint8Array(0);
986
+ var ByteStreamError = class extends Error {
987
+ name = "ByteStreamError";
988
+ };
989
+ function toAsyncIterator(input) {
990
+ const asyncIter = input[Symbol.asyncIterator];
991
+ if (asyncIter) return asyncIter.call(input);
992
+ const syncIter = input[Symbol.iterator]();
993
+ return { next() {
994
+ return Promise.resolve(syncIter.next());
995
+ } };
996
+ }
997
+ /**
998
+ * Discard an iterable we are contractually forbidden from consuming — a body
999
+ * skipped for HEAD/204, or one abandoned because the peer reported an error.
1000
+ *
1001
+ * The `.next()` is not optional: `.return()` on a generator still in suspended
1002
+ * start is a no-op, so the body never runs and its `try/finally` never unwinds.
1003
+ * Without it a wrapped ReadableStream or socket is never cancelled.
1004
+ */
1005
+ async function discard(source) {
1006
+ if (source === void 0) return;
1007
+ const it = toAsyncIterator(source);
1008
+ try {
1009
+ await it.next();
1010
+ await it.return?.();
1011
+ } catch {}
1012
+ }
1013
+ function concatChunks(parts, totalLen) {
1014
+ if (parts.length === 1) {
1015
+ const only = parts[0];
1016
+ if (only !== void 0) return only;
1017
+ }
1018
+ const out = new Uint8Array(totalLen);
1019
+ let off = 0;
1020
+ for (const p of parts) {
1021
+ out.set(p, off);
1022
+ off += p.byteLength;
1023
+ }
1024
+ return out;
1025
+ }
1026
+ /**
1027
+ * Pull-based reader over a byte source. Holds at most one pending buffer, and
1028
+ * hands out `subarray` views rather than copies — a body never passes through
1029
+ * an allocation here.
1030
+ */
1031
+ var ByteReader = class {
1032
+ #iter;
1033
+ #buf = EMPTY;
1034
+ #done = false;
1035
+ constructor(input) {
1036
+ this.#iter = toAsyncIterator(input);
1037
+ }
1038
+ /** Bytes already pulled from the source but not yet consumed. */
1039
+ bufferedLength() {
1040
+ return this.#buf.byteLength;
1041
+ }
1042
+ /** Pull one more non-empty chunk. Returns false at end of stream. */
1043
+ async #pull() {
1044
+ if (this.#done) return false;
1045
+ while (true) {
1046
+ const next = await this.#iter.next();
1047
+ if (next.done) {
1048
+ this.#done = true;
1049
+ return false;
1050
+ }
1051
+ const chunk = next.value;
1052
+ if (chunk.byteLength === 0) continue;
1053
+ this.#buf = this.#buf.byteLength === 0 ? chunk : concatChunks([this.#buf, chunk], this.#buf.byteLength + chunk.byteLength);
1054
+ return true;
1055
+ }
1056
+ }
1057
+ async peekByte() {
1058
+ while (this.#buf.byteLength === 0) if (!await this.#pull()) return void 0;
1059
+ return this.#buf[0];
1060
+ }
1061
+ /** Up to `max` bytes. `undefined` means end of stream. */
1062
+ async readSome(max) {
1063
+ while (this.#buf.byteLength === 0) if (!await this.#pull()) return void 0;
1064
+ const take = Math.min(max, this.#buf.byteLength);
1065
+ const out = this.#buf.subarray(0, take);
1066
+ this.#buf = this.#buf.subarray(take);
1067
+ return out;
1068
+ }
1069
+ /**
1070
+ * One CRLF-terminated line, without the CRLF. A bare LF is rejected: real
1071
+ * peers always send CRLF, and tolerating a bare LF is precisely the lenience
1072
+ * that lets request smuggling through a proxy pair.
1073
+ *
1074
+ * The `maxBytes` bound is best-effort: it only rejects a line if the check
1075
+ * happens to run before the line is fully buffered. A line already sitting in
1076
+ * the buffer bypasses the check. Callers needing a hard per-line limit or
1077
+ * aggregate bounds must keep their own running total.
1078
+ */
1079
+ async readLine(maxBytes) {
1080
+ let searched = 0;
1081
+ while (true) {
1082
+ const idx = this.#buf.indexOf(LF, searched);
1083
+ if (idx !== -1) {
1084
+ if (idx === 0 || this.#buf[idx - 1] !== CR) throw new ByteStreamError("bare LF line terminator; CRLF required");
1085
+ const line = this.#buf.subarray(0, idx - 1);
1086
+ this.#buf = this.#buf.subarray(idx + 1);
1087
+ return line;
1088
+ }
1089
+ searched = this.#buf.byteLength;
1090
+ if (searched > maxBytes) throw new ByteStreamError(`line exceeds ${maxBytes} bytes without CRLF`);
1091
+ if (!await this.#pull()) throw new ByteStreamError(`stream ended after ${searched} bytes without CRLF`);
1092
+ }
1093
+ }
1094
+ /** Everything not yet consumed, lazily. */
1095
+ async *rest() {
1096
+ while (true) {
1097
+ if (this.#buf.byteLength > 0) {
1098
+ const out = this.#buf;
1099
+ this.#buf = EMPTY;
1100
+ yield out;
1101
+ continue;
1102
+ }
1103
+ if (!await this.#pull()) return;
1104
+ }
1105
+ }
1106
+ };
1107
+ //#endregion
1108
+ //#region ../webrun-http-streams/src/http1/errors.ts
1109
+ /**
1110
+ * Raised for any byte sequence this codec refuses to interpret. Every case is
1111
+ * a refusal to guess: HTTP/1.1 parsers that guess are how request smuggling
1112
+ * works.
1113
+ */
1114
+ var HttpParseError = class extends Error {
1115
+ name = "HttpParseError";
1116
+ };
1117
+ //#endregion
1118
+ //#region ../webrun-http-streams/src/envelope.ts
1119
+ const NEWLINE = 10;
1120
+ /** Mirrors the HTTP/1.1 codec's default `maxHeaderBytes`. */
1121
+ const MAX_ENVELOPE_BYTES = 65536;
1122
+ const encoder$1 = new TextEncoder();
1123
+ const decoder = new TextDecoder();
1124
+ /**
1125
+ * Encode an HTTP envelope plus optional body as one continuous byte stream:
1126
+ *
1127
+ * <JSON.stringify(envelope)>\n<body bytes...>
1128
+ *
1129
+ * `JSON.stringify` with default whitespace never emits a literal `\n`, so the
1130
+ * first `0x0a` byte unambiguously terminates the envelope.
1131
+ *
1132
+ * This is the same wire shape used by the legacy `webrun-http-port` package.
1133
+ */
1134
+ async function* encodeMessage(envelope, body) {
1135
+ yield encoder$1.encode(`${JSON.stringify(envelope)}\n`);
1136
+ if (!body) return;
1137
+ for await (const chunk of body) if (chunk.byteLength > 0) yield chunk;
1138
+ }
1139
+ /**
1140
+ * Consume an envelope-then-body byte stream. Returns the parsed envelope and
1141
+ * an async iterable over the remaining body bytes.
1142
+ */
1143
+ async function decodeMessage(input) {
1144
+ const iter = toAsyncIterator(input);
1145
+ const accum = [];
1146
+ let accumLen = 0;
1147
+ let before;
1148
+ let tail;
1149
+ while (true) {
1150
+ const next = await iter.next();
1151
+ if (next.done) throw new HttpParseError(`decodeMessage: stream ended after ${accumLen} bytes without delimiter (\\n)`);
1152
+ const chunk = next.value;
1153
+ if (chunk.byteLength === 0) continue;
1154
+ const nl = chunk.indexOf(NEWLINE);
1155
+ if (nl === -1) {
1156
+ accum.push(chunk);
1157
+ accumLen += chunk.byteLength;
1158
+ if (accumLen > MAX_ENVELOPE_BYTES) throw new HttpParseError(`decodeMessage: envelope exceeds ${MAX_ENVELOPE_BYTES} bytes without delimiter (\\n)`);
1159
+ continue;
1160
+ }
1161
+ accum.push(chunk.subarray(0, nl));
1162
+ accumLen += nl;
1163
+ before = concatChunks(accum, accumLen);
1164
+ tail = chunk.subarray(nl + 1);
1165
+ break;
1166
+ }
1167
+ let envelope;
1168
+ try {
1169
+ envelope = JSON.parse(decoder.decode(before));
1170
+ } catch (err) {
1171
+ throw new HttpParseError(`decodeMessage: malformed envelope JSON at bytes 0..${before.byteLength}: ${err.message}`);
1172
+ }
1173
+ async function* body() {
1174
+ if (tail.byteLength > 0) yield tail;
1175
+ while (true) {
1176
+ const next = await iter.next();
1177
+ if (next.done) return;
1178
+ if (next.value.byteLength > 0) yield next.value;
1179
+ }
1180
+ }
1181
+ return {
1182
+ envelope,
1183
+ body: body()
1184
+ };
1185
+ }
1186
+ const OPEN_BRACE = 123;
1187
+ /**
1188
+ * The original wire format — `<JSON.stringify(envelope)>\n<body bytes…>` —
1189
+ * expressed as a `MessageCodec`. Direction-agnostic: requests and responses
1190
+ * serialise identically.
1191
+ *
1192
+ * Retained so a peer pair can be upgraded in either order; see ADR-0006.
1193
+ */
1194
+ const jsonEnvelopeCodec = {
1195
+ name: "json-envelope",
1196
+ sniff: (byte) => byte === OPEN_BRACE,
1197
+ encodeRequest: (env, body) => encodeMessage(env, body),
1198
+ encodeResponse: (env, body) => encodeMessage(env, body),
1199
+ decodeRequest: (input) => decodeMessage(input),
1200
+ decodeResponse: (input) => decodeMessage(input)
1201
+ };
1202
+ //#endregion
1203
+ //#region ../webrun-http-streams/src/http1/headers.ts
1204
+ const TCHAR = /* @__PURE__ */ new Set("!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
1205
+ const SP = 32;
1206
+ const HTAB = 9;
1207
+ function isTokenChar(byte) {
1208
+ return byte > SP && byte < 127 && TCHAR.has(String.fromCharCode(byte));
1209
+ }
1210
+ function isToken(value) {
1211
+ if (value.length === 0) return false;
1212
+ for (const ch of value) if (!TCHAR.has(ch)) return false;
1213
+ return true;
1214
+ }
1215
+ /**
1216
+ * Header bytes are latin-1: byte-preserving, matching Node, and lossless
1217
+ * across a decode/encode round-trip. Control characters are rejected
1218
+ * separately by `assertValidHeaderValue`.
1219
+ */
1220
+ function decodeLatin1(bytes) {
1221
+ let out = "";
1222
+ for (const byte of bytes) out += String.fromCharCode(byte);
1223
+ return out;
1224
+ }
1225
+ function encodeLatin1(text) {
1226
+ const out = new Uint8Array(text.length);
1227
+ for (let i = 0; i < text.length; i++) {
1228
+ const code = text.charCodeAt(i);
1229
+ if (code > 255) throw new HttpParseError(`not latin-1 encodable: ${JSON.stringify(text)}`);
1230
+ out[i] = code;
1231
+ }
1232
+ return out;
1233
+ }
1234
+ const HOST_IP_LITERAL = /^\[[0-9A-Fa-f:.]+\](:\d{1,5})?$/;
1235
+ const HOST_REG_NAME = /^[A-Za-z0-9._~-]+(:\d{1,5})?$/;
1236
+ /**
1237
+ * Validates a value that is about to become (or was read as) a `Host`
1238
+ * header: untrusted input either way, so an unrecognised shape is a refusal,
1239
+ * not a guess. Rejects a present-but-empty value, one carrying userinfo
1240
+ * (`evil.com@good.com`), and — the reason this also runs on encode — one
1241
+ * smuggling a CRLF-terminated line into the authority of a caller-supplied
1242
+ * url. Never applied to trusted configuration (a codec's `opts.host`).
1243
+ */
1244
+ function assertValidHost(host) {
1245
+ if (!HOST_IP_LITERAL.test(host) && !HOST_REG_NAME.test(host)) throw new HttpParseError(`invalid Host header: ${JSON.stringify(host)}`);
1246
+ }
1247
+ /**
1248
+ * A request-target must be entirely visible ASCII (RFC 9110 VCHAR) — nothing
1249
+ * `<= 0x20` (space and every control character, CR/LF included) and nothing
1250
+ * `>= 0x7F` (DEL and beyond). Applied on encode (`splitTarget`, so a
1251
+ * caller-supplied url can never inject a second request line) and on decode
1252
+ * (so a relay that decodes then re-encodes can never put an embedded control
1253
+ * character back on the wire).
1254
+ */
1255
+ function assertValidTarget(target) {
1256
+ for (let i = 0; i < target.length; i++) {
1257
+ const code = target.charCodeAt(i);
1258
+ if (code <= SP || code >= 127) throw new HttpParseError(`invalid request-target: ${JSON.stringify(target)}`);
1259
+ }
1260
+ }
1261
+ /**
1262
+ * Shared control-character check behind both `assertValidHeaderValue` and
1263
+ * `assertValidStatusText` — the two were previously checked by different,
1264
+ * looser rules (statusText only rejected CR/LF), which is exactly the
1265
+ * asymmetry class C1 and M5 are both instances of. `label` is prepended
1266
+ * verbatim to each message, so callers keep their own wording.
1267
+ */
1268
+ function assertNoControlChars(label, value) {
1269
+ for (let i = 0; i < value.length; i++) {
1270
+ const code = value.charCodeAt(i);
1271
+ if (code === 13 || code === 10) throw new HttpParseError(`${label} contains CR or LF`);
1272
+ if (code < SP && code !== HTAB || code === 127) throw new HttpParseError(`${label} contains a control character`);
1273
+ if (code > 255) throw new HttpParseError(`${label} is not latin-1 encodable`);
1274
+ }
1275
+ }
1276
+ function assertValidHeaderValue(name, value) {
1277
+ assertNoControlChars(`header "${name}" value`, value);
1278
+ }
1279
+ /** Same character class as a header value (M5) — CR/LF, every C0 control, and DEL. */
1280
+ function assertValidStatusText(value) {
1281
+ assertNoControlChars("statusText", value);
1282
+ }
1283
+ function encodeHeaderLines(headers) {
1284
+ let out = "";
1285
+ for (const [name, value] of headers) {
1286
+ if (!isToken(name)) throw new HttpParseError(`invalid header name: ${JSON.stringify(name)}`);
1287
+ assertValidHeaderValue(name, value);
1288
+ out += `${name}: ${value}\r\n`;
1289
+ }
1290
+ return out;
1291
+ }
1292
+ /**
1293
+ * Read to the blank line. `alreadyUsed` is the byte count of the start line,
1294
+ * so the bound covers the whole head section rather than the headers alone.
1295
+ */
1296
+ async function readHeaderSection(reader, maxHeaderBytes, alreadyUsed) {
1297
+ const headers = [];
1298
+ let used = alreadyUsed;
1299
+ while (true) {
1300
+ const remaining = maxHeaderBytes - used;
1301
+ if (remaining <= 0) throw new HttpParseError(`head section exceeds ${maxHeaderBytes} bytes`);
1302
+ const lineBytes = await reader.readLine(remaining);
1303
+ used += lineBytes.byteLength + 2;
1304
+ if (used > maxHeaderBytes) throw new HttpParseError(`head section exceeds ${maxHeaderBytes} bytes`);
1305
+ if (lineBytes.byteLength === 0) return headers;
1306
+ if (lineBytes[0] === SP || lineBytes[0] === HTAB) throw new HttpParseError("obs-fold header continuation is not accepted");
1307
+ const line = decodeLatin1(lineBytes);
1308
+ const colon = line.indexOf(":");
1309
+ if (colon <= 0) throw new HttpParseError(`malformed header line: ${JSON.stringify(line)}`);
1310
+ const name = line.slice(0, colon);
1311
+ if (!isToken(name)) throw new HttpParseError(`invalid header name: ${JSON.stringify(name)}`);
1312
+ const value = line.slice(colon + 1).replace(/^[ \t]+/, "").replace(/[ \t]+$/, "");
1313
+ assertValidHeaderValue(name, value);
1314
+ headers.push([name, value]);
1315
+ }
1316
+ }
1317
+ function getAll(headers, name) {
1318
+ const lower = name.toLowerCase();
1319
+ return headers.filter(([k]) => k.toLowerCase() === lower).map(([, v]) => v);
1320
+ }
1321
+ function withoutHeaders(headers, names) {
1322
+ const drop = new Set(names.map((n) => n.toLowerCase()));
1323
+ return headers.filter(([k]) => !drop.has(k.toLowerCase()));
1324
+ }
1325
+ /**
1326
+ * Parse a `Content-Length` field into a single validated value.
1327
+ *
1328
+ * RFC 9110 §8.6 permits the value to be a comma-separated list of identical
1329
+ * numbers (a relay may have appended one), so the list is folded; differing
1330
+ * values are a refusal, not a choice. Shared by `resolveFraming` on decode and
1331
+ * by the encoder's declared-length check, which previously carried its own
1332
+ * copy that did NOT split on commas — so decode accepted `5, 5` while encode
1333
+ * rejected it, and a relay that decoded then re-encoded threw.
1334
+ *
1335
+ * Returns undefined when the field is absent.
1336
+ */
1337
+ function parseContentLength(values) {
1338
+ if (values.length === 0) return void 0;
1339
+ const unique = new Set(values.flatMap((v) => v.split(",").map((s) => s.trim())));
1340
+ if (unique.size !== 1) throw new HttpParseError(`conflicting Content-Length values: ${[...unique].join(", ")}`);
1341
+ const raw = [...unique][0];
1342
+ if (!/^\d{1,15}$/.test(raw)) throw new HttpParseError(`invalid Content-Length: ${JSON.stringify(raw)}`);
1343
+ return Number(raw);
1344
+ }
1345
+ /**
1346
+ * Statuses that carry no body whatever the headers say (RFC 9110 §8.6): 1xx,
1347
+ * 204 and 304. Defined once because the encoder must not frame a body for
1348
+ * them and the decoder must not try to read one — two lists that agreed today
1349
+ * and were free to drift tomorrow.
1350
+ *
1351
+ * A response to HEAD is also bodyless, but that depends on the request rather
1352
+ * than the status, so callers test it separately.
1353
+ */
1354
+ function isBodylessStatus(status) {
1355
+ return status < 200 || status === 204 || status === 304;
1356
+ }
1357
+ /**
1358
+ * RFC 9112 §6.3, with every ambiguity turned into a refusal. In particular a
1359
+ * message declaring both Content-Length and Transfer-Encoding is rejected
1360
+ * rather than resolved — disagreeing on which one wins is request smuggling.
1361
+ */
1362
+ function resolveFraming(headers, version) {
1363
+ const te = getAll(headers, "transfer-encoding");
1364
+ const cl = getAll(headers, "content-length");
1365
+ if (te.length > 0 && cl.length > 0) throw new HttpParseError("message declares both Content-Length and Transfer-Encoding; refusing (request smuggling)");
1366
+ if (te.length > 0) {
1367
+ const encodings = te.join(",").split(",").map((s) => s.trim().toLowerCase()).filter((s) => s !== "");
1368
+ if (encodings.length !== 1 || encodings[0] !== "chunked") throw new HttpParseError(`unsupported Transfer-Encoding: ${JSON.stringify(te.join(", "))}`);
1369
+ if (version === "HTTP/1.0") throw new HttpParseError("Transfer-Encoding is not valid in HTTP/1.0");
1370
+ return { kind: "chunked" };
1371
+ }
1372
+ const length = parseContentLength(cl);
1373
+ if (length !== void 0) return {
1374
+ kind: "length",
1375
+ length
1376
+ };
1377
+ return { kind: "none" };
1378
+ }
1379
+ //#endregion
1380
+ //#region ../webrun-http-streams/src/http1/chunked.ts
1381
+ const CRLF = new Uint8Array([13, 10]);
1382
+ const LAST_CHUNK = new Uint8Array([
1383
+ 48,
1384
+ 13,
1385
+ 10,
1386
+ 13,
1387
+ 10
1388
+ ]);
1389
+ const encoder = new TextEncoder();
1390
+ /**
1391
+ * Chunk sizes are written in HEXADECIMAL — a 21-byte chunk is `15`. Writing
1392
+ * them in decimal is the defect note 15 found in @libp2p/http, and it is the
1393
+ * one that cannot be caught downstream: decimal digits are also valid hex, so
1394
+ * a conforming parser silently reads the wrong length.
1395
+ *
1396
+ * Zero-length source chunks are skipped; a zero-sized chunk on the wire is the
1397
+ * body terminator.
1398
+ */
1399
+ async function* encodeChunked(body) {
1400
+ for await (const chunk of body) {
1401
+ if (chunk.byteLength === 0) continue;
1402
+ yield encoder.encode(`${chunk.byteLength.toString(16)}\r\n`);
1403
+ yield chunk;
1404
+ yield CRLF;
1405
+ }
1406
+ yield LAST_CHUNK;
1407
+ }
1408
+ /**
1409
+ * Decode a chunked body, yielding data chunks as they arrive. Nothing is
1410
+ * accumulated: a chunk larger than the transport's frame is yielded in pieces.
1411
+ */
1412
+ async function* decodeChunked(reader, maxLineBytes) {
1413
+ while (true) {
1414
+ const sizeLine = decodeLatin1(await reader.readLine(maxLineBytes));
1415
+ const semicolon = sizeLine.indexOf(";");
1416
+ const sizeText = semicolon === -1 ? sizeLine : sizeLine.slice(0, semicolon);
1417
+ if (!/^[0-9a-fA-F]{1,32}$/.test(sizeText)) throw new HttpParseError(`invalid chunk size: ${JSON.stringify(sizeLine)}`);
1418
+ const size = Number.parseInt(sizeText, 16);
1419
+ if (!Number.isSafeInteger(size)) throw new HttpParseError(`chunk size is too large: ${JSON.stringify(sizeLine)}`);
1420
+ if (size === 0) {
1421
+ let used = 0;
1422
+ while (true) {
1423
+ const remaining = maxLineBytes - used;
1424
+ if (remaining <= 0) throw new HttpParseError(`trailer section exceeds ${maxLineBytes} bytes`);
1425
+ const lineBytes = await reader.readLine(remaining);
1426
+ used += lineBytes.byteLength + 2;
1427
+ if (used > maxLineBytes) throw new HttpParseError(`trailer section exceeds ${maxLineBytes} bytes`);
1428
+ if (lineBytes.byteLength === 0) break;
1429
+ }
1430
+ return;
1431
+ }
1432
+ let remaining = size;
1433
+ while (remaining > 0) {
1434
+ const part = await reader.readSome(remaining);
1435
+ if (part === void 0) throw new HttpParseError(`chunk truncated: ${remaining} of ${size} bytes missing`);
1436
+ remaining -= part.byteLength;
1437
+ yield part;
1438
+ }
1439
+ try {
1440
+ if ((await reader.readLine(2)).byteLength !== 0) throw new HttpParseError("chunk data not terminated by CRLF");
1441
+ } catch (err) {
1442
+ if (err instanceof ByteStreamError) throw new HttpParseError("chunk data not terminated by CRLF");
1443
+ throw err;
1444
+ }
1445
+ }
1446
+ }
1447
+ //#endregion
1448
+ //#region ../webrun-http-streams/src/http1/decode.ts
1449
+ const VERSIONS = /* @__PURE__ */ new Set(["HTTP/1.1", "HTTP/1.0"]);
1450
+ const ABSOLUTE_FORM = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//;
1451
+ /**
1452
+ * One message per Duplex call (ADR-0006), so bytes after a complete message
1453
+ * are an error. Checked against what is ALREADY BUFFERED rather than by
1454
+ * awaiting end-of-stream: a live socket from a keep-alive peer never reaches
1455
+ * EOF, so awaiting one would hang instead of failing.
1456
+ */
1457
+ function assertNoBufferedBytes(reader) {
1458
+ const extra = reader.bufferedLength();
1459
+ if (extra > 0) throw new HttpParseError(`${extra} trailing bytes after a complete message`);
1460
+ }
1461
+ /**
1462
+ * The public error contract (I2): everything leaving `decodeRequest` /
1463
+ * `decodeResponse` is an `HttpParseError`, whether the refusal happened
1464
+ * synchronously (a malformed start line or header) or lazily while the body
1465
+ * is later drained. `ByteStreamError` — the `ByteReader`'s own class, never
1466
+ * exported — is the one thing converted here, message and all preserved via
1467
+ * `cause`. Anything else is a genuine failure of the underlying source (a
1468
+ * dropped socket, say) and must reach the caller unchanged: blanket-catching
1469
+ * would hide that distinction.
1470
+ */
1471
+ function toHttpParseError(err) {
1472
+ if (err instanceof ByteStreamError) throw new HttpParseError(err.message, { cause: err });
1473
+ throw err;
1474
+ }
1475
+ /** Applies `toHttpParseError` across the whole lifetime of a body generator. */
1476
+ async function* convertBodyErrors(source) {
1477
+ try {
1478
+ yield* source;
1479
+ } catch (err) {
1480
+ toHttpParseError(err);
1481
+ }
1482
+ }
1483
+ async function* readBody(reader, framing, opts, noneMeansEof) {
1484
+ if (framing.kind === "chunked") {
1485
+ yield* decodeChunked(reader, opts.maxHeaderBytes);
1486
+ assertNoBufferedBytes(reader);
1487
+ return;
1488
+ }
1489
+ if (framing.kind === "length") {
1490
+ let remaining = framing.length;
1491
+ while (remaining > 0) {
1492
+ const part = await reader.readSome(remaining);
1493
+ if (part === void 0) throw new HttpParseError(`body truncated: ${remaining} of ${framing.length} bytes missing`);
1494
+ remaining -= part.byteLength;
1495
+ yield part;
1496
+ }
1497
+ assertNoBufferedBytes(reader);
1498
+ return;
1499
+ }
1500
+ if (noneMeansEof) {
1501
+ yield* reader.rest();
1502
+ return;
1503
+ }
1504
+ assertNoBufferedBytes(reader);
1505
+ }
1506
+ /**
1507
+ * `readLine`'s bound is best-effort — it is skipped when the line is already
1508
+ * buffered — so a very long start line can reach these messages intact. Since
1509
+ * a refusal is now echoed back to the sender in a 400 body, quote only enough
1510
+ * to diagnose rather than reflecting the whole thing.
1511
+ */
1512
+ function quoteLine(line) {
1513
+ const MAX = 120;
1514
+ return line.length <= MAX ? JSON.stringify(line) : `${JSON.stringify(line.slice(0, MAX))} (truncated from ${line.length} chars)`;
1515
+ }
1516
+ async function decodeRequest(input, opts) {
1517
+ const reader = new ByteReader(input);
1518
+ try {
1519
+ const startBytes = await reader.readLine(opts.maxHeaderBytes);
1520
+ const startLine = decodeLatin1(startBytes);
1521
+ const parts = startLine.split(" ");
1522
+ if (parts.length !== 3) throw new HttpParseError(`malformed request line: ${quoteLine(startLine)}`);
1523
+ const [method, target, version] = parts;
1524
+ if (!isToken(method)) throw new HttpParseError(`invalid method: ${JSON.stringify(method)}`);
1525
+ if (!VERSIONS.has(version)) throw new HttpParseError(`unsupported HTTP version: ${JSON.stringify(version)}`);
1526
+ if (target === "*") throw new HttpParseError("asterisk-form request target is not supported");
1527
+ assertValidTarget(target);
1528
+ const headers = await readHeaderSection(reader, opts.maxHeaderBytes, startBytes.byteLength + 2);
1529
+ const hosts = getAll(headers, "host");
1530
+ if (hosts.length > 1) throw new HttpParseError("multiple Host headers");
1531
+ const host = hosts[0];
1532
+ let url;
1533
+ if (target.startsWith("/")) {
1534
+ if (version === "HTTP/1.1" && host === void 0) throw new HttpParseError("HTTP/1.1 request has no Host header");
1535
+ if (host !== void 0) assertValidHost(host);
1536
+ url = `${opts.scheme}://${host ?? opts.host}${target}`;
1537
+ } else if (ABSOLUTE_FORM.test(target)) url = target;
1538
+ else throw new HttpParseError(`unsupported request target: ${JSON.stringify(target)}`);
1539
+ try {
1540
+ new URL(url);
1541
+ } catch {
1542
+ throw new HttpParseError(`decoded url is not a valid URL: ${JSON.stringify(url)}`);
1543
+ }
1544
+ return {
1545
+ envelope: {
1546
+ url,
1547
+ method,
1548
+ headers
1549
+ },
1550
+ body: convertBodyErrors(readBody(reader, resolveFraming(headers, version), opts, false))
1551
+ };
1552
+ } catch (err) {
1553
+ toHttpParseError(err);
1554
+ }
1555
+ }
1556
+ async function decodeResponse(input, opts, method) {
1557
+ const reader = new ByteReader(input);
1558
+ try {
1559
+ const startBytes = await reader.readLine(opts.maxHeaderBytes);
1560
+ const startLine = decodeLatin1(startBytes);
1561
+ const firstSp = startLine.indexOf(" ");
1562
+ if (firstSp === -1) throw new HttpParseError(`malformed status line: ${quoteLine(startLine)}`);
1563
+ const version = startLine.slice(0, firstSp);
1564
+ if (!VERSIONS.has(version)) throw new HttpParseError(`unsupported HTTP version: ${JSON.stringify(version)}`);
1565
+ const afterVersion = startLine.slice(firstSp + 1);
1566
+ const secondSp = afterVersion.indexOf(" ");
1567
+ const codeText = secondSp === -1 ? afterVersion : afterVersion.slice(0, secondSp);
1568
+ if (!/^\d{3}$/.test(codeText)) throw new HttpParseError(`invalid status code: ${JSON.stringify(codeText)}`);
1569
+ const status = Number(codeText);
1570
+ const statusText = secondSp === -1 ? "" : afterVersion.slice(secondSp + 1);
1571
+ const headers = await readHeaderSection(reader, opts.maxHeaderBytes, startBytes.byteLength + 2);
1572
+ const bodyless = isBodylessStatus(status) || method.toUpperCase() === "HEAD";
1573
+ const framing = bodyless ? { kind: "none" } : resolveFraming(headers, version);
1574
+ return {
1575
+ envelope: {
1576
+ status,
1577
+ statusText,
1578
+ headers
1579
+ },
1580
+ body: convertBodyErrors(readBody(reader, framing, opts, !bodyless))
1581
+ };
1582
+ } catch (err) {
1583
+ toHttpParseError(err);
1584
+ }
1585
+ }
1586
+ //#endregion
1587
+ //#region ../webrun-http-streams/src/http1/encode.ts
1588
+ /** Headers the codec owns: a caller-supplied copy is dropped and re-derived. */
1589
+ const REQUEST_OWNED = [
1590
+ "host",
1591
+ "connection",
1592
+ "transfer-encoding"
1593
+ ];
1594
+ const RESPONSE_OWNED = ["connection", "transfer-encoding"];
1595
+ /**
1596
+ * Split a URL into an origin-form request target and an authority *without*
1597
+ * going through `new URL()`. `URL` normalises percent-encoding and would
1598
+ * re-serialise the target — which is the exact class of defect note 15 found
1599
+ * in @libp2p/http, where rebuilding the request line silently dropped
1600
+ * `url.search`. Here the target is a verbatim slice of the caller's string.
1601
+ */
1602
+ function splitTarget(url, opts) {
1603
+ if (url.startsWith("/")) {
1604
+ assertValidTarget(url);
1605
+ return {
1606
+ target: url,
1607
+ authority: opts.host
1608
+ };
1609
+ }
1610
+ const match = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/([^/?#]*)([^#]*)/.exec(url);
1611
+ if (!match) throw new HttpParseError(`cannot derive a request target from url: ${JSON.stringify(url)}`);
1612
+ const rawAuthority = match[1];
1613
+ if (rawAuthority === "") throw new HttpParseError(`url has no authority: ${JSON.stringify(url)}`);
1614
+ const at = rawAuthority.lastIndexOf("@");
1615
+ const authority = at === -1 ? rawAuthority : rawAuthority.slice(at + 1);
1616
+ assertValidHost(authority);
1617
+ const rawTarget = match[2];
1618
+ const target = rawTarget === "" ? "/" : rawTarget.startsWith("/") ? rawTarget : `/${rawTarget}`;
1619
+ assertValidTarget(target);
1620
+ return {
1621
+ target,
1622
+ authority
1623
+ };
1624
+ }
1625
+ async function* emitBody(body, declared) {
1626
+ if (declared === void 0) {
1627
+ yield* encodeChunked(body);
1628
+ return;
1629
+ }
1630
+ let sent = 0;
1631
+ for await (const chunk of body) {
1632
+ if (chunk.byteLength === 0) continue;
1633
+ sent += chunk.byteLength;
1634
+ if (sent > declared) throw new HttpParseError(`body exceeds declared Content-Length ${declared} (${sent} bytes so far)`);
1635
+ yield chunk;
1636
+ }
1637
+ if (sent !== declared) throw new HttpParseError(`body is ${sent} bytes but Content-Length declares ${declared}`);
1638
+ }
1639
+ async function* encodeRequest(env, body, opts) {
1640
+ if (!isToken(env.method)) throw new HttpParseError(`invalid method: ${JSON.stringify(env.method)}`);
1641
+ const { target, authority } = splitTarget(env.url, opts);
1642
+ if (authority === "") throw new HttpParseError("no Host available: url has no authority and no host is configured");
1643
+ const carried = withoutHeaders(env.headers, REQUEST_OWNED);
1644
+ const declared = parseContentLength(getAll(carried, "content-length"));
1645
+ let head = `${env.method} ${target} HTTP/1.1\r\n`;
1646
+ head += `Host: ${authority}\r\n`;
1647
+ head += encodeHeaderLines(carried);
1648
+ head += "Connection: close\r\n";
1649
+ if (body !== void 0 && declared === void 0) head += "Transfer-Encoding: chunked\r\n";
1650
+ head += "\r\n";
1651
+ yield encodeLatin1(head);
1652
+ if (body === void 0) {
1653
+ if (declared !== void 0 && declared !== 0) throw new HttpParseError(`body is 0 bytes but Content-Length declares ${declared}`);
1654
+ return;
1655
+ }
1656
+ yield* emitBody(body, declared);
1657
+ }
1658
+ async function* encodeResponse(env, body, _opts, requestMethod) {
1659
+ if (!Number.isInteger(env.status) || env.status < 100 || env.status > 599) throw new HttpParseError(`invalid status: ${env.status}`);
1660
+ const reason = env.statusText ?? "";
1661
+ assertValidStatusText(reason);
1662
+ const bodylessStatus = isBodylessStatus(env.status);
1663
+ const bodyless = bodylessStatus || requestMethod?.toUpperCase() === "HEAD";
1664
+ const owned = bodylessStatus ? [...RESPONSE_OWNED, "content-length"] : RESPONSE_OWNED;
1665
+ const carried = withoutHeaders(env.headers, owned);
1666
+ const declared = parseContentLength(getAll(carried, "content-length"));
1667
+ let head = `HTTP/1.1 ${env.status} ${reason}\r\n`;
1668
+ head += encodeHeaderLines(carried);
1669
+ head += "Connection: close\r\n";
1670
+ if (!bodyless && body !== void 0 && declared === void 0) head += "Transfer-Encoding: chunked\r\n";
1671
+ head += "\r\n";
1672
+ yield encodeLatin1(head);
1673
+ if (bodyless) {
1674
+ await discard(body);
1675
+ return;
1676
+ }
1677
+ if (body === void 0) {
1678
+ if (declared !== void 0 && declared !== 0) throw new HttpParseError(`body is 0 bytes but Content-Length declares ${declared}`);
1679
+ return;
1680
+ }
1681
+ yield* emitBody(body, declared);
1682
+ }
1683
+ //#endregion
1684
+ //#region ../webrun-http-streams/src/http1/index.ts
1685
+ function newHttpCodec(options = {}) {
1686
+ const opts = {
1687
+ scheme: options.scheme ?? "http",
1688
+ host: options.host ?? "localhost",
1689
+ maxHeaderBytes: options.maxHeaderBytes ?? 65536
1690
+ };
1691
+ return {
1692
+ name: "http/1.1",
1693
+ sniff: (byte) => isTokenChar(byte),
1694
+ encodeRequest: (env, body) => encodeRequest(env, body, opts),
1695
+ encodeResponse: (env, body, o) => encodeResponse(env, body, opts, o?.method),
1696
+ decodeRequest: (input) => decodeRequest(input, opts),
1697
+ decodeResponse: (input, o) => decodeResponse(input, opts, o.method)
1698
+ };
1699
+ }
1700
+ const httpCodec = newHttpCodec();
1701
+ //#endregion
1702
+ //#region ../webrun-http-streams/src/sniff.ts
1703
+ /**
1704
+ * Dispatches on byte 0. The formats are self-identifying — a JSON envelope
1705
+ * always begins `{`, which is not a token character and so can never begin an
1706
+ * HTTP start-line — so no negotiation handshake, magic prefix, or version byte
1707
+ * is needed.
1708
+ *
1709
+ * This is what makes a mixed-version peer pair safe: readers accept either
1710
+ * format, so the two ends can be upgraded in any order.
1711
+ */
1712
+ function newSniffingCodec(options) {
1713
+ const { write, accept } = options;
1714
+ async function pick(input) {
1715
+ const reader = new ByteReader(input);
1716
+ const first = await reader.peekByte();
1717
+ if (first === void 0) throw new HttpParseError("sniff: stream ended before any bytes arrived");
1718
+ const codec = accept.find((c) => c.sniff(first));
1719
+ if (!codec) throw new HttpParseError(`sniff: no accepted codec recognises a message starting with byte 0x${first.toString(16).padStart(2, "0")}`);
1720
+ return {
1721
+ codec,
1722
+ input: reader.rest()
1723
+ };
1724
+ }
1725
+ return {
1726
+ name: `sniff(write=${write.name}; accept=${accept.map((c) => c.name).join(",")})`,
1727
+ sniff: (byte) => accept.some((c) => c.sniff(byte)),
1728
+ encodeRequest: (env, body) => write.encodeRequest(env, body),
1729
+ encodeResponse: (env, body, o) => write.encodeResponse(env, body, o),
1730
+ decodeRequest: async (input) => {
1731
+ const picked = await pick(input);
1732
+ try {
1733
+ return {
1734
+ ...await picked.codec.decodeRequest(picked.input),
1735
+ codec: picked.codec
1736
+ };
1737
+ } catch (error) {
1738
+ if (error !== null && typeof error === "object") error.codec = picked.codec;
1739
+ throw error;
1740
+ }
1741
+ },
1742
+ decodeResponse: async (input, o) => {
1743
+ const picked = await pick(input);
1744
+ return picked.codec.decodeResponse(picked.input, o);
1745
+ }
1746
+ };
1747
+ }
1748
+ //#endregion
1749
+ //#region ../webrun-http-streams/src/codec-default.ts
1750
+ /**
1751
+ * Writes HTTP/1.1; accepts HTTP/1.1 or the legacy JSON envelope. Used whenever
1752
+ * no `codec` option is supplied.
1753
+ */
1754
+ const defaultCodec = newSniffingCodec({
1755
+ write: httpCodec,
1756
+ accept: [httpCodec, jsonEnvelopeCodec]
1757
+ });
1758
+ //#endregion
1759
+ //#region ../webrun-http-streams/src/http-data.ts
1760
+ /** Carries the serialized JS error between two webrun peers. */
1761
+ const PEER_ERROR_HEADER = "x-webrun-error";
1762
+ /** Hard cap on the peer-error header (M3): see `encodeErrorResponse`. */
1763
+ const MAX_DETAIL_CHARS = 4096;
1764
+ /**
1765
+ * JSON with every non-printable-ASCII character escaped, so the result is a
1766
+ * legal latin-1 header value whatever the error message contained.
1767
+ */
1768
+ function asciiJson(value) {
1769
+ return JSON.stringify(value).replace(/[^\x20-\x7E]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`);
1770
+ }
1771
+ /**
1772
+ * Decision 13: a real HTTP peer cannot receive a JavaScript exception, only a
1773
+ * response. So an uncaught handler error becomes a conforming 500 whose body
1774
+ * carries the message, with the serialized error in a namespaced header that
1775
+ * another webrun peer re-throws from.
1776
+ */
1777
+ function encodeErrorResponse(codec, error, method, status = 500, statusText = "Internal Server Error") {
1778
+ const serialized = serializeError(error);
1779
+ let detail = asciiJson(serialized);
1780
+ if (detail.length > MAX_DETAIL_CHARS) detail = asciiJson({ message: serialized.message });
1781
+ if (detail.length > MAX_DETAIL_CHARS) detail = detail.slice(0, MAX_DETAIL_CHARS);
1782
+ const message = serialized.message ?? statusText;
1783
+ return codec.encodeResponse({
1784
+ status,
1785
+ statusText,
1786
+ headers: [["Content-Type", "text/plain; charset=utf-8"], [PEER_ERROR_HEADER, detail]]
1787
+ }, [new TextEncoder().encode(message)], { method });
1788
+ }
1789
+ /**
1790
+ * `output` is the `Duplex` call's own generator — one logical call, per the
1791
+ * `Duplex` contract in `@statewalker/webrun-streams`: "Consumer `.return()`
1792
+ * on the output → producer's `finally` runs." On a mux transport
1793
+ * (`emulateMux`), that `finally` is what frees the stream-table slot; skip it
1794
+ * and every peer-error response leaks one slot, unboundedly, until the mux
1795
+ * itself is exhausted (`maxStreams` reached, every further call rejected).
1796
+ *
1797
+ * Cancelling `output` here — not inside the codec's own decode logic — is
1798
+ * deliberate. `codec.decodeResponse` already pulled from `output` to read the
1799
+ * head before this function runs, so unlike the body case above there's no
1800
+ * suspended-start no-op to worry about. And it's safe specifically *because*
1801
+ * this is the client's own response-only generator: the request was already
1802
+ * fully sent before we got here, so there is nothing left to write on it.
1803
+ * The equivalent is NOT safe inside `src/http1/decode.ts`'s `ByteReader` —
1804
+ * that code also runs when a caller wires `codec.decodeRequest`/
1805
+ * `decodeResponse` directly onto a raw bidirectional socket (see
1806
+ * `tests/http1-node-interop.test.ts`), where the *same* object is read from
1807
+ * and then written back to (a server reads the request, then replies on the
1808
+ * same socket); cancelling the read side there tears down the whole
1809
+ * connection out from under the pending write. That was tried and reverted —
1810
+ * see the Task 11 report for the "socket hang up" failure it caused.
1811
+ */
1812
+ async function throwIfPeerError(result, output) {
1813
+ const found = result.envelope.headers.find(([k]) => k.toLowerCase() === PEER_ERROR_HEADER);
1814
+ if (!found) return;
1815
+ await discard(result.body);
1816
+ try {
1817
+ await output.return?.(void 0);
1818
+ } catch {}
1819
+ let payload;
1820
+ try {
1821
+ payload = JSON.parse(found[1]);
1822
+ } catch {
1823
+ payload = { message: found[1] };
1824
+ }
1825
+ throw deserializeError(payload);
1826
+ }
1827
+ /**
1828
+ * Initiate an HTTP call over a `Duplex`. The caller's `call: Duplex` is
1829
+ * obtained from any `webrun-streams-*` adapter's `connect`. Returns the
1830
+ * response envelope and an async iterable over the response body bytes.
1831
+ *
1832
+ * The call is one logical Duplex invocation; multiplexing of concurrent
1833
+ * calls is the adapter's concern (native or `emulateMux`).
1834
+ */
1835
+ async function httpFetch(call, env, body, options = {}) {
1836
+ const codec = options.codec ?? defaultCodec;
1837
+ const output = call(codec.encodeRequest(env, body));
1838
+ const result = await codec.decodeResponse(output, { method: env.method });
1839
+ await throwIfPeerError(result, output);
1840
+ return result;
1841
+ }
1842
+ /**
1843
+ * Wrap an HTTP handler as a `Duplex` so it can be registered with any
1844
+ * `webrun-streams-*` adapter's `serve`.
1845
+ */
1846
+ function httpServe(handler, options = {}) {
1847
+ const codec = options.codec ?? defaultCodec;
1848
+ return async function* httpHandlerDuplex(input) {
1849
+ let decoded;
1850
+ try {
1851
+ decoded = await codec.decodeRequest(input);
1852
+ } catch (error) {
1853
+ if (!(error instanceof HttpParseError)) throw error;
1854
+ yield* encodeErrorResponse(error.codec ?? codec, error, "GET", 400, "Bad Request");
1855
+ return;
1856
+ }
1857
+ const replyCodec = decoded.codec ?? codec;
1858
+ let result;
1859
+ try {
1860
+ result = await handler(decoded.envelope, decoded.body);
1861
+ } catch (error) {
1862
+ yield* encodeErrorResponse(replyCodec, error, decoded.envelope.method);
1863
+ return;
1864
+ }
1865
+ yield* replyCodec.encodeResponse(result.envelope, result.body, { method: decoded.envelope.method });
1866
+ };
1867
+ }
1868
+ //#endregion
1869
+ //#region ../webrun-http-streams/src/request-streams.ts
1870
+ /**
1871
+ * The one place that knows a runtime may not implement request body streams,
1872
+ * and the buffering both fallbacks need. `fetch.ts` and `http-stubs.ts` each
1873
+ * carry a client and a server direction that must agree on the answer, so the
1874
+ * predicate lives here rather than being written out four times.
1875
+ */
1876
+ /**
1877
+ * Whether `Request.prototype` exposes a `body` accessor. Two call sites read
1878
+ * that property, and two more depend on the constructor accepting a
1879
+ * `ReadableStream` as `init.body`; this predicate answers for all four.
1880
+ *
1881
+ * What was verified, and all that is claimed here: Firefox (146 at the time of
1882
+ * writing) has *neither*, and Chromium and Node have *both*. On Firefox it is
1883
+ * not that `body` is `undefined` on the instance —
1884
+ * `Object.getOwnPropertyDescriptor(Request.prototype, "body")` is `null`, the
1885
+ * accessor is genuinely absent — and because a `ReadableStream` is then not a
1886
+ * recognised `BodyInit`, the constructor falls through to the string branch and
1887
+ * stores the literal text `[object ReadableStream]`.
1888
+ *
1889
+ * The two halves are NOT guaranteed to ship together, so do not read this as a
1890
+ * test for "request streams" in general. Safari is the counterexample: it has
1891
+ * had `Request.body` since 11.1 but only accepts a stream as `init.body` from
1892
+ * Technology Preview 250, so a shipping Safari has the reader half without the
1893
+ * upload half and this returns `true` there. That looks benign — WebKit appears
1894
+ * to store the stream on the `Request` rather than stringify it, and its error
1895
+ * comes from `fetch()`, which this package never calls on the objects it builds
1896
+ * — but it is untested, and it is the case to look at first if a Safari report
1897
+ * arrives. The sharper probe, if one is ever needed, is whether
1898
+ * `new Request(url, {method:"POST", body:new ReadableStream(), duplex:"half"})`
1899
+ * has a `content-type` of `text/plain;charset=UTF-8` (stringified) or `null`
1900
+ * (stored); it is not used here because it costs a `Request` and a
1901
+ * `ReadableStream` per call and agrees with the descriptor check on every
1902
+ * runtime measured.
1903
+ *
1904
+ * A capability check, never a user-agent test: the question is what this
1905
+ * runtime does, and the answer flips on its own the day Firefox ships request
1906
+ * streams. Evaluated per call rather than cached at module load so that a test
1907
+ * can install a `Request` without the capability and exercise the real branch
1908
+ * under Node.
1909
+ */
1910
+ function supportsRequestStreams() {
1911
+ return typeof Request === "function" && Object.getOwnPropertyDescriptor(Request.prototype, "body") != null;
1912
+ }
1913
+ /**
1914
+ * Drain a body iterable into one contiguous buffer. Only the fallbacks need it.
1915
+ * Allocates its own buffer rather than reusing `bytes.ts`'s `concatChunks`,
1916
+ * which passes a single chunk straight through: that chunk is a view onto the
1917
+ * decoder's own read buffer, and the result here is handed to `new Request` and
1918
+ * outlives the decode. The allocation is also what makes it a
1919
+ * `Uint8Array<ArrayBuffer>`, which `BodyInit` accepts and the looser
1920
+ * `Uint8Array<ArrayBufferLike>` does not.
1921
+ */
1922
+ async function collectBytes$1(source) {
1923
+ const parts = [];
1924
+ let total = 0;
1925
+ for await (const chunk of source) {
1926
+ if (chunk.byteLength === 0) continue;
1927
+ parts.push(chunk);
1928
+ total += chunk.byteLength;
1929
+ }
1930
+ const out = new Uint8Array(total);
1931
+ let offset = 0;
1932
+ for (const part of parts) {
1933
+ out.set(part, offset);
1934
+ offset += part.byteLength;
1935
+ }
1936
+ return out;
1937
+ }
1938
+ //#endregion
1939
+ //#region ../webrun-http-streams/src/http-stubs.ts
1940
+ const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([
1941
+ 101,
1942
+ 103,
1943
+ 204,
1944
+ 205,
1945
+ 304
1946
+ ]);
1947
+ const REQUEST_FIELDS = [
1948
+ "url",
1949
+ "method",
1950
+ "mode",
1951
+ "credentials",
1952
+ "cache",
1953
+ "redirect",
1954
+ "referrer",
1955
+ "referrerPolicy",
1956
+ "integrity",
1957
+ "keepalive"
1958
+ ];
1959
+ /** `SerializedHttpEnvelope.content` is never absent, only empty. */
1960
+ async function* noBytes() {}
1961
+ async function* oneChunk(chunk) {
1962
+ yield chunk;
1963
+ }
1964
+ /**
1965
+ * Returns an HTTP handler that serializes a Request, hands the envelope to
1966
+ * `send` for transport, and deserializes the reply into a Response. Used on
1967
+ * the caller side.
1968
+ */
387
1969
  function newHttpClientStub(send) {
388
- return async (request) => {
389
- request = await request;
390
- const fields = [
391
- "url",
392
- "method",
393
- "mode",
394
- "credentials",
395
- "cache",
396
- "redirect",
397
- "referrer",
398
- "referrerPolicy",
399
- "integrity",
400
- "keepalive"
401
- ];
402
- const headers = [...request.headers].map(([key, value]) => {
403
- return [key, value]
404
- });
405
- const requestOptions = fields.reduce((options, key) => {
406
- const val = request[key];
407
- if (val !== undefined) options[key] = val;
408
- return options;
409
- }, {
410
- headers
411
- });
412
- const requestContent = (request.body)
413
- ? fromReadableStream(request.body)
414
- : [];
415
- const result = await send({
416
- options: requestOptions,
417
- content: requestContent,
418
- });
419
- let responseBody = null;
420
- let responseOptions = {};
421
- if (!result) {
422
- responseOptions = {
423
- status: 404,
424
- statusText: "Error 404: Not Found",
425
- };
426
- } else {
427
- responseOptions = result.options;
428
- const responseContent = result.content;
429
- if (
430
- requestOptions.method !== "HEAD" &&
431
- requestOptions.method !== "OPTIONS"
432
- ) {
433
- responseBody = toReadableStream(responseContent);
434
- } else if (responseContent && responseContent.return) {
435
- responseContent.return();
436
- }
437
- }
438
- return new Response(responseBody, responseOptions);
439
- };
440
- }
441
-
442
- /**
443
- * This method returns the server-side call handler transforming
444
- * serialized requests to HTTP Request instances, delegating
445
- * their handing to the given `handler` method. This handler
446
- * should handle the HTTP request and return a valid Response instance.
447
- * The resulting instance is serialized as an object with the same
448
- * structure as the initial serialized request:
449
- * - `options` - contains serialized request or response parameters
450
- * - `content` - an async iterator over binary blocks for
451
- * the request or response
452
- * @param {*} handler this handler method accepts standard HTTP Request
453
- * and returns back an HTTP Response instance
454
- * @returns a method deserializing HTTP requests and returning back
455
- * serialized HTTP responses
456
- */
1970
+ return async (requestOrPromise) => {
1971
+ const request = await requestOrPromise;
1972
+ const headers = [...request.headers].map(([k, v]) => [k, v]);
1973
+ const options = {
1974
+ url: request.url,
1975
+ headers
1976
+ };
1977
+ for (const field of REQUEST_FIELDS) {
1978
+ const val = request[field];
1979
+ if (val !== void 0 && field !== "url") options[field] = val;
1980
+ }
1981
+ let content;
1982
+ if (request.body != null) content = fromReadableStream(request.body);
1983
+ else if (!supportsRequestStreams()) content = oneChunk(new Uint8Array(await request.arrayBuffer()));
1984
+ else content = noBytes();
1985
+ const result = await send({
1986
+ options,
1987
+ content
1988
+ });
1989
+ if (!result) return new Response(null, {
1990
+ status: 404,
1991
+ statusText: "Error 404: Not Found"
1992
+ });
1993
+ const responseOptions = result.options;
1994
+ const method = options.method;
1995
+ if (method === "HEAD" || method === "OPTIONS" || NULL_BODY_STATUSES.has(responseOptions.status)) {
1996
+ await discard(result.content);
1997
+ return new Response(null, responseOptions);
1998
+ }
1999
+ return new Response(toReadableStream(result.content[Symbol.asyncIterator]()), responseOptions);
2000
+ };
2001
+ }
2002
+ /**
2003
+ * Returns a server-side transport handler. It deserializes the incoming
2004
+ * request envelope, delegates to `handler`, and serializes the response.
2005
+ */
457
2006
  function newHttpServerStub(handler) {
458
- return async (params) => {
459
- const {
460
- options: requestOptions,
461
- content: requestContent,
462
- } = await params;
463
- let requestBody = undefined;
464
- const { url, mode, headers = [] } = requestOptions;
465
- const requestHeaders = new Headers();
466
- for (let [key, value] of headers) {
467
- requestHeaders.append(key, value);
468
- }
469
- delete requestOptions.mode;
470
- delete requestOptions.url;
471
- if (
472
- requestOptions.method !== "GET" &&
473
- requestOptions.method !== "HEAD" &&
474
- requestOptions.method !== "OPTIONS"
475
- ) {
476
- requestBody = toReadableStream(requestContent);
477
- requestOptions.duplex = "half";
478
- } else if (requestContent && requestContent.return) {
479
- requestContent.return();
480
- }
481
- const request = new Request(url, {
482
- ...requestOptions,
483
- headers: requestHeaders,
484
- // duplex: 'full',
485
- body: requestBody,
486
- });
487
- const response = await handler(request);
488
- const responseOptions = {
489
- status: response.status,
490
- statusText: response.statusText,
491
- headers: Object.fromEntries([...response.headers]),
492
- };
493
- const responseContent = await fromReadableStream(await response.body);
494
- return {
495
- options: responseOptions,
496
- content: responseContent,
497
- };
498
- };
499
- }
500
-
501
- async function* httpToIterator(params) {
502
- const { options, content } = await params;
503
- const encoder = new TextEncoder();
504
- yield encoder.encode(JSON.stringify(options));
505
- yield* content;
506
- }
507
-
508
- async function httpFromIterator(it) {
509
- it = await it;
510
- const { done, value } = await it.next();
511
- let options = {};
512
- if (!done && value) {
513
- const decoder = new TextDecoder();
514
- const str = decoder.decode(value);
515
- options = JSON.parse(str);
516
- }
517
- return {
518
- options,
519
- content: it,
520
- };
521
- }
522
-
2007
+ return async (envelopeOrPromise) => {
2008
+ const { options, content } = await envelopeOrPromise;
2009
+ const { url, method, headers = [], ...rest } = options;
2010
+ const { mode: _mode, ...forwardable } = rest;
2011
+ const requestHeaders = new Headers();
2012
+ for (const [key, value] of headers) requestHeaders.append(key, value);
2013
+ const hasBody = method !== "GET" && method !== "HEAD" && method !== "OPTIONS";
2014
+ const requestInit = {
2015
+ ...forwardable,
2016
+ method,
2017
+ headers: requestHeaders
2018
+ };
2019
+ if (!hasBody) await discard(content);
2020
+ else if (supportsRequestStreams()) {
2021
+ requestInit.body = toReadableStream(content[Symbol.asyncIterator]());
2022
+ requestInit.duplex = "half";
2023
+ } else requestInit.body = await collectBytes$1(content);
2024
+ const response = await handler(new Request(url, requestInit));
2025
+ return {
2026
+ options: {
2027
+ status: response.status,
2028
+ statusText: response.statusText,
2029
+ headers: Object.fromEntries([...response.headers])
2030
+ },
2031
+ content: response.body ? fromReadableStream(response.body) : (async function* () {})()
2032
+ };
2033
+ };
2034
+ }
2035
+ //#endregion
2036
+ //#region ../webrun-http-streams/src/fetch.ts
2037
+ /**
2038
+ * Connection-scoped headers. The codec surfaces them verbatim (decision 12),
2039
+ * but they are meaningless to a `Request`/`Response`, and re-emitting them
2040
+ * from a relay would corrupt its framing.
2041
+ */
2042
+ const HOP_BY_HOP = /* @__PURE__ */ new Set([
2043
+ "connection",
2044
+ "host",
2045
+ "keep-alive",
2046
+ "proxy-authenticate",
2047
+ "proxy-authorization",
2048
+ "te",
2049
+ "trailer",
2050
+ "transfer-encoding",
2051
+ "upgrade"
2052
+ ]);
2053
+ function forwardableHeaders(headers) {
2054
+ return headers.filter(([name]) => !HOP_BY_HOP.has(name.toLowerCase()));
2055
+ }
2056
+ function headersToArray(headers) {
2057
+ const out = [];
2058
+ headers.forEach((value, key) => {
2059
+ out.push([key, value]);
2060
+ });
2061
+ return out;
2062
+ }
2063
+ async function* readableToAsyncIterable(stream) {
2064
+ if (!stream) return;
2065
+ const reader = stream.getReader();
2066
+ let consumed = false;
2067
+ try {
2068
+ while (true) {
2069
+ const { done, value } = await reader.read();
2070
+ if (done) {
2071
+ consumed = true;
2072
+ return;
2073
+ }
2074
+ if (value) yield value;
2075
+ }
2076
+ } finally {
2077
+ if (!consumed) try {
2078
+ await reader.cancel();
2079
+ } catch {}
2080
+ try {
2081
+ reader.releaseLock();
2082
+ } catch {}
2083
+ }
2084
+ }
2085
+ function asyncIterableToReadable(iter) {
2086
+ const it = iter[Symbol.asyncIterator]();
2087
+ return new ReadableStream({
2088
+ async pull(controller) {
2089
+ try {
2090
+ const { done, value } = await it.next();
2091
+ if (done) {
2092
+ controller.close();
2093
+ return;
2094
+ }
2095
+ controller.enqueue(value);
2096
+ } catch (err) {
2097
+ controller.error(err);
2098
+ }
2099
+ },
2100
+ async cancel() {
2101
+ await it.return?.();
2102
+ }
2103
+ });
2104
+ }
2105
+ /**
2106
+ * Run a `Request` through a `Duplex` call and reconstruct the `Response` on
2107
+ * the other side. The request's `signal` is plumbed into the body iteration —
2108
+ * abort terminates the underlying call.
2109
+ */
2110
+ async function fetchOverDuplex(call, request, options = {}) {
2111
+ if (request.signal?.aborted) throw abortReason(request.signal);
2112
+ const env = {
2113
+ url: request.url,
2114
+ method: request.method,
2115
+ headers: headersToArray(request.headers)
2116
+ };
2117
+ let body;
2118
+ if (request.body != null) body = readableToAsyncIterable(request.body);
2119
+ else if (!supportsRequestStreams()) {
2120
+ const buffered = new Uint8Array(await request.arrayBuffer());
2121
+ if (buffered.byteLength > 0) body = [buffered];
2122
+ }
2123
+ const { envelope, body: respBody } = await httpFetch(call, env, body, options);
2124
+ const responseInit = {
2125
+ status: envelope.status,
2126
+ statusText: envelope.statusText,
2127
+ headers: forwardableHeaders(envelope.headers)
2128
+ };
2129
+ if (request.method === "HEAD" || request.method === "OPTIONS" || NULL_BODY_STATUSES.has(envelope.status)) {
2130
+ await respBody.return?.();
2131
+ return new Response(null, responseInit);
2132
+ }
2133
+ return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
2134
+ }
2135
+ /**
2136
+ * Wrap a `(Request) => Promise<Response>` handler as a `Duplex` so it can be
2137
+ * registered with any `webrun-streams-*` adapter's `serve`.
2138
+ */
2139
+ function serveFetchOverDuplex(handler, options = {}) {
2140
+ return httpServe(async (env, body) => {
2141
+ const reqInit = {
2142
+ method: env.method,
2143
+ headers: forwardableHeaders(env.headers)
2144
+ };
2145
+ if (env.method !== "GET" && env.method !== "HEAD") {
2146
+ if (supportsRequestStreams()) {
2147
+ reqInit.body = asyncIterableToReadable(body);
2148
+ reqInit.duplex = "half";
2149
+ } else reqInit.body = await collectBytes$1(body);
2150
+ }
2151
+ const response = await handler(new Request(env.url, reqInit));
2152
+ const respEnv = {
2153
+ status: response.status,
2154
+ statusText: response.statusText,
2155
+ headers: headersToArray(response.headers)
2156
+ };
2157
+ if (env.method === "HEAD" || env.method === "OPTIONS" || NULL_BODY_STATUSES.has(response.status)) {
2158
+ await response.body?.cancel();
2159
+ return {
2160
+ envelope: respEnv,
2161
+ body: void 0
2162
+ };
2163
+ }
2164
+ return {
2165
+ envelope: respEnv,
2166
+ body: response.body ? readableToAsyncIterable(response.body) : void 0
2167
+ };
2168
+ }, options);
2169
+ }
2170
+ async function* withAbort(iter, signal) {
2171
+ if (!signal) {
2172
+ yield* iter;
2173
+ return;
2174
+ }
2175
+ for await (const chunk of iter) {
2176
+ if (signal.aborted) throw abortReason(signal);
2177
+ yield chunk;
2178
+ }
2179
+ }
2180
+ function abortReason(signal) {
2181
+ const reason = signal.reason;
2182
+ if (reason instanceof Error) return reason;
2183
+ const err = new Error(reason === void 0 ? "Aborted" : String(reason));
2184
+ err.name = "AbortError";
2185
+ return err;
2186
+ }
2187
+ //#endregion
2188
+ //#region ../webrun-http-streams/src/duplex-site-builder.ts
2189
+ /**
2190
+ * Host a `SiteHandler` over a `Connect/Serve` pair. The `start(serve, params)`
2191
+ * call hands the handler to any `webrun-streams-*` adapter's `serve`.
2192
+ *
2193
+ * ```ts
2194
+ * import { SiteBuilder } from "@statewalker/webrun-site-builder";
2195
+ * import { DuplexSiteBuilder } from "@statewalker/webrun-http-streams";
2196
+ * import { serve } from "@statewalker/webrun-streams-port";
2197
+ *
2198
+ * const handler = new SiteBuilder()
2199
+ * .setEndpoint("/api/time", () => new Response(new Date().toISOString()))
2200
+ * .build();
2201
+ *
2202
+ * const stop = await new DuplexSiteBuilder().setHandler(handler).start(serve, { port });
2203
+ * // later: await stop();
2204
+ * ```
2205
+ *
2206
+ * Holds no site-configuration state. Endpoints, files, auth belong to the
2207
+ * `SiteHandler` producer (typically `SiteBuilder`).
2208
+ */
2209
+ var DuplexSiteBuilder = class {
2210
+ #handler;
2211
+ #codec;
2212
+ setHandler(handler) {
2213
+ this.#handler = handler;
2214
+ return this;
2215
+ }
2216
+ /** Pin the wire format. Defaults to HTTP/1.1 with legacy acceptance. */
2217
+ setCodec(codec) {
2218
+ this.#codec = codec;
2219
+ return this;
2220
+ }
2221
+ async start(serve, params) {
2222
+ if (!this.#handler) throw new Error("DuplexSiteBuilder.start: setHandler(handler) must be called before start()");
2223
+ const handler = this.#handler;
2224
+ return serve(params, serveFetchOverDuplex(async (req) => handler(req), { codec: this.#codec }));
2225
+ }
2226
+ };
2227
+ //#endregion
2228
+ //#region ../webrun-http-streams/src/http-error.ts
2229
+ var HttpError = class HttpError extends Error {
2230
+ status;
2231
+ statusText;
2232
+ constructor(options = {}) {
2233
+ super(options.message ?? options.statusText ?? "HTTP Error");
2234
+ this.status = options.status;
2235
+ this.statusText = options.statusText;
2236
+ }
2237
+ getResponseOptions(options = {}) {
2238
+ return {
2239
+ ...this.toJson(),
2240
+ ...options
2241
+ };
2242
+ }
2243
+ toJson() {
2244
+ return {
2245
+ status: this.status,
2246
+ statusText: this.statusText,
2247
+ message: this.message
2248
+ };
2249
+ }
2250
+ static fromError(error) {
2251
+ if (error instanceof HttpError) return error;
2252
+ const message = error instanceof Error ? error.message : String(error);
2253
+ return new HttpError({
2254
+ status: 500,
2255
+ statusText: "Bad Request",
2256
+ message
2257
+ });
2258
+ }
2259
+ static errorResourceNotFound(options = {}) {
2260
+ return new HttpError({
2261
+ status: 404,
2262
+ statusText: "Error 404: Resource not found",
2263
+ ...options
2264
+ });
2265
+ }
2266
+ static errorForbidden(options = {}) {
2267
+ return new HttpError({
2268
+ status: 403,
2269
+ statusText: "Error 403: Forbidden",
2270
+ ...options
2271
+ });
2272
+ }
2273
+ static errorResourceGone(options = {}) {
2274
+ return new HttpError({
2275
+ status: 410,
2276
+ statusText: "Error 410: Resource Gone",
2277
+ ...options
2278
+ });
2279
+ }
2280
+ static errorInternalError(options = {}) {
2281
+ return new HttpError({
2282
+ ...options,
2283
+ status: 500,
2284
+ statusText: "Error 500: Internal error"
2285
+ });
2286
+ }
2287
+ };
2288
+ //#endregion
2289
+ //#region src/http/http-send-recieve.ts
2290
+ async function* httpToIterator(envelopeOrPromise) {
2291
+ const { options, content } = await envelopeOrPromise;
2292
+ yield new TextEncoder().encode(JSON.stringify(options));
2293
+ yield* content;
2294
+ }
2295
+ async function httpFromIterator(iterable) {
2296
+ const it = (await iterable)[Symbol.asyncIterator]();
2297
+ const { done, value } = await it.next();
2298
+ let options = {};
2299
+ if (!done && value) {
2300
+ const str = new TextDecoder().decode(value);
2301
+ options = JSON.parse(str);
2302
+ }
2303
+ return {
2304
+ options,
2305
+ content: { [Symbol.asyncIterator]() {
2306
+ return it;
2307
+ } }
2308
+ };
2309
+ }
2310
+ /**
2311
+ * @deprecated For new code, prefer the `MessagePort`-based stack from
2312
+ * `@statewalker/webrun-http-port`. Once a `MessagePort` is established between
2313
+ * page and worker, `httpServe(port, handler)` provides equivalent semantics
2314
+ * with `callBidi` multiplexing, full-duplex streaming, and `AbortSignal`.
2315
+ * This helper remains for existing ServiceWorker setups that still consume the
2316
+ * `MessageTarget` surface; it will be reimplemented on top of
2317
+ * `webrun-http-port` in a follow-up release.
2318
+ */
523
2319
  function handleHttpRequests(communicationPort, handler) {
524
- const serverStub = newHttpServerStub(handler);
525
- return handleStreams(communicationPort, (it) => {
526
- return httpToIterator(serverStub(httpFromIterator(it)));
527
- });
2320
+ const serverStub = newHttpServerStub(handler);
2321
+ return handleStreams(communicationPort, async (it) => {
2322
+ const envelope = await httpFromIterator(it);
2323
+ return httpToIterator(await serverStub(envelope));
2324
+ });
528
2325
  }
529
-
2326
+ /**
2327
+ * @deprecated For new code, prefer the `MessagePort`-based stack from
2328
+ * `@statewalker/webrun-http-port/fetch`. Once the page and SW share a
2329
+ * `MessagePort`, `fetchOverPort(port, request)` provides the same
2330
+ * `Request → Response` semantics with multiplexing via `callBidi`, JSONL
2331
+ * envelope framing, and native `AbortSignal` support. This helper remains for
2332
+ * existing ServiceWorker setups; it will be reimplemented on top of
2333
+ * `webrun-http-port` in a follow-up release.
2334
+ */
530
2335
  async function sendHttpRequest(communicationPort, request) {
531
- const clientStub = newHttpClientStub(async (req) => {
532
- return await httpFromIterator(
533
- sendStream(communicationPort, httpToIterator(req)),
534
- );
535
- });
536
- return await clientStub(request);
537
- }
538
-
2336
+ return await newHttpClientStub(async (req) => {
2337
+ return await httpFromIterator(sendStream(communicationPort, httpToIterator(req)));
2338
+ })(request);
2339
+ }
2340
+ //#endregion
2341
+ //#region src/relay/split-service-url.ts
2342
+ /**
2343
+ * Splits a URL of the form `<base>/<separator><key>/<path>` into parts.
2344
+ * Example: `https://host/~FS/a/b` → `{ baseUrl: "https://host/~FS/", key: "FS", path: "a/b" }`.
2345
+ */
539
2346
  function splitServiceUrl(url, separator = "~") {
540
- let key = "";
541
- let path = "";
542
- let str = url + "";
543
- const idx = str.indexOf(separator);
544
- let baseUrl = "";
545
- if (idx >= 0) {
546
- baseUrl = str.substring(0, idx + separator.length);
547
- str.substring(idx + separator.length)
548
- .replace(/^([^\/]+)/, (match, $1) => {
549
- baseUrl += match;
550
- if (baseUrl.length < str.length) baseUrl += "/";
551
- key = $1;
552
- path = str.substring(baseUrl.length);
553
- return "";
554
- });
555
- }
556
- return {
557
- url,
558
- key,
559
- baseUrl,
560
- path,
561
- };
562
- }
563
-
2347
+ const str = `${url}`;
2348
+ const idx = str.indexOf(separator);
2349
+ let baseUrl = "";
2350
+ let key = "";
2351
+ let path = "";
2352
+ if (idx >= 0) {
2353
+ baseUrl = str.substring(0, idx + separator.length);
2354
+ str.substring(idx + separator.length).replace(/^([^/]+)/, (match, $1) => {
2355
+ baseUrl += match;
2356
+ if (baseUrl.length < str.length) baseUrl += "/";
2357
+ key = $1;
2358
+ path = str.substring(baseUrl.length);
2359
+ return "";
2360
+ });
2361
+ }
2362
+ return {
2363
+ url: str,
2364
+ key,
2365
+ baseUrl,
2366
+ path
2367
+ };
2368
+ }
2369
+ //#endregion
2370
+ //#region src/relay/index.ts
2371
+ /**
2372
+ * Returns a MessagePort that transparently bridges messages to/from the
2373
+ * ServiceWorker controlling this page.
2374
+ */
564
2375
  function newServiceWorkerPort() {
565
- // Bind the service worker to
566
- const channel = new MessageChannel();
567
- channel.port1.onmessage = (event) => {
568
- navigator.serviceWorker.controller.postMessage(event.data, event.ports);
569
- };
570
- navigator.serviceWorker.addEventListener("message", (event) => {
571
- channel.port1.postMessage(event.data, event.ports);
572
- });
573
- return channel.port2;
574
- }
575
-
576
- async function initServiceWorker1({ swUrl, scopeUrl, type }) {
577
- const {
578
- installing,
579
- waiting,
580
- active,
581
- } = await navigator.serviceWorker.register(swUrl, {
582
- // type: "module",
583
- scope: scopeUrl,
584
- });
585
- (installing || waiting || active).addEventListener("statechange", (e) => {
586
- console.log("state", e.target.state);
587
- });
588
-
589
- const serviceWorker = navigator.serviceWorker;
590
- await new Promise((resolve) => {
591
- // Resolve right away if this page is already controlled.
592
- if (serviceWorker.controller) {
593
- resolve();
594
- } else {
595
- const onChange = () => {
596
- resolve();
597
- serviceWorker.removeEventListener("controllerchange", onChange);
598
- serviceWorker.oncontrollerchange = null;
599
- };
600
- serviceWorker.addEventListener("controllerchange", onChange);
601
- serviceWorker.oncontrollerchange = onChange;
602
- }
603
- });
604
- const worker = serviceWorker.controller;
605
- const isActivated = () => worker.state === "activated";
606
- return await new Promise(async (resolve) => {
607
- if (isActivated()) resolve(worker);
608
- else {
609
- worker.onstatechange = () => {
610
- if (!isActivated()) return;
611
- worker.onstatechange = null;
612
- resolve(worker);
613
- };
614
- }
615
- });
616
- }
617
-
2376
+ const channel = new MessageChannel();
2377
+ channel.port1.onmessage = (event) => {
2378
+ navigator.serviceWorker.controller?.postMessage(event.data, [...event.ports]);
2379
+ };
2380
+ navigator.serviceWorker.addEventListener("message", (event) => {
2381
+ channel.port1.postMessage(event.data, [...event.ports]);
2382
+ });
2383
+ return channel.port2;
2384
+ }
2385
+ /**
2386
+ * Registers a ServiceWorker and resolves once it's activated and controlling the page.
2387
+ */
618
2388
  async function initServiceWorker({ swUrl, scopeUrl, type }) {
619
- await navigator.serviceWorker.register(swUrl, {
620
- type,
621
- scope: scopeUrl,
622
- });
623
- const worker = await getServiceWorkerController();
624
- await awaitServiceWorkerActivation(worker);
625
- return worker;
626
-
627
- async function getServiceWorkerController() {
628
- return await new Promise((resolve) => {
629
- // Resolve right away if this page is already controlled.
630
- const serviceWorker = navigator.serviceWorker;
631
- if (serviceWorker.controller) {
632
- resolve(serviceWorker.controller);
633
- } else {
634
- const onChange = () => {
635
- resolve(serviceWorker.controller);
636
- serviceWorker.removeEventListener("controllerchange", onChange);
637
- serviceWorker.oncontrollerchange = null;
638
- };
639
- serviceWorker.addEventListener("controllerchange", onChange);
640
- serviceWorker.oncontrollerchange = onChange;
641
- }
642
- });
643
- }
644
-
645
- // ------------------------------
646
- // Await until the service is ready
647
- async function awaitServiceWorkerActivation(worker) {
648
- const isActivated = () => worker.state === "activated";
649
- await new Promise(async (resolve) => {
650
- if (isActivated()) resolve();
651
- else {
652
- worker.onstatechange = () => {
653
- if (!isActivated()) return;
654
- worker.onstatechange = null;
655
- resolve();
656
- };
657
- }
658
- });
659
- }
660
- }
661
-
662
- async function initHttpService(handler, {
663
- key,
664
- port,
665
- }) {
666
- return await registerConnectionsHandler({
667
- key,
668
- communicationPort: port,
669
- handler: async (event, data, callPort) => {
670
- handleHttpRequests(callPort, handler);
671
- return true;
672
- },
673
- });
674
- }
675
-
676
- async function callHttpService(request, {
677
- key,
678
- port,
679
- }) {
680
- const callPort = await initializeConnection({
681
- key,
682
- communicationPort: port,
683
- });
684
- const result = await sendHttpRequest(callPort, request);
685
- return result;
686
- }
687
-
688
- function getRelayWindowMessageHandler({
689
- swUrl = new URL("./index-sw.js", import.meta.url) + "",
690
- scopeUrl = new URL("../", import.meta.url) + "",
691
- } = {}) {
692
- let externalPort;
693
- return async (ev) => {
694
- if (ev.data.type === "CONNECT") {
695
- const newExternalPort = (ev.ports || [])[0];
696
- if (externalPort) {
697
- newExternalPort.close();
698
- return;
699
- }
700
- externalPort = newExternalPort;
701
- await initServiceWorker({
702
- swUrl,
703
- scopeUrl,
704
- });
705
- const serviceWorkerPort = newServiceWorkerPort();
706
- serviceWorkerPort.onmessage = (event) => {
707
- externalPort.postMessage(event.data, event.ports);
708
- };
709
-
710
- externalPort.onmessage = (event) => {
711
- serviceWorkerPort.postMessage(event.data, event.ports);
712
- };
713
- }
714
- };
715
- }
716
-
717
- async function newRemoteRelayChannel({
718
- baseUrl = new URL("../public-relay/", import.meta.url),
719
- url = new URL("relay.html", baseUrl),
720
- container = document.body,
721
- } = {}) {
722
- const messageChannel = new MessageChannel();
723
- const { iframe, promise } = await newIFrame(url);
724
- Object.assign(iframe.style, {
725
- // height: "100px"
726
- position: "fixed",
727
- width: "1px",
728
- height: "1px",
729
- top: "-1000",
730
- left: "-1000",
731
- display: "block",
732
- opacity: "0",
733
- border: "none",
734
- outline: "none",
735
- });
736
- container.appendChild(iframe);
737
- promise.then(() => {
738
- iframe.contentWindow.postMessage(
739
- {
740
- type: "CONNECT",
741
- },
742
- "*",
743
- [messageChannel.port1],
744
- );
745
- });
746
- return {
747
- baseUrl,
748
- port: messageChannel.port2,
749
- close: () => {
750
- iframe.parentElement && iframe.parentElement.removeChild(iframe);
751
- messageChannel.port1.close();
752
- messageChannel.port2.close();
753
- },
754
- };
755
-
756
- async function newIFrame(url) {
757
- const iframe = document.createElement("iframe");
758
- iframe.src = url;
759
- Object.assign(iframe.style, {
760
- padding: 0,
761
- margin: 0,
762
- border: "none",
763
- outline: "none",
764
- width: "100%",
765
- height: "100%",
766
- });
767
- return {
768
- iframe,
769
- promise: new Promise((resolve, reject) => {
770
- try {
771
- iframe.onerror = reject;
772
- iframe.onload = () => resolve(iframe);
773
- } catch (error) {
774
- reject(error);
775
- }
776
- }),
777
- };
778
- }
779
- }
780
- // -------------------------------------------------------
781
-
782
- async function initializeConnection({
783
- key,
784
- communicationPort,
785
- ...options
786
- }) {
787
- const channel = new MessageChannel();
788
- const accepted = await callChannel(
789
- communicationPort,
790
- "CONNECT",
791
- { key, ...options },
792
- channel.port2,
793
- );
794
- if (!accepted) {
795
- channel.port1.close();
796
- channel.port2.close();
797
- return null;
798
- }
799
- return channel.port1;
800
- }
801
-
802
- async function registerConnectionsHandler({
803
- key,
804
- handler,
805
- communicationPort,
806
- }) {
807
- const [register, cleanup] = newRegistry();
808
- await callChannel(communicationPort, "REGISTER", { key });
809
- register(() => callChannel(communicationPort, "UNREGISTER", { key }));
810
- register(
811
- handleChannelCalls(
812
- communicationPort,
813
- "CONNECT",
814
- handler,
815
- ),
816
- );
817
- return cleanup;
818
- }
819
-
820
- export { callChannel, callHttpService, deserializeError, fromReadableStream, getRelayWindowMessageHandler, handleChannelCalls, handleHttpRequests, handleStreams, initHttpService, initServiceWorker, initServiceWorker1, initializeConnection, newHttpClientStub, newHttpServerStub, newInvokationChannel, newRegistry, newRemoteRelayChannel, newServiceWorkerPort, recieveData, registerConnectionsHandler, sendData, sendHttpRequest, sendStream, serializeError, splitServiceUrl, toReadableStream };
2389
+ await navigator.serviceWorker.register(swUrl, {
2390
+ type,
2391
+ scope: scopeUrl
2392
+ });
2393
+ const worker = await getServiceWorkerController();
2394
+ await awaitServiceWorkerActivation(worker);
2395
+ return worker;
2396
+ }
2397
+ function getServiceWorkerController() {
2398
+ return new Promise((resolve) => {
2399
+ const container = navigator.serviceWorker;
2400
+ if (container.controller) {
2401
+ resolve(container.controller);
2402
+ return;
2403
+ }
2404
+ const onChange = () => {
2405
+ if (!container.controller) return;
2406
+ resolve(container.controller);
2407
+ container.removeEventListener("controllerchange", onChange);
2408
+ };
2409
+ container.addEventListener("controllerchange", onChange);
2410
+ });
2411
+ }
2412
+ function awaitServiceWorkerActivation(worker) {
2413
+ return new Promise((resolve) => {
2414
+ if (worker.state === "activated") {
2415
+ resolve();
2416
+ return;
2417
+ }
2418
+ const onStateChange = () => {
2419
+ if (worker.state !== "activated") return;
2420
+ worker.removeEventListener("statechange", onStateChange);
2421
+ resolve();
2422
+ };
2423
+ worker.addEventListener("statechange", onStateChange);
2424
+ });
2425
+ }
2426
+ /**
2427
+ * Registers `handler` as the server for the given service `key` on the relay.
2428
+ * Returns a cleanup function that unregisters the service.
2429
+ */
2430
+ async function initHttpService(handler, { key, port }) {
2431
+ return await registerConnectionsHandler({
2432
+ key,
2433
+ communicationPort: port,
2434
+ handler: async (_event, _data, callPort) => {
2435
+ handleHttpRequests(callPort, handler);
2436
+ return true;
2437
+ }
2438
+ });
2439
+ }
2440
+ /**
2441
+ * Sends a `Request` to the service registered under `key` on the relay and
2442
+ * resolves with the corresponding `Response`.
2443
+ */
2444
+ async function callHttpService(request, { key, port }) {
2445
+ const callPort = await initializeConnection({
2446
+ key,
2447
+ communicationPort: port
2448
+ });
2449
+ if (!callPort) throw new Error(`No service with key "${key}"`);
2450
+ return await sendHttpRequest(callPort, request);
2451
+ }
2452
+ /**
2453
+ * Returns a `window.onmessage` handler for use inside the relay iframe:
2454
+ * it accepts a CONNECT message, starts the relay ServiceWorker, and bridges
2455
+ * the parent's MessagePort with the SW.
2456
+ */
2457
+ function getRelayWindowMessageHandler({ swUrl = `${new URL("./index-sw.js", import.meta.url)}`, scopeUrl = `${new URL("../", import.meta.url)}` } = {}) {
2458
+ let externalPort;
2459
+ return async (ev) => {
2460
+ if (ev.data?.type !== "CONNECT") return;
2461
+ const newExternalPort = ev.ports?.[0];
2462
+ if (!newExternalPort) return;
2463
+ if (externalPort) {
2464
+ newExternalPort.close();
2465
+ return;
2466
+ }
2467
+ externalPort = newExternalPort;
2468
+ await initServiceWorker({
2469
+ swUrl,
2470
+ scopeUrl
2471
+ });
2472
+ const serviceWorkerPort = newServiceWorkerPort();
2473
+ serviceWorkerPort.onmessage = (event) => {
2474
+ externalPort?.postMessage(event.data, [...event.ports]);
2475
+ };
2476
+ externalPort.onmessage = (event) => {
2477
+ serviceWorkerPort.postMessage(event.data, [...event.ports]);
2478
+ };
2479
+ };
2480
+ }
2481
+ /**
2482
+ * Embeds a hidden relay iframe, establishes a MessageChannel with it, and
2483
+ * returns the port to be used with `initHttpService` / `callHttpService`.
2484
+ */
2485
+ async function newRemoteRelayChannel({ baseUrl = new URL("../public-relay/", import.meta.url), url = new URL("relay.html", baseUrl), container = document.body } = {}) {
2486
+ const messageChannel = new MessageChannel();
2487
+ const { iframe, promise } = newIFrame(url);
2488
+ Object.assign(iframe.style, {
2489
+ position: "fixed",
2490
+ width: "1px",
2491
+ height: "1px",
2492
+ top: "-1000px",
2493
+ left: "-1000px",
2494
+ display: "block",
2495
+ opacity: "0",
2496
+ border: "none",
2497
+ outline: "none"
2498
+ });
2499
+ container.appendChild(iframe);
2500
+ promise.then(() => {
2501
+ iframe.contentWindow?.postMessage({ type: "CONNECT" }, "*", [messageChannel.port1]);
2502
+ });
2503
+ return {
2504
+ baseUrl,
2505
+ port: messageChannel.port2,
2506
+ close: () => {
2507
+ iframe.parentElement?.removeChild(iframe);
2508
+ messageChannel.port1.close();
2509
+ messageChannel.port2.close();
2510
+ }
2511
+ };
2512
+ function newIFrame(src) {
2513
+ const iframe = document.createElement("iframe");
2514
+ iframe.src = `${src}`;
2515
+ Object.assign(iframe.style, {
2516
+ padding: "0",
2517
+ margin: "0",
2518
+ border: "none",
2519
+ outline: "none",
2520
+ width: "100%",
2521
+ height: "100%"
2522
+ });
2523
+ return {
2524
+ iframe,
2525
+ promise: new Promise((resolve, reject) => {
2526
+ iframe.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to load ${src}`));
2527
+ iframe.onload = () => resolve(iframe);
2528
+ })
2529
+ };
2530
+ }
2531
+ }
2532
+ async function initializeConnection({ key, communicationPort, ...options }) {
2533
+ const channel = new MessageChannel();
2534
+ if (!await callChannel(communicationPort, "CONNECT", {
2535
+ key,
2536
+ ...options
2537
+ }, channel.port2)) {
2538
+ channel.port1.close();
2539
+ channel.port2.close();
2540
+ return null;
2541
+ }
2542
+ return channel.port1;
2543
+ }
2544
+ async function registerConnectionsHandler({ key, handler, communicationPort }) {
2545
+ const [register, cleanup] = newRegistry();
2546
+ await callChannel(communicationPort, "REGISTER", { key });
2547
+ register(() => callChannel(communicationPort, "UNREGISTER", { key }));
2548
+ register(handleChannelCalls(communicationPort, "CONNECT", async (event, data, port) => {
2549
+ return await handler(event, data, port);
2550
+ }));
2551
+ return cleanup;
2552
+ }
2553
+ //#endregion
2554
+ export { DuplexSiteBuilder, HttpError, HttpParseError, PEER_ERROR_HEADER, TransportClosedError, callChannel, callHttpService, collect, collectBytes, collectString, decodeJsonl, decodeMessage, decodeText, defaultCodec, deserializeError, emulateMux, encodeJsonl, encodeMessage, encodeText, fetchOverDuplex, fromReadableStream, getRelayWindowMessageHandler, handleChannelCalls, handleHttpRequests, handleStreams, httpCodec, httpFetch, httpServe, initHttpService, initServiceWorker, initializeConnection, joinLines, jsonEnvelopeCodec, map, newAsyncGenerator, newHttpClientStub, newHttpCodec, newHttpServerStub, newInvokationChannel, newRegistry, newRemoteRelayChannel, newServiceWorkerPort, newSniffingCodec, normalizeToUint8Array, recieveIterator, registerConnectionsHandler, sendHttpRequest, sendIterator, sendStream, serializeError, serveFetchOverDuplex, splitLines, splitServiceUrl, toChunks, toReadableStream };