@statewalker/webrun-http-browser 0.3.3 → 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 (93) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +321 -51
  3. package/demo/demo-1.html +109 -194
  4. package/demo/demo-2.html +118 -299
  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-2 copy.html +0 -598
  69. package/demo/demo-3.html +0 -167
  70. package/dist/index-sw-umd.js +0 -798
  71. package/dist/index-sw-umd.min.js +0 -2
  72. package/dist/index-sw.js +0 -791
  73. package/dist/index-sw.min.js +0 -2
  74. package/dist/index-umd.js +0 -853
  75. package/dist/index-umd.min.js +0 -2
  76. package/dist/index.min.js +0 -2
  77. package/index.js +0 -1
  78. package/src/core/data-calls.js +0 -36
  79. package/src/core/data-channels.js +0 -192
  80. package/src/core/data-send-recieve.js +0 -28
  81. package/src/core/errors.js +0 -13
  82. package/src/core/index.js +0 -4
  83. package/src/http/HttpError.js +0 -59
  84. package/src/http/http-send-recieve.js +0 -40
  85. package/src/http/http-stubs.js +0 -135
  86. package/src/http/index.js +0 -3
  87. package/src/http/readable-streams.js +0 -33
  88. package/src/index.js +0 -7
  89. package/src/relay/index-sw.js +0 -216
  90. package/src/relay/index.js +0 -263
  91. package/src/relay/splitServiceUrl.js +0 -24
  92. package/src/sw/http-sw-dispatcher.js +0 -128
  93. package/src/sw/sw-dispatcher.js +0 -264
package/dist/sw.js ADDED
@@ -0,0 +1,1076 @@
1
+ //#region ../webrun-http-streams/src/bytes.ts
2
+ function toAsyncIterator(input) {
3
+ const asyncIter = input[Symbol.asyncIterator];
4
+ if (asyncIter) return asyncIter.call(input);
5
+ const syncIter = input[Symbol.iterator]();
6
+ return { next() {
7
+ return Promise.resolve(syncIter.next());
8
+ } };
9
+ }
10
+ /**
11
+ * Discard an iterable we are contractually forbidden from consuming — a body
12
+ * skipped for HEAD/204, or one abandoned because the peer reported an error.
13
+ *
14
+ * The `.next()` is not optional: `.return()` on a generator still in suspended
15
+ * start is a no-op, so the body never runs and its `try/finally` never unwinds.
16
+ * Without it a wrapped ReadableStream or socket is never cancelled.
17
+ */
18
+ async function discard(source) {
19
+ if (source === void 0) return;
20
+ const it = toAsyncIterator(source);
21
+ try {
22
+ await it.next();
23
+ await it.return?.();
24
+ } catch {}
25
+ }
26
+ //#endregion
27
+ //#region ../webrun-streams/src/errors.ts
28
+ function serializeError(error) {
29
+ if (error instanceof Error) {
30
+ const out = {
31
+ message: error.message,
32
+ stack: error.stack
33
+ };
34
+ const bag = error;
35
+ for (const key of Object.keys(bag)) out[key] = bag[key];
36
+ return out;
37
+ }
38
+ if (typeof error === "object" && error !== null) {
39
+ const bag = error;
40
+ return {
41
+ message: String(bag.message ?? error),
42
+ ...bag
43
+ };
44
+ }
45
+ return { message: String(error) };
46
+ }
47
+ function deserializeError(error) {
48
+ const payload = typeof error === "string" ? { message: error } : error;
49
+ return Object.assign(new Error(payload.message), payload);
50
+ }
51
+ //#endregion
52
+ //#region ../webrun-streams/src/new-async-generator.ts
53
+ /**
54
+ * The newAsyncGenerator function creates async generators from callback-based initialization
55
+ * functions, providing a bridge between imperative event handling and declarative async
56
+ * iteration patterns.
57
+ *
58
+ * The initialization function receives two callback functions that control the generator's
59
+ * behavior. The `next` function yields values to consumers, while the `done` function
60
+ * signals completion or error conditions. The initialization function can return a cleanup
61
+ * function that will be called when the generator is closed or an error occurs.
62
+ *
63
+ * The generator manages an internal queue of values and completion signals, ensuring that
64
+ * producers can yield values without overwhelming consumers. Proper backpressure is implemented
65
+ * by having the `next` and `done` functions return promises that resolve only after the
66
+ * consumer has processed the values. This prevents memory leaks and ensures that producers
67
+ * are aware of whether their values were successfully handled.
68
+ * The generator also handles cleanup by draining any remaining items in the queue and notifying
69
+ * producers that their values were not processed if the generator is closed early. This ensures
70
+ * that resources are properly managed and that no memory leaks occur.
71
+ *
72
+ * Example usage:
73
+ * ```typescript
74
+ * const asyncGen = newAsyncGenerator<number>((next, done) => {
75
+ * let count = 0;
76
+ * const interval = setInterval(() => {
77
+ * if (count < 5) {
78
+ * next(count++);
79
+ * } else {
80
+ * done();
81
+ * clearInterval(interval);
82
+ * }
83
+ * }, 1000);
84
+ * return () => clearInterval(interval); // Cleanup function
85
+ * });
86
+ * (async () => {
87
+ * for await (const num of asyncGen) {
88
+ * console.log(num); // Logs numbers 0 to 4 at 1 second intervals
89
+ * }
90
+ * console.log("Completed");
91
+ * })();
92
+ * ```
93
+ * @template T The type of values yielded by the generator
94
+ * @template E The type of errors that can be thrown; defaults to Error
95
+ * @param init Initialization function that sets up the generator behavior with next/done callbacks;
96
+ * The first parameter - `next` function - is used to yield values to consumers;
97
+ * It returns a Promise<boolean> indicating whether the value was successfully handled;
98
+ * The second parameter - `done` function - is used to signal completion or error;
99
+ * It returns a Promise<boolean> indicating whether the completion was successfully handled;
100
+ * The initialization function can optionally return a cleanup function that will be called
101
+ * when the generator is closed or an error occurs;
102
+ * @param skipValues If true, only the most recent value is kept in the queue,
103
+ * skipping intermediate values (not consumed values are considered skipped);
104
+ * This is useful for scenarios where only the latest value matters, such as UI updates.
105
+ * Defaults to false, meaning all values are queued and processed in order.
106
+ * @returns AsyncGenerator that properly manages backpressure and resource cleanup
107
+ */
108
+ async function* newAsyncGenerator(init, skipValues = false) {
109
+ let head;
110
+ let tail;
111
+ /** Flag to prevent new values from being queued after generator closes */
112
+ let closed = false;
113
+ /** Wake-up function to notify the generator loop of new items */
114
+ let wakeUp;
115
+ /**
116
+ * Drains the internal queue, notifying all pending producers that their values
117
+ * were not processed due to generator closure. This prevents memory leaks and
118
+ * ensures proper backpressure signaling.
119
+ */
120
+ const drainQueue = () => {
121
+ for (; head; head = head.next) {
122
+ closed = closed || head.done;
123
+ head.resolve(false);
124
+ }
125
+ tail = void 0;
126
+ };
127
+ /**
128
+ * Enqueues a value or completion signal with a promise that resolves when the item
129
+ * is processed. This enables backpressure by allowing producers to know when their
130
+ * values have been consumed.
131
+ */
132
+ const enqueue = (params) => {
133
+ if (skipValues) drainQueue();
134
+ return !closed ? new Promise((resolve) => {
135
+ const next = {
136
+ ...params,
137
+ next: void 0,
138
+ resolve
139
+ };
140
+ if (tail) tail.next = next;
141
+ tail = next;
142
+ if (!head) head = tail;
143
+ wakeUp?.();
144
+ }) : Promise.resolve(false);
145
+ };
146
+ /**
147
+ * Producer function to yield a value to consumers. Returns a promise that resolves
148
+ * to true if the value was successfully processed, false if the generator is closed
149
+ * or the value was skipped due to backpressure.
150
+ */
151
+ const next = (value) => enqueue({
152
+ done: false,
153
+ value
154
+ });
155
+ /**
156
+ * Producer function to signal completion or error. Optional error parameter will
157
+ * cause the generator to throw that error to consumers.
158
+ */
159
+ const done = (error) => enqueue({
160
+ done: true,
161
+ error
162
+ });
163
+ const unsubscribe = init(next, done);
164
+ try {
165
+ while (!closed) {
166
+ if (!head) {
167
+ await new Promise((resolve) => {
168
+ wakeUp = resolve;
169
+ }).then(() => {
170
+ wakeUp = void 0;
171
+ });
172
+ continue;
173
+ }
174
+ const slot = head;
175
+ head = head.next;
176
+ if (tail === slot) tail = head;
177
+ try {
178
+ if (slot.done) {
179
+ closed = true;
180
+ if (slot.error !== void 0) throw slot.error;
181
+ break;
182
+ }
183
+ yield slot.value;
184
+ } finally {
185
+ slot.resolve(true);
186
+ }
187
+ }
188
+ } finally {
189
+ /**
190
+ * Cleanup phase - ensures proper resource management and notification of any
191
+ * remaining producers. This runs whether the generator completes normally,
192
+ * encounters an error, or is closed early by the consumer.
193
+ */
194
+ closed = true;
195
+ wakeUp?.();
196
+ if (typeof unsubscribe === "function") await unsubscribe();
197
+ /**
198
+ * Drain any remaining items in the queue and notify their producers that
199
+ * the values were not processed. This prevents memory leaks and ensures
200
+ * proper backpressure signaling.
201
+ */
202
+ drainQueue();
203
+ }
204
+ }
205
+ //#endregion
206
+ //#region ../webrun-streams/src/readable-streams.ts
207
+ function toReadableStream(it) {
208
+ return new ReadableStream({ async pull(controller) {
209
+ let handled = false;
210
+ try {
211
+ while (true) {
212
+ const slot = await it.next();
213
+ if (!slot || slot.done) break;
214
+ const value = await slot.value;
215
+ controller.enqueue(value);
216
+ }
217
+ } catch (error) {
218
+ handled = true;
219
+ controller.error(error);
220
+ } finally {
221
+ if (!handled) controller.close();
222
+ }
223
+ } });
224
+ }
225
+ async function* fromReadableStream(stream) {
226
+ const reader = stream.getReader();
227
+ while (true) {
228
+ const { done, value } = await reader.read();
229
+ if (done) break;
230
+ if (value !== void 0) yield value;
231
+ }
232
+ }
233
+ //#endregion
234
+ //#region ../webrun-streams/src/recieve-iterator.ts
235
+ /**
236
+ * Inverse of {@link sendIterator}: turns a sequence of `{done, value, error}`
237
+ * chunks (delivered to the supplied callback by `installer`) into an async
238
+ * generator.
239
+ *
240
+ * The `installer` is given a `deliver` function. It should call `deliver`
241
+ * once for each incoming chunk and may return a cleanup callback that
242
+ * runs when the consumer stops iterating.
243
+ */
244
+ function recieveIterator(installer) {
245
+ return newAsyncGenerator((next, done) => {
246
+ const cleanup = installer(async (chunk = { done: true }) => {
247
+ const { done: isDone = true, value, error } = chunk;
248
+ if (error) return await done(error);
249
+ if (isDone) return await done();
250
+ return await next(value);
251
+ });
252
+ if (cleanup) return async () => {
253
+ await cleanup();
254
+ };
255
+ });
256
+ }
257
+ //#endregion
258
+ //#region ../webrun-streams/src/send-iterator.ts
259
+ /**
260
+ * Drain an async iterator into a sink that consumes one chunk at a time.
261
+ *
262
+ * Each yielded value becomes `{ done: false, value }`. Completion emits
263
+ * `{ done: true }`. If the iterator throws, the error is caught and the
264
+ * final `done` chunk carries it so the peer can rethrow on its side.
265
+ */
266
+ async function sendIterator(send, it) {
267
+ let error;
268
+ try {
269
+ for await (const value of it) await send({
270
+ done: false,
271
+ value
272
+ });
273
+ } catch (err) {
274
+ error = err;
275
+ } finally {
276
+ await send({
277
+ done: true,
278
+ error
279
+ });
280
+ }
281
+ }
282
+ //#endregion
283
+ //#region ../webrun-http-streams/src/request-streams.ts
284
+ /**
285
+ * The one place that knows a runtime may not implement request body streams,
286
+ * and the buffering both fallbacks need. `fetch.ts` and `http-stubs.ts` each
287
+ * carry a client and a server direction that must agree on the answer, so the
288
+ * predicate lives here rather than being written out four times.
289
+ */
290
+ /**
291
+ * Whether `Request.prototype` exposes a `body` accessor. Two call sites read
292
+ * that property, and two more depend on the constructor accepting a
293
+ * `ReadableStream` as `init.body`; this predicate answers for all four.
294
+ *
295
+ * What was verified, and all that is claimed here: Firefox (146 at the time of
296
+ * writing) has *neither*, and Chromium and Node have *both*. On Firefox it is
297
+ * not that `body` is `undefined` on the instance —
298
+ * `Object.getOwnPropertyDescriptor(Request.prototype, "body")` is `null`, the
299
+ * accessor is genuinely absent — and because a `ReadableStream` is then not a
300
+ * recognised `BodyInit`, the constructor falls through to the string branch and
301
+ * stores the literal text `[object ReadableStream]`.
302
+ *
303
+ * The two halves are NOT guaranteed to ship together, so do not read this as a
304
+ * test for "request streams" in general. Safari is the counterexample: it has
305
+ * had `Request.body` since 11.1 but only accepts a stream as `init.body` from
306
+ * Technology Preview 250, so a shipping Safari has the reader half without the
307
+ * upload half and this returns `true` there. That looks benign — WebKit appears
308
+ * to store the stream on the `Request` rather than stringify it, and its error
309
+ * comes from `fetch()`, which this package never calls on the objects it builds
310
+ * — but it is untested, and it is the case to look at first if a Safari report
311
+ * arrives. The sharper probe, if one is ever needed, is whether
312
+ * `new Request(url, {method:"POST", body:new ReadableStream(), duplex:"half"})`
313
+ * has a `content-type` of `text/plain;charset=UTF-8` (stringified) or `null`
314
+ * (stored); it is not used here because it costs a `Request` and a
315
+ * `ReadableStream` per call and agrees with the descriptor check on every
316
+ * runtime measured.
317
+ *
318
+ * A capability check, never a user-agent test: the question is what this
319
+ * runtime does, and the answer flips on its own the day Firefox ships request
320
+ * streams. Evaluated per call rather than cached at module load so that a test
321
+ * can install a `Request` without the capability and exercise the real branch
322
+ * under Node.
323
+ */
324
+ function supportsRequestStreams() {
325
+ return typeof Request === "function" && Object.getOwnPropertyDescriptor(Request.prototype, "body") != null;
326
+ }
327
+ /**
328
+ * Drain a body iterable into one contiguous buffer. Only the fallbacks need it.
329
+ * Allocates its own buffer rather than reusing `bytes.ts`'s `concatChunks`,
330
+ * which passes a single chunk straight through: that chunk is a view onto the
331
+ * decoder's own read buffer, and the result here is handed to `new Request` and
332
+ * outlives the decode. The allocation is also what makes it a
333
+ * `Uint8Array<ArrayBuffer>`, which `BodyInit` accepts and the looser
334
+ * `Uint8Array<ArrayBufferLike>` does not.
335
+ */
336
+ async function collectBytes(source) {
337
+ const parts = [];
338
+ let total = 0;
339
+ for await (const chunk of source) {
340
+ if (chunk.byteLength === 0) continue;
341
+ parts.push(chunk);
342
+ total += chunk.byteLength;
343
+ }
344
+ const out = new Uint8Array(total);
345
+ let offset = 0;
346
+ for (const part of parts) {
347
+ out.set(part, offset);
348
+ offset += part.byteLength;
349
+ }
350
+ return out;
351
+ }
352
+ //#endregion
353
+ //#region ../webrun-http-streams/src/http-stubs.ts
354
+ const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([
355
+ 101,
356
+ 103,
357
+ 204,
358
+ 205,
359
+ 304
360
+ ]);
361
+ const REQUEST_FIELDS = [
362
+ "url",
363
+ "method",
364
+ "mode",
365
+ "credentials",
366
+ "cache",
367
+ "redirect",
368
+ "referrer",
369
+ "referrerPolicy",
370
+ "integrity",
371
+ "keepalive"
372
+ ];
373
+ /** `SerializedHttpEnvelope.content` is never absent, only empty. */
374
+ async function* noBytes() {}
375
+ async function* oneChunk(chunk) {
376
+ yield chunk;
377
+ }
378
+ /**
379
+ * Returns an HTTP handler that serializes a Request, hands the envelope to
380
+ * `send` for transport, and deserializes the reply into a Response. Used on
381
+ * the caller side.
382
+ */
383
+ function newHttpClientStub(send) {
384
+ return async (requestOrPromise) => {
385
+ const request = await requestOrPromise;
386
+ const headers = [...request.headers].map(([k, v]) => [k, v]);
387
+ const options = {
388
+ url: request.url,
389
+ headers
390
+ };
391
+ for (const field of REQUEST_FIELDS) {
392
+ const val = request[field];
393
+ if (val !== void 0 && field !== "url") options[field] = val;
394
+ }
395
+ let content;
396
+ if (request.body != null) content = fromReadableStream(request.body);
397
+ else if (!supportsRequestStreams()) content = oneChunk(new Uint8Array(await request.arrayBuffer()));
398
+ else content = noBytes();
399
+ const result = await send({
400
+ options,
401
+ content
402
+ });
403
+ if (!result) return new Response(null, {
404
+ status: 404,
405
+ statusText: "Error 404: Not Found"
406
+ });
407
+ const responseOptions = result.options;
408
+ const method = options.method;
409
+ if (method === "HEAD" || method === "OPTIONS" || NULL_BODY_STATUSES.has(responseOptions.status)) {
410
+ await discard(result.content);
411
+ return new Response(null, responseOptions);
412
+ }
413
+ return new Response(toReadableStream(result.content[Symbol.asyncIterator]()), responseOptions);
414
+ };
415
+ }
416
+ /**
417
+ * Returns a server-side transport handler. It deserializes the incoming
418
+ * request envelope, delegates to `handler`, and serializes the response.
419
+ */
420
+ function newHttpServerStub(handler) {
421
+ return async (envelopeOrPromise) => {
422
+ const { options, content } = await envelopeOrPromise;
423
+ const { url, method, headers = [], ...rest } = options;
424
+ const { mode: _mode, ...forwardable } = rest;
425
+ const requestHeaders = new Headers();
426
+ for (const [key, value] of headers) requestHeaders.append(key, value);
427
+ const hasBody = method !== "GET" && method !== "HEAD" && method !== "OPTIONS";
428
+ const requestInit = {
429
+ ...forwardable,
430
+ method,
431
+ headers: requestHeaders
432
+ };
433
+ if (!hasBody) await discard(content);
434
+ else if (supportsRequestStreams()) {
435
+ requestInit.body = toReadableStream(content[Symbol.asyncIterator]());
436
+ requestInit.duplex = "half";
437
+ } else requestInit.body = await collectBytes(content);
438
+ const response = await handler(new Request(url, requestInit));
439
+ return {
440
+ options: {
441
+ status: response.status,
442
+ statusText: response.statusText,
443
+ headers: Object.fromEntries([...response.headers])
444
+ },
445
+ content: response.body ? fromReadableStream(response.body) : (async function* () {})()
446
+ };
447
+ };
448
+ }
449
+ //#endregion
450
+ //#region src/core/data-channels.ts
451
+ const MESSAGE_TYPE_REQUEST = "REQUEST";
452
+ const MESSAGE_TYPE_RESPONSE = "RESPONSE";
453
+ let __invocationCounter = 0;
454
+ function newInvokationChannel({ port, handler = () => {
455
+ throw new Error("Handler not implemented");
456
+ }, onError = console.error, newCallId = () => ++__invocationCounter }) {
457
+ const requests = {};
458
+ const listener = async (event) => {
459
+ const data = event.data ?? {};
460
+ if (data.type === MESSAGE_TYPE_REQUEST) try {
461
+ const result = await handler(data.request, ...event.ports);
462
+ const [response, ...transfers] = Array.isArray(result) ? result : result !== void 0 ? [result] : [];
463
+ port.postMessage({
464
+ type: MESSAGE_TYPE_RESPONSE,
465
+ callId: data.callId,
466
+ response
467
+ }, transfers);
468
+ } catch (error) {
469
+ port.postMessage({
470
+ type: MESSAGE_TYPE_RESPONSE,
471
+ callId: data.callId,
472
+ error: serializeError(error)
473
+ });
474
+ }
475
+ else if (data.type === MESSAGE_TYPE_RESPONSE) {
476
+ const pending = requests[data.callId];
477
+ delete requests[data.callId];
478
+ if (!pending) return;
479
+ if (data.error) pending.reject(deserializeError(data.error));
480
+ else pending.resolve(data.response);
481
+ }
482
+ };
483
+ const start = async () => {
484
+ try {
485
+ port.addEventListener("message", listener);
486
+ await port.start?.();
487
+ } catch (e) {
488
+ onError(e);
489
+ }
490
+ };
491
+ const close = async () => {
492
+ try {
493
+ port.removeEventListener("message", listener);
494
+ await port.close?.();
495
+ } catch (e) {
496
+ onError(e);
497
+ }
498
+ };
499
+ const invoke = (request = {}, ...transfers) => {
500
+ const callId = newCallId();
501
+ return new Promise((resolve, reject) => {
502
+ try {
503
+ requests[callId] = {
504
+ resolve,
505
+ reject
506
+ };
507
+ port.postMessage({
508
+ type: MESSAGE_TYPE_REQUEST,
509
+ callId,
510
+ request
511
+ }, transfers);
512
+ } catch (error) {
513
+ delete requests[callId];
514
+ reject(error);
515
+ }
516
+ });
517
+ };
518
+ return {
519
+ start,
520
+ close,
521
+ invoke
522
+ };
523
+ }
524
+ async function* sendStream(communicationPort, input, params = {}) {
525
+ const messageChannel = new MessageChannel();
526
+ communicationPort.postMessage({
527
+ type: "START_CALL",
528
+ params
529
+ }, [messageChannel.port2]);
530
+ const channel = newStreamChannel(messageChannel.port1);
531
+ try {
532
+ await channel.start();
533
+ channel.sendAll(input);
534
+ yield* channel.recieveAll();
535
+ } finally {
536
+ await channel.close();
537
+ }
538
+ }
539
+ function handleStreams(communicationPort, handler) {
540
+ const listener = async (event) => {
541
+ const { type, params } = event.data ?? {};
542
+ if (type !== "START_CALL") return;
543
+ const port = event.ports[0];
544
+ const channel = newStreamChannel(port);
545
+ try {
546
+ await channel.start();
547
+ const response = await handler(channel.recieveAll(), params ?? {});
548
+ await channel.sendAll(response);
549
+ } finally {
550
+ await channel.close();
551
+ }
552
+ };
553
+ communicationPort.addEventListener("message", listener);
554
+ communicationPort.start?.();
555
+ return () => communicationPort.removeEventListener("message", listener);
556
+ }
557
+ function newStreamChannel(port) {
558
+ let listeners = [];
559
+ let iterators = [];
560
+ const notifyAll = async (data) => {
561
+ for (const listener of listeners) await listener(data);
562
+ };
563
+ const channel = newInvokationChannel({
564
+ port,
565
+ handler: (data) => notifyAll(data)
566
+ });
567
+ const start = () => channel.start();
568
+ const close = async () => {
569
+ await notifyAll({ done: true });
570
+ for (const it of [...iterators]) await it.return?.();
571
+ await channel.close();
572
+ };
573
+ async function* recieveAll() {
574
+ yield* recieveIterator((deliver) => {
575
+ listeners.push(deliver);
576
+ return () => {
577
+ listeners = listeners.filter((l) => l !== deliver);
578
+ };
579
+ });
580
+ }
581
+ async function sendAll(it) {
582
+ await sendIterator((chunk) => void channel.invoke(chunk), (async function* () {
583
+ try {
584
+ iterators.push(it);
585
+ yield* it;
586
+ } finally {
587
+ iterators = iterators.filter((i) => i !== it);
588
+ }
589
+ })());
590
+ }
591
+ return {
592
+ start,
593
+ close,
594
+ recieveAll,
595
+ sendAll
596
+ };
597
+ }
598
+ //#endregion
599
+ //#region src/http/http-send-recieve.ts
600
+ async function* httpToIterator(envelopeOrPromise) {
601
+ const { options, content } = await envelopeOrPromise;
602
+ yield new TextEncoder().encode(JSON.stringify(options));
603
+ yield* content;
604
+ }
605
+ async function httpFromIterator(iterable) {
606
+ const it = (await iterable)[Symbol.asyncIterator]();
607
+ const { done, value } = await it.next();
608
+ let options = {};
609
+ if (!done && value) {
610
+ const str = new TextDecoder().decode(value);
611
+ options = JSON.parse(str);
612
+ }
613
+ return {
614
+ options,
615
+ content: { [Symbol.asyncIterator]() {
616
+ return it;
617
+ } }
618
+ };
619
+ }
620
+ /**
621
+ * @deprecated For new code, prefer the `MessagePort`-based stack from
622
+ * `@statewalker/webrun-http-port`. Once a `MessagePort` is established between
623
+ * page and worker, `httpServe(port, handler)` provides equivalent semantics
624
+ * with `callBidi` multiplexing, full-duplex streaming, and `AbortSignal`.
625
+ * This helper remains for existing ServiceWorker setups that still consume the
626
+ * `MessageTarget` surface; it will be reimplemented on top of
627
+ * `webrun-http-port` in a follow-up release.
628
+ */
629
+ function handleHttpRequests(communicationPort, handler) {
630
+ const serverStub = newHttpServerStub(handler);
631
+ return handleStreams(communicationPort, async (it) => {
632
+ const envelope = await httpFromIterator(it);
633
+ return httpToIterator(await serverStub(envelope));
634
+ });
635
+ }
636
+ /**
637
+ * @deprecated For new code, prefer the `MessagePort`-based stack from
638
+ * `@statewalker/webrun-http-port/fetch`. Once the page and SW share a
639
+ * `MessagePort`, `fetchOverPort(port, request)` provides the same
640
+ * `Request → Response` semantics with multiplexing via `callBidi`, JSONL
641
+ * envelope framing, and native `AbortSignal` support. This helper remains for
642
+ * existing ServiceWorker setups; it will be reimplemented on top of
643
+ * `webrun-http-port` in a follow-up release.
644
+ */
645
+ async function sendHttpRequest(communicationPort, request) {
646
+ return await newHttpClientStub(async (req) => {
647
+ return await httpFromIterator(sendStream(communicationPort, httpToIterator(req)));
648
+ })(request);
649
+ }
650
+ //#endregion
651
+ //#region ../../node_modules/.pnpm/idb-keyval@6.3.0/node_modules/idb-keyval/dist/index.js
652
+ function promisifyRequest(request) {
653
+ return new Promise((resolve, reject) => {
654
+ request.oncomplete = request.onsuccess = () => resolve(request.result);
655
+ request.onabort = request.onerror = () => reject(request.error);
656
+ });
657
+ }
658
+ function createStore(dbName, storeName) {
659
+ let dbp;
660
+ const getDB = () => {
661
+ if (dbp) return dbp;
662
+ const request = indexedDB.open(dbName);
663
+ request.onupgradeneeded = () => request.result.createObjectStore(storeName);
664
+ dbp = promisifyRequest(request);
665
+ dbp.then((db) => {
666
+ db.onclose = () => dbp = void 0;
667
+ }, () => {
668
+ dbp = void 0;
669
+ });
670
+ return dbp;
671
+ };
672
+ return (txMode, callback) => getDB().then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName)));
673
+ }
674
+ let defaultGetStoreFunc;
675
+ function defaultGetStore() {
676
+ if (!defaultGetStoreFunc) defaultGetStoreFunc = createStore("keyval-store", "keyval");
677
+ return defaultGetStoreFunc;
678
+ }
679
+ /**
680
+ * Get a value by its key.
681
+ *
682
+ * @param key
683
+ * @param customStore Method to get a custom store. Use with caution (see the docs).
684
+ */
685
+ function get(key, customStore = defaultGetStore()) {
686
+ return customStore("readonly", (store) => promisifyRequest(store.get(key)));
687
+ }
688
+ /**
689
+ * Set a value with a key.
690
+ *
691
+ * @param key
692
+ * @param value
693
+ * @param customStore Method to get a custom store. Use with caution (see the docs).
694
+ */
695
+ function set(key, value, customStore = defaultGetStore()) {
696
+ return customStore("readwrite", (store) => {
697
+ store.put(value, key);
698
+ return promisifyRequest(store.transaction);
699
+ });
700
+ }
701
+ //#endregion
702
+ //#region src/core/data-calls.ts
703
+ function callChannel(target, callType, params, ...transfers) {
704
+ return new Promise((resolve, reject) => {
705
+ const channel = new MessageChannel();
706
+ channel.port1.onmessage = (ev) => {
707
+ const { result, error } = ev.data;
708
+ if (error) reject(deserializeError(error));
709
+ else resolve(result);
710
+ };
711
+ target.postMessage({
712
+ type: callType,
713
+ params
714
+ }, [channel.port2, ...transfers]);
715
+ });
716
+ }
717
+ function handleChannelCalls(target, callType, handler) {
718
+ const listener = async (event) => {
719
+ if (!event.data || event.data.type !== callType) return;
720
+ const [port, ...transfers] = event.ports ?? [];
721
+ const response = {};
722
+ try {
723
+ response.result = await handler(event, event.data.params, ...transfers);
724
+ } catch (error) {
725
+ response.error = serializeError(error);
726
+ }
727
+ port?.postMessage(response);
728
+ };
729
+ target.addEventListener("message", listener);
730
+ target.start?.();
731
+ return () => target.removeEventListener("message", listener);
732
+ }
733
+ //#endregion
734
+ //#region src/core/registry.ts
735
+ function newRegistry(onError = console.error) {
736
+ let counter = 0;
737
+ const registrations = {};
738
+ const register = (action) => {
739
+ const id = counter++;
740
+ const fn = Object.assign((skip) => {
741
+ try {
742
+ delete registrations[id];
743
+ return skip ? void 0 : action();
744
+ } catch (error) {
745
+ onError(error);
746
+ }
747
+ }, { action });
748
+ registrations[id] = fn;
749
+ return fn;
750
+ };
751
+ const unregister = (action) => {
752
+ for (const r of Object.values(registrations)) if (r.action === action) r(true);
753
+ };
754
+ const clear = () => {
755
+ for (const r of Object.values(registrations)) r();
756
+ };
757
+ return Object.assign([
758
+ register,
759
+ clear,
760
+ unregister
761
+ ], {
762
+ register,
763
+ clear,
764
+ unregister
765
+ });
766
+ }
767
+ //#endregion
768
+ //#region src/sw/sw-dispatcher.ts
769
+ /**
770
+ * Page-side counterpart: registers a ServiceWorker and sets up a
771
+ * MessageChannel with it, re-establishing the connection after SW wake-ups.
772
+ */
773
+ var SwPortHandler = class {
774
+ options;
775
+ _serviceWorkerUrl;
776
+ _serviceWorker;
777
+ _registrationPromise;
778
+ _cleanupRegistrations;
779
+ constructor(options) {
780
+ this.options = options;
781
+ if (!this.key) throw new Error("Key is not defined.");
782
+ if (!this.scope) throw new Error("Scope is not defined");
783
+ }
784
+ get key() {
785
+ return this.options.key;
786
+ }
787
+ get scope() {
788
+ return this.options.scope ?? new URL("./", this.serviceWorkerUrl).pathname;
789
+ }
790
+ get rootUrl() {
791
+ return new URL(this.scope, import.meta.url);
792
+ }
793
+ get serviceWorkerUrl() {
794
+ if (!this._serviceWorkerUrl) {
795
+ const url = this.options.serviceWorkerUrl ? new URL(this.options.serviceWorkerUrl) : new URL("./index-sw.js", this.rootUrl);
796
+ this._serviceWorkerUrl = `${url}`;
797
+ }
798
+ return this._serviceWorkerUrl;
799
+ }
800
+ async _newCommunicationChannel() {
801
+ const messageChannel = new MessageChannel();
802
+ return [messageChannel.port1, messageChannel.port2];
803
+ }
804
+ async _setCommunicationPort(port) {
805
+ await this.options.bindPort(port);
806
+ }
807
+ async _updateCommunicationChannel() {
808
+ if (!this._serviceWorker) return;
809
+ const [port1, port2] = await this._newCommunicationChannel();
810
+ await this._setCommunicationPort(port1);
811
+ await callChannel(this._serviceWorker, "UPDATE_COMMUNICATION_PORT", this._getRegistrationInfo(), port2);
812
+ }
813
+ _getRegistrationInfo() {
814
+ return { key: this.key };
815
+ }
816
+ async start() {
817
+ if (!this._registrationPromise) this._registrationPromise = (async () => {
818
+ const [register, cleanup] = newRegistry();
819
+ this._cleanupRegistrations = cleanup;
820
+ register(() => {
821
+ this._cleanupRegistrations = void 0;
822
+ });
823
+ const registration = await navigator.serviceWorker.register(this.serviceWorkerUrl, { scope: this.scope });
824
+ register(() => registration.unregister());
825
+ register(handleChannelCalls(navigator.serviceWorker, "UPDATE_COMMUNICATION_PORT", async (_event, _params, port) => {
826
+ await this._setCommunicationPort(port);
827
+ return this._getRegistrationInfo();
828
+ }));
829
+ this._serviceWorker = await getServiceWorkerController();
830
+ await awaitServiceWorkerActivation(this._serviceWorker);
831
+ await this._updateCommunicationChannel();
832
+ })();
833
+ return this._registrationPromise;
834
+ }
835
+ async stop() {
836
+ this._cleanupRegistrations?.();
837
+ const registrations = await navigator.serviceWorker.getRegistrations();
838
+ for (const registration of registrations) try {
839
+ await registration.unregister();
840
+ } catch (error) {
841
+ console.log("Service Worker registration failed: ", error);
842
+ }
843
+ }
844
+ };
845
+ function getServiceWorkerController() {
846
+ return new Promise((resolve) => {
847
+ const container = navigator.serviceWorker;
848
+ if (container.controller) {
849
+ resolve(container.controller);
850
+ return;
851
+ }
852
+ const onChange = () => {
853
+ if (!container.controller) return;
854
+ resolve(container.controller);
855
+ container.removeEventListener("controllerchange", onChange);
856
+ };
857
+ container.addEventListener("controllerchange", onChange);
858
+ });
859
+ }
860
+ function awaitServiceWorkerActivation(worker) {
861
+ return new Promise((resolve) => {
862
+ if (worker.state === "activated") {
863
+ resolve();
864
+ return;
865
+ }
866
+ const onStateChange = () => {
867
+ if (worker.state !== "activated") return;
868
+ worker.removeEventListener("statechange", onStateChange);
869
+ resolve();
870
+ };
871
+ worker.addEventListener("statechange", onStateChange);
872
+ });
873
+ }
874
+ /**
875
+ * ServiceWorker-side counterpart: maintains an index of connected clients keyed by
876
+ * clientId, persisted in IndexedDB so it can reclaim ports across SW restarts.
877
+ */
878
+ var SwPortDispatcher = class {
879
+ self;
880
+ log;
881
+ handlersIndex = /* @__PURE__ */ new Map();
882
+ /**
883
+ * Set of keys that were registered at some point on this SW (sticky —
884
+ * survives client closures + SW wake-ups via IndexedDB). Lets the fetch
885
+ * handler distinguish "site URL whose owner is gone → 404" from
886
+ * "unknown URL under scope → pass through to the network".
887
+ */
888
+ claimedKeys = /* @__PURE__ */ new Set();
889
+ _cleanup;
890
+ _activationPromise;
891
+ constructor({ self, log = () => {} }) {
892
+ this.self = self;
893
+ this.log = log;
894
+ }
895
+ get scope() {
896
+ return `${new URL("./", this.self.location.href)}`;
897
+ }
898
+ /**
899
+ * Look up the active channel for a key. Returns `undefined` when there
900
+ * is no registration (or the registering client has gone away — stale
901
+ * entries are pruned in place).
902
+ */
903
+ async loadChannelInfo(key) {
904
+ await this._checkActivation();
905
+ for (const channelInfo of this.handlersIndex.values()) {
906
+ if (channelInfo.key !== key) continue;
907
+ const clientId = channelInfo.clientId;
908
+ if (!clientId) continue;
909
+ if (!await this.self.clients.get(clientId)) {
910
+ this.handlersIndex.delete(clientId);
911
+ continue;
912
+ }
913
+ return channelInfo;
914
+ }
915
+ }
916
+ /** Whether `key` has ever been registered on this SW. */
917
+ isClaimedKey(key) {
918
+ return this.claimedKeys.has(key);
919
+ }
920
+ start() {
921
+ this._cleanup = handleChannelCalls(this.self, "UPDATE_COMMUNICATION_PORT", async (event, channelInfo, port) => {
922
+ const clientId = event.source?.id;
923
+ this.log("[UPDATE_COMMUNICATION_PORT]", clientId, channelInfo);
924
+ await this._updateChannelInfo({
925
+ ...channelInfo,
926
+ port,
927
+ clientId
928
+ });
929
+ return { ...channelInfo };
930
+ });
931
+ this.self.addEventListener("install", (event) => {
932
+ this.log("Skip waiting on install.", event);
933
+ this.self.skipWaiting();
934
+ });
935
+ this.self.addEventListener("activate", (event) => {
936
+ this.log("Claim control over all clients.", event);
937
+ event.waitUntil((async () => {
938
+ await this.self.clients.claim();
939
+ await this._checkActivation();
940
+ })());
941
+ });
942
+ }
943
+ async stop() {
944
+ if (this._cleanup) {
945
+ this._cleanup();
946
+ this._cleanup = void 0;
947
+ }
948
+ }
949
+ _checkActivation() {
950
+ if (!this._activationPromise) this._activationPromise = this._activate();
951
+ return this._activationPromise;
952
+ }
953
+ async _activate() {
954
+ this.log("[checkActivation]");
955
+ const clientsIndex = await this._loadClientIds();
956
+ for (const [clientId, client] of clientsIndex.entries()) {
957
+ const messageChannel = new MessageChannel();
958
+ const port = messageChannel.port1;
959
+ const channelInfo = await callChannel(client, "UPDATE_COMMUNICATION_PORT", {}, messageChannel.port2);
960
+ await this._updateChannelInfo({
961
+ ...channelInfo,
962
+ port,
963
+ clientId
964
+ });
965
+ }
966
+ }
967
+ async _updateChannelInfo(channelInfo) {
968
+ this.log("[updateClientInfo]", channelInfo);
969
+ if (!channelInfo.clientId) return;
970
+ this.handlersIndex.set(channelInfo.clientId, channelInfo);
971
+ if (channelInfo.key && !this.claimedKeys.has(channelInfo.key)) {
972
+ this.claimedKeys.add(channelInfo.key);
973
+ await set("claimedKeys", [...this.claimedKeys].sort());
974
+ }
975
+ await this._updateClientIds([]);
976
+ }
977
+ async _loadClientIds() {
978
+ const storedKeys = await get("claimedKeys") ?? [];
979
+ for (const key of storedKeys) this.claimedKeys.add(key);
980
+ const stored = await get("clientIds") ?? [];
981
+ return await this._updateClientIds(new Set(stored));
982
+ }
983
+ async _updateClientIds(clientIds) {
984
+ const ids = new Set(clientIds);
985
+ for (const clientId of this.handlersIndex.keys()) ids.add(clientId);
986
+ const index = /* @__PURE__ */ new Map();
987
+ for (const clientId of ids) {
988
+ const client = await this.self.clients.get(clientId);
989
+ if (client) index.set(clientId, client);
990
+ else this.handlersIndex.delete(clientId);
991
+ }
992
+ await set("clientIds", [...index.keys()].sort());
993
+ return index;
994
+ }
995
+ };
996
+ //#endregion
997
+ //#region src/sw/http-sw-dispatcher.ts
998
+ var SwHttpAdapter = class extends SwPortHandler {
999
+ _handlers = /* @__PURE__ */ new Map();
1000
+ _cleanupRequestChannel;
1001
+ constructor(options) {
1002
+ super({
1003
+ bindPort: () => {},
1004
+ ...options
1005
+ });
1006
+ }
1007
+ async _setCommunicationPort(port) {
1008
+ this._cleanupRequestChannel?.();
1009
+ this._cleanupRequestChannel = handleHttpRequests(port, this._handleHttpRequest.bind(this));
1010
+ }
1011
+ async _handleHttpRequest(request) {
1012
+ const requestUrl = request.url;
1013
+ for (const [urlPrefix, handler] of this._handlers) if (requestUrl.indexOf(urlPrefix) === 0) return handler(request);
1014
+ return new Response(null, {
1015
+ status: 404,
1016
+ statusText: "Error 404: Not found"
1017
+ });
1018
+ }
1019
+ /**
1020
+ * Registers an HTTP handler on a prefix. Returns the resulting base URL and a
1021
+ * disposer.
1022
+ */
1023
+ async register(prefix, handler) {
1024
+ const cleanPrefix = `./${(prefix ?? "").replace(/^[./]+/, "")}`;
1025
+ const baseUrl = `${new URL(cleanPrefix, this.rootUrl)}`;
1026
+ this._handlers.set(baseUrl, handler);
1027
+ const handlers = this._handlers;
1028
+ return {
1029
+ baseUrl,
1030
+ prefix: cleanPrefix,
1031
+ async remove() {
1032
+ handlers.delete(baseUrl);
1033
+ }
1034
+ };
1035
+ }
1036
+ };
1037
+ var SwHttpDispatcher = class extends SwPortDispatcher {
1038
+ start() {
1039
+ super.start();
1040
+ this.self.addEventListener("fetch", this._handleFetchEvent.bind(this));
1041
+ }
1042
+ _handleFetchEvent(event) {
1043
+ event.respondWith((async () => {
1044
+ const request = event.request;
1045
+ try {
1046
+ const requestUrl = request.url;
1047
+ const rootUrl = this.scope;
1048
+ if (requestUrl.indexOf(rootUrl) === 0) {
1049
+ const key = requestUrl.substring(rootUrl.length).replace(/^\/?([^/]+).*$/, "$1");
1050
+ const channelInfo = await this.loadChannelInfo(key);
1051
+ if (channelInfo?.port) return await sendHttpRequest(channelInfo.port, request);
1052
+ if (this.isClaimedKey(key)) return new Response(null, {
1053
+ status: 404,
1054
+ statusText: "Not Found (no active handler)"
1055
+ });
1056
+ }
1057
+ return await fetch(request);
1058
+ } catch (error) {
1059
+ console.error(error);
1060
+ return new Response(null, {
1061
+ status: 500,
1062
+ statusText: "Error 500: Internal error"
1063
+ });
1064
+ }
1065
+ })());
1066
+ }
1067
+ };
1068
+ function startHttpDispatcher(options) {
1069
+ const dispatcher = new SwHttpDispatcher(options);
1070
+ dispatcher.start();
1071
+ return () => {
1072
+ dispatcher.stop();
1073
+ };
1074
+ }
1075
+ //#endregion
1076
+ export { SwHttpAdapter, SwHttpDispatcher, SwPortDispatcher, SwPortHandler, startHttpDispatcher };