@statewalker/webrun-http-browser 0.10.0 → 0.12.1

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 (59) hide show
  1. package/README.md +155 -10
  2. package/dist/core/index.d.ts +1 -0
  3. package/dist/core/index.d.ts.map +1 -1
  4. package/dist/core/service-worker-control.d.ts +2 -1
  5. package/dist/core/service-worker-control.d.ts.map +1 -1
  6. package/dist/core/sw-scope.d.ts +31 -0
  7. package/dist/core/sw-scope.d.ts.map +1 -0
  8. package/dist/index.js +816 -211
  9. package/dist/link/http-link.d.ts +16 -3
  10. package/dist/link/http-link.d.ts.map +1 -1
  11. package/dist/relay/index-sw.d.ts +8 -2
  12. package/dist/relay/index-sw.d.ts.map +1 -1
  13. package/dist/relay/index.d.ts +1 -1
  14. package/dist/relay/index.d.ts.map +1 -1
  15. package/dist/relay-sw.js +433 -83
  16. package/dist/relay-worker.d.ts +1 -0
  17. package/dist/relay-worker.d.ts.map +1 -1
  18. package/dist/relay-worker.js +433 -83
  19. package/dist/sw/handler-endpoint.d.ts +5 -2
  20. package/dist/sw/handler-endpoint.d.ts.map +1 -1
  21. package/dist/sw/http-sw-dispatcher.d.ts +114 -7
  22. package/dist/sw/http-sw-dispatcher.d.ts.map +1 -1
  23. package/dist/sw/index.d.ts +1 -0
  24. package/dist/sw/index.d.ts.map +1 -1
  25. package/dist/sw/protocol.d.ts +3 -0
  26. package/dist/sw/protocol.d.ts.map +1 -1
  27. package/dist/sw/sw-dispatcher.d.ts +5 -2
  28. package/dist/sw/sw-dispatcher.d.ts.map +1 -1
  29. package/dist/sw/sw-http-adapter.d.ts +19 -4
  30. package/dist/sw/sw-http-adapter.d.ts.map +1 -1
  31. package/dist/sw/worker-service.d.ts +5 -2
  32. package/dist/sw/worker-service.d.ts.map +1 -1
  33. package/dist/sw-worker.js +596 -132
  34. package/dist/sw.js +811 -209
  35. package/dist/worker.js +604 -106
  36. package/dist/ws/server.d.ts +50 -2
  37. package/dist/ws/server.d.ts.map +1 -1
  38. package/dist/ws/socket-core.d.ts +5 -2
  39. package/dist/ws/socket-core.d.ts.map +1 -1
  40. package/dist/ws/upgrade.d.ts +12 -1
  41. package/dist/ws/upgrade.d.ts.map +1 -1
  42. package/package.json +3 -2
  43. package/src/core/index.ts +1 -0
  44. package/src/core/service-worker-control.ts +5 -3
  45. package/src/core/sw-scope.ts +27 -0
  46. package/src/link/http-link.ts +20 -4
  47. package/src/relay/index-sw.ts +15 -2
  48. package/src/relay/index.ts +2 -2
  49. package/src/relay-worker.ts +1 -0
  50. package/src/sw/handler-endpoint.ts +17 -4
  51. package/src/sw/http-sw-dispatcher.ts +267 -67
  52. package/src/sw/index.ts +5 -0
  53. package/src/sw/protocol.ts +11 -1
  54. package/src/sw/sw-dispatcher.ts +17 -11
  55. package/src/sw/sw-http-adapter.ts +36 -10
  56. package/src/sw/worker-service.ts +11 -4
  57. package/src/ws/server.ts +166 -11
  58. package/src/ws/socket-core.ts +7 -2
  59. package/src/ws/upgrade.ts +22 -4
package/dist/index.js CHANGED
@@ -1272,8 +1272,9 @@ async function awaitServiceWorkerControl(registration, { timeout = DEFAULT_SERVI
1272
1272
  * Returns a function that stops answering.
1273
1273
  */
1274
1274
  function handleClaimRequests(self) {
1275
- return handleChannelCalls(self, CLAIM_CALL, (event) => {
1276
- const claimed = self.clients.claim().then(() => true);
1275
+ const sw = self;
1276
+ return handleChannelCalls(sw, CLAIM_CALL, (event) => {
1277
+ const claimed = sw.clients.claim().then(() => true);
1277
1278
  event.waitUntil?.(claimed);
1278
1279
  return claimed;
1279
1280
  });
@@ -3161,6 +3162,99 @@ async function* throughAbort(input, signal) {
3161
3162
  }
3162
3163
  }
3163
3164
  /**
3165
+ * Runs `fn` on a later task, never a microtask: a producer looping on
3166
+ * `whenPortReady` must let the transport's own events run. `setImmediate`
3167
+ * where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
3168
+ * are clamped to 4 ms in browsers.
3169
+ */
3170
+ const setImmediateFn = globalThis.setImmediate;
3171
+ /** @internal */
3172
+ const nextTask = setImmediateFn ? (fn) => {
3173
+ setImmediateFn(fn);
3174
+ } : (() => {
3175
+ let channel;
3176
+ const queue = [];
3177
+ return (fn) => {
3178
+ if (!channel) {
3179
+ channel = new MessageChannel();
3180
+ channel.port1.onmessage = () => queue.shift()?.();
3181
+ }
3182
+ queue.push(fn);
3183
+ channel.port2.postMessage(0);
3184
+ };
3185
+ })();
3186
+ const states = /* @__PURE__ */ new WeakMap();
3187
+ /** @internal Registered by the multiplexer for each port with a window. */
3188
+ function setPortSendState(port, state) {
3189
+ states.set(port, state);
3190
+ }
3191
+ /**
3192
+ * Bytes queued on `port` because its window is spent (0 for a port without
3193
+ * flow control). A sender that should not queue without bound waits for
3194
+ * `whenPortReady` before posting more.
3195
+ */
3196
+ function portBufferedAmount(port) {
3197
+ return states.get(port)?.bufferedAmount() ?? 0;
3198
+ }
3199
+ /**
3200
+ * Resolves once at most `below` bytes are queued on `port`, always on a later
3201
+ * task (for a port without flow control, on the next one).
3202
+ */
3203
+ function whenPortReady(port, below = 0) {
3204
+ return states.get(port)?.ready(below) ?? new Promise((resolve) => nextTask(resolve));
3205
+ }
3206
+ const agreements = /* @__PURE__ */ new WeakMap();
3207
+ /** @internal Registered by the multiplexer for each port with a window. */
3208
+ function setPortAgreement(port, agreement) {
3209
+ agreements.set(port, agreement);
3210
+ }
3211
+ /**
3212
+ * @internal What has been agreed with the peer on `port` so far; `undefined`
3213
+ * on a port without a window. Not exported from the package.
3214
+ */
3215
+ function portAgreement(port) {
3216
+ return agreements.get(port)?.();
3217
+ }
3218
+ const ends = /* @__PURE__ */ new WeakMap();
3219
+ /** @internal Registered by the multiplexer for each port with a window. */
3220
+ function setPortEndState(port, state) {
3221
+ ends.set(port, state);
3222
+ }
3223
+ /**
3224
+ * @internal End this side's direction of `port`: an `end` is queued behind
3225
+ * everything already posted on it, and anything posted afterwards is dropped.
3226
+ * `false`, and nothing changes, when `end` was not agreed (no window, the
3227
+ * handshake not answered yet, or a peer that does not speak it): the caller
3228
+ * then closes the port. Never waits on the peer. Not exported from the package.
3229
+ */
3230
+ function endPort(port) {
3231
+ return ends.get(port)?.end() ?? false;
3232
+ }
3233
+ /**
3234
+ * @internal Resolves once the peer has ended its direction of `port`. It never
3235
+ * settles otherwise: not on a port where `end` is not agreed when it is asked
3236
+ * (no window, the handshake not answered yet, a peer or codec without it), and
3237
+ * not when the port closes first, so a caller races it against the port's
3238
+ * close signal. Not exported from the package.
3239
+ */
3240
+ function whenPortEnded(port) {
3241
+ let waiting = peerEnds.get(port);
3242
+ if (!waiting) {
3243
+ let resolve = () => {};
3244
+ const promise = new Promise((r) => {
3245
+ resolve = r;
3246
+ });
3247
+ if (!ends.get(port)?.watchPeerEnd(resolve)) return promise;
3248
+ waiting = {
3249
+ promise,
3250
+ resolve
3251
+ };
3252
+ peerEnds.set(port, waiting);
3253
+ }
3254
+ return waiting.promise;
3255
+ }
3256
+ const peerEnds = /* @__PURE__ */ new WeakMap();
3257
+ /**
3164
3258
  * Makes `target` disposable with `await using`: installs `dispose` as its
3165
3259
  * `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
3166
3260
  * iterators lose what a native generator gets from `AsyncIteratorPrototype`;
@@ -3232,6 +3326,61 @@ function returningAtOnce(gen, abandon) {
3232
3326
  * tests and adapters assert on it.
3233
3327
  */
3234
3328
  const STREAM_ABORT = "webrun-rpc:stream-abort";
3329
+ /**
3330
+ * The `type` of a pipelined chunk: a plain port message, not a `callPort`
3331
+ * request, sent only on a port where both ends agreed `pipeline`. The window's
3332
+ * credit is its confirmation.
3333
+ */
3334
+ const CHUNK = "webrun-rpc:chunk";
3335
+ /**
3336
+ * What a pipelined chunk counts beyond its bytes, against the window, on the
3337
+ * receiving side. Both codecs' windows count more for the chunk's envelope
3338
+ * (the estimate at least 64, msgpack's keys alone more than this), so an honest
3339
+ * sender never makes the receiver count past the window; a sender of empty
3340
+ * chunks still cannot have it hold an unbounded number of them.
3341
+ */
3342
+ const CHUNK_OVERHEAD = 32;
3343
+ /**
3344
+ * The bytes of `view` as a plain `Uint8Array` that owns its whole buffer:
3345
+ * `view` itself when it already is one, else a copy. Structured clone carries
3346
+ * and keeps a view's whole `ArrayBuffer`, and a byte codec may decode into
3347
+ * views of the frame, while the window and the receiver's count see only the
3348
+ * view: a 4 KiB `subarray` of a 4 MiB buffer would cost 4 MiB per chunk.
3349
+ * Copied with the `Uint8Array` constructor, not `slice()`: a Node `Buffer`'s
3350
+ * `slice()` is another view on the same memory.
3351
+ */
3352
+ function ownBytes(view) {
3353
+ if (view instanceof Uint8Array && view.constructor === Uint8Array && view.byteOffset === 0 && view.byteLength === view.buffer.byteLength) return view;
3354
+ return new Uint8Array(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
3355
+ }
3356
+ /**
3357
+ * A producer's chunk as bytes this end may send, in either mode: any view or
3358
+ * `ArrayBuffer`, normalised by `ownBytes`. Anything else fails the stream here,
3359
+ * as the producer's own error, rather than reaching a peer that would refuse it.
3360
+ */
3361
+ /**
3362
+ * A received confirmed chunk's value, as its own bytes, put back in place of
3363
+ * the original in `chunk`: `listenPort` keeps the request until the handler
3364
+ * returns, and the original would keep its whole buffer that long. A function
3365
+ * of its own, so that no temporary of the suspended handler holds the
3366
+ * original either.
3367
+ */
3368
+ function adoptValue(chunk) {
3369
+ const original = chunk.value;
3370
+ if (ArrayBuffer.isView(original)) chunk.value = ownBytes(original);
3371
+ return chunk.value;
3372
+ }
3373
+ function chunkBytes(value) {
3374
+ if (ArrayBuffer.isView(value)) return ownBytes(value);
3375
+ if (value instanceof ArrayBuffer || Object.prototype.toString.call(value) === "[object ArrayBuffer]") return new Uint8Array(value);
3376
+ throw new TypeError(`webrun-rpc: a stream chunk must be bytes (a typed array, a DataView or an ArrayBuffer), got ${value === null ? "null" : typeof value}`);
3377
+ }
3378
+ const MIXED_MESSAGE = "webrun-rpc: peer mixed confirmed and pipelined chunks; the stream port is closed";
3379
+ /** This end may pipeline its own chunks: agreed, and the window holds them. */
3380
+ const mayPipeline = (port) => {
3381
+ const agreement = portAgreement(port);
3382
+ return !!agreement?.enforced && agreement.caps.has("pipeline");
3383
+ };
3235
3384
  /** Caller's input travels on this channel; the handler listens on it. */
3236
3385
  const CHANNEL_IN = "in";
3237
3386
  /** The handler's output travels on this channel; the caller listens on it. */
@@ -3244,7 +3393,9 @@ const CHANNEL_OUT = "out";
3244
3393
  * same way on the other channel. Within each direction the next chunk is never
3245
3394
  * sent until the previous one has been delivered *and* pulled past by the
3246
3395
  * consumer (spec D11) — the reply to a chunk call *is* the confirmation, and
3247
- * `listenPort` withholds it until then.
3396
+ * `listenPort` withholds it until then. On a port whose window both ends
3397
+ * speak and that agreed `pipeline`, chunks go as plain messages instead, and
3398
+ * the window's credit is the confirmation: up to a window is in flight.
3248
3399
  *
3249
3400
  * A stream port carries exactly one invocation. To make several calls, open
3250
3401
  * several ports: `mux.openPort({ kind: "stream" })` per call.
@@ -3261,22 +3412,49 @@ function serveDuplexOverPort(port, handler, options = {}) {
3261
3412
  const notice = installAbortNotice(port, controller);
3262
3413
  abortOnPortClose(port, controller);
3263
3414
  const clock = installStreamTimeout(controller, options.timeout);
3264
- const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch);
3415
+ const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch, notice);
3265
3416
  let output;
3266
3417
  try {
3267
3418
  output = handler(inbound.stream);
3268
3419
  } catch (err) {
3269
- const pump = sendChunks(port, CHANNEL_OUT, failing(err), options, controller.signal, clock.touch);
3420
+ const pump = sendChunks(port, CHANNEL_OUT, failing(err), options, controller.signal, clock.touch, () => {});
3270
3421
  pump.catch(() => {});
3271
3422
  disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
3272
3423
  return teardownOnce(controller, notice, clock, inbound, void 0);
3273
3424
  }
3274
- const pump = sendChunks(port, CHANNEL_OUT, output, options, controller.signal, clock.touch);
3425
+ let onSent = () => {};
3426
+ const sent = new Promise((resolve) => {
3427
+ onSent = resolve;
3428
+ });
3429
+ const pump = sendChunks(port, CHANNEL_OUT, output, options, controller.signal, clock.touch, onSent);
3275
3430
  pump.catch(() => {});
3276
3431
  disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
3432
+ endServedStream(port, sent, inbound.over);
3277
3433
  return teardownOnce(controller, notice, clock, inbound, output);
3278
3434
  }
3279
3435
  /**
3436
+ * The serving side's normal finish. Once the handler's last chunk has gone
3437
+ * without an error, and no more input is coming (`over`: the caller's last
3438
+ * chunk arrived without an error and was read, or the caller ended its
3439
+ * direction behind it, for a handler that did not read all its input), this
3440
+ * side ends its direction too, where `end` was agreed. Without `end` nothing
3441
+ * happens here: the caller closes the port. A stream that never gets that far
3442
+ * was abandoned, and the caller's notice and `close` free the port; this then
3443
+ * never resumes. It needs no race against the port's close signal: nothing
3444
+ * awaits it, and what keeps it waiting is the input's watcher, a listener of
3445
+ * the port that a close drops, after which the wait is collected.
3446
+ *
3447
+ * Nothing this side still posts is lost behind its `end`. The caller ends only
3448
+ * once its own last chunk was confirmed, so no confirmation is owed to it
3449
+ * here, and what the freed port's close signal aborts has settled: the last
3450
+ * chunk has left, and input nobody reads is dropped, as on a close.
3451
+ */
3452
+ async function endServedStream(port, sent, over) {
3453
+ await sent;
3454
+ await over;
3455
+ endPort(port);
3456
+ }
3457
+ /**
3280
3458
  * A stream that completes normally has nobody left to call the returned
3281
3459
  * teardown — the caller only knows the stream ended, not that it must also
3282
3460
  * dispose the serve-side handle. Without this, the last `touch()`'s timer
@@ -3316,22 +3494,31 @@ function runCallerSide(port, input, options) {
3316
3494
  const notice = installAbortNotice(port, controller);
3317
3495
  abortOnPortClose(port, controller);
3318
3496
  const clock = installStreamTimeout(controller, options.timeout);
3319
- const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
3320
- const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
3497
+ const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch, notice);
3498
+ let sent = false;
3499
+ const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch, () => {
3500
+ sent = true;
3501
+ });
3321
3502
  pump.catch(() => {});
3322
3503
  return returningAtOnce((async function* () {
3504
+ let received = false;
3323
3505
  try {
3324
3506
  yield* inbound.stream;
3507
+ received = true;
3325
3508
  } finally {
3509
+ const ended = received && sent;
3326
3510
  if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
3327
- notice.post();
3511
+ if (!ended) notice.post();
3328
3512
  notice.stop();
3329
3513
  clock.stop();
3330
3514
  inbound.stop();
3331
3515
  await pump.catch(() => {});
3332
- try {
3333
- await port.close?.();
3334
- } catch {}
3516
+ if (!(ended && endPort(port))) {
3517
+ notice.post();
3518
+ try {
3519
+ await port.close?.();
3520
+ } catch {}
3521
+ }
3335
3522
  }
3336
3523
  })(), () => {
3337
3524
  if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
@@ -3425,7 +3612,7 @@ function installStreamTimeout(controller, timeout) {
3425
3612
  * the local consumer has not started iterating, an inbound chunk waits for it —
3426
3613
  * which is the correct backpressure, and different from having no listener.
3427
3614
  */
3428
- function receiveChunks(port, channelName, controller, touch) {
3615
+ function receiveChunks(port, channelName, controller, touch, notice) {
3429
3616
  let deliver;
3430
3617
  const waiting = [];
3431
3618
  let finished = false;
@@ -3435,25 +3622,64 @@ function receiveChunks(port, channelName, controller, touch) {
3435
3622
  const ended = new Promise((resolve) => {
3436
3623
  resolveEnded = resolve;
3437
3624
  });
3625
+ let resolveOver = () => {};
3626
+ const over = new Promise((resolve) => {
3627
+ resolveOver = resolve;
3628
+ });
3438
3629
  let aborted = false;
3439
3630
  let abortReason;
3440
3631
  const ready = () => deliver || finished ? Promise.resolve() : new Promise((r) => waiting.push(r));
3441
3632
  const wake = () => {
3442
3633
  for (const r of waiting.splice(0)) r();
3443
3634
  };
3444
- const off = listenPort(port, async ({ done, value, error }) => {
3635
+ const violate = (message) => {
3636
+ poison = new Error(message);
3637
+ controller.abort(poison);
3638
+ setTimeout(() => {
3639
+ try {
3640
+ port.close?.();
3641
+ } catch {}
3642
+ }, 0);
3643
+ return poison;
3644
+ };
3645
+ let lastChunkArrived = false;
3646
+ let watching = false;
3647
+ const watchPeerEnd = () => {
3648
+ if (watching) return true;
3649
+ if (!portAgreement(port)?.caps.has("end")) return false;
3650
+ watching = true;
3651
+ whenPortEnded(port).then(() => {
3652
+ port.removeEventListener("message", watcher);
3653
+ if (lastChunkArrived) {
3654
+ resolveOver();
3655
+ return;
3656
+ }
3657
+ notice.post();
3658
+ violate("webrun-rpc: the peer ended its direction without finishing it; the stream port is closed");
3659
+ });
3660
+ return true;
3661
+ };
3662
+ const watcher = (event) => {
3663
+ const data = event.data;
3664
+ if (data?.channelName !== channelName) return;
3665
+ const chunk = data.type === CHUNK ? data.chunk : data.type === "request" ? data.params : void 0;
3666
+ if (!chunk) return;
3667
+ if (!watchPeerEnd()) {
3668
+ port.removeEventListener("message", watcher);
3669
+ return;
3670
+ }
3671
+ if (chunk.done && !chunk.error) lastChunkArrived = true;
3672
+ };
3673
+ port.addEventListener("message", watcher);
3674
+ watchPeerEnd();
3675
+ let held = 0;
3676
+ const off = listenPort(port, async (params) => {
3677
+ const { done, error } = params;
3678
+ const value = adoptValue(params);
3445
3679
  touch();
3446
3680
  if (poison) throw poison;
3447
- if (outstanding) {
3448
- poison = /* @__PURE__ */ new Error("webrun-rpc: peer sent a second unconfirmed chunk; the stream port is closed");
3449
- controller.abort(poison);
3450
- setTimeout(() => {
3451
- try {
3452
- port.close?.();
3453
- } catch {}
3454
- }, 0);
3455
- throw poison;
3456
- }
3681
+ if (held > 0) throw violate(MIXED_MESSAGE);
3682
+ if (outstanding) throw violate("webrun-rpc: peer sent a second unconfirmed chunk; the stream port is closed");
3457
3683
  outstanding = true;
3458
3684
  try {
3459
3685
  await ready();
@@ -3463,15 +3689,60 @@ function receiveChunks(port, channelName, controller, touch) {
3463
3689
  value,
3464
3690
  error: error ? deserializeError(error) : void 0
3465
3691
  });
3692
+ if (done && !error) resolveOver();
3466
3693
  } finally {
3467
3694
  outstanding = false;
3468
3695
  }
3469
3696
  if (done) resolveEnded();
3470
3697
  }, { channelName });
3698
+ const onChunk = (event) => {
3699
+ const data = event.data;
3700
+ if (data?.type !== CHUNK || data.channelName !== channelName || !data.chunk) return;
3701
+ if (poison) return;
3702
+ const agreement = portAgreement(port);
3703
+ if (!agreement?.caps.has("pipeline")) {
3704
+ violate("webrun-rpc: peer sent a pipelined chunk where pipelining was not agreed");
3705
+ return;
3706
+ }
3707
+ if (outstanding) {
3708
+ violate(MIXED_MESSAGE);
3709
+ return;
3710
+ }
3711
+ if (data.chunk.value !== void 0 && !ArrayBuffer.isView(data.chunk.value)) {
3712
+ violate("webrun-rpc: peer sent a pipelined chunk whose value is not bytes");
3713
+ return;
3714
+ }
3715
+ if (held > agreement.window) {
3716
+ violate(`webrun-rpc: peer sent past the window (${agreement.window} bytes) without waiting for credit; the stream port is closed`);
3717
+ return;
3718
+ }
3719
+ touch();
3720
+ const { done, error } = data.chunk;
3721
+ const value = data.chunk.value && ownBytes(data.chunk.value);
3722
+ const size = (value?.byteLength ?? 0) + CHUNK_OVERHEAD;
3723
+ held += size;
3724
+ return (async () => {
3725
+ try {
3726
+ await ready();
3727
+ if (finished) return;
3728
+ await deliver?.({
3729
+ done,
3730
+ value,
3731
+ error: error ? deserializeError(error) : void 0
3732
+ });
3733
+ if (done && !error) resolveOver();
3734
+ if (done) resolveEnded();
3735
+ } finally {
3736
+ held -= size;
3737
+ }
3738
+ })();
3739
+ };
3740
+ port.addEventListener("message", onChunk);
3471
3741
  const detach = () => {
3472
3742
  finished = true;
3473
3743
  wake();
3474
3744
  off();
3745
+ port.removeEventListener("message", onChunk);
3475
3746
  controller.signal.removeEventListener("abort", onAbort);
3476
3747
  resolveEnded();
3477
3748
  };
@@ -3501,25 +3772,35 @@ function receiveChunks(port, channelName, controller, touch) {
3501
3772
  return detach;
3502
3773
  }),
3503
3774
  stop: detach,
3504
- ended
3775
+ ended,
3776
+ over
3505
3777
  };
3506
3778
  }
3507
3779
  /**
3508
3780
  * The sending half of one direction: one `callPort` per chunk, one call
3509
- * outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8).
3781
+ * outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8) —
3782
+ * or, once pipelining is agreed, plain chunk messages held back by the window.
3783
+ * `onSent` runs once the end of the output has left without an error: posted
3784
+ * when pipelined, confirmed when not.
3510
3785
  */
3511
- async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
3786
+ async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch, onSent) {
3512
3787
  const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
3513
3788
  const stream = throughAbort(framed, signal);
3514
3789
  const producer = output;
3515
3790
  const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
3516
3791
  if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
3792
+ let onAborted = () => {};
3793
+ const aborted = new Promise((resolve) => {
3794
+ onAborted = () => resolve();
3795
+ });
3796
+ if (signal.aborted) onAborted();
3797
+ else signal.addEventListener("abort", onAborted, { once: true });
3517
3798
  try {
3518
3799
  await sendIterator(async ({ done, value, error }) => {
3519
3800
  if (signal.aborted) return;
3520
3801
  const chunk = {
3521
3802
  done,
3522
- value,
3803
+ value: done ? value : chunkBytes(value),
3523
3804
  error: error === void 0 ? void 0 : serializeError(error)
3524
3805
  };
3525
3806
  log?.("[duplexOverPort] send", {
@@ -3527,48 +3808,34 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
3527
3808
  done,
3528
3809
  size: value?.byteLength
3529
3810
  });
3811
+ if (mayPipeline(port)) {
3812
+ port.postMessage({
3813
+ type: CHUNK,
3814
+ channelName,
3815
+ chunk
3816
+ });
3817
+ touch();
3818
+ if (done && !error) onSent();
3819
+ if (portBufferedAmount(port) > 0) await Promise.race([whenPortReady(port), aborted]);
3820
+ return;
3821
+ }
3530
3822
  await callPort(port, chunk, {
3531
3823
  channelName,
3532
3824
  timeout: NO_TIMEOUT,
3533
3825
  signal
3534
3826
  });
3535
3827
  touch();
3828
+ if (done && !error) onSent();
3536
3829
  }, stream);
3537
3830
  } catch (err) {
3538
3831
  if (signal.aborted) return;
3539
3832
  throw err;
3540
3833
  } finally {
3541
3834
  signal.removeEventListener("abort", returnProducer);
3835
+ signal.removeEventListener("abort", onAborted);
3542
3836
  }
3543
3837
  }
3544
3838
  /**
3545
- * Runs `fn` on a later task, never a microtask: a producer looping on
3546
- * `whenPortReady` must let the transport's own events run. `setImmediate`
3547
- * where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
3548
- * are clamped to 4 ms in browsers.
3549
- */
3550
- const setImmediateFn = globalThis.setImmediate;
3551
- /** @internal */
3552
- const nextTask = setImmediateFn ? (fn) => {
3553
- setImmediateFn(fn);
3554
- } : (() => {
3555
- let channel;
3556
- const queue = [];
3557
- return (fn) => {
3558
- if (!channel) {
3559
- channel = new MessageChannel();
3560
- channel.port1.onmessage = () => queue.shift()?.();
3561
- }
3562
- queue.push(fn);
3563
- channel.port2.postMessage(0);
3564
- };
3565
- })();
3566
- const states = /* @__PURE__ */ new WeakMap();
3567
- /** @internal Registered by the multiplexer for each port with a window. */
3568
- function setPortSendState(port, state) {
3569
- states.set(port, state);
3570
- }
3571
- /**
3572
3839
  * One virtual port.
3573
3840
  *
3574
3841
  * `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
@@ -3646,6 +3913,21 @@ function newVirtualPort(send, requestClose) {
3646
3913
  const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
3647
3914
  /** The longest delay a timer holds; past it `setInterval` fires at once. */
3648
3915
  const MAX_TIMER_MS = 2 ** 31 - 1;
3916
+ /**
3917
+ * What a windowed port offers in its `open`: every capability this end
3918
+ * speaks. `end` only over a codec that reads it (`PortCodec.halfClose`).
3919
+ */
3920
+ function ownCaps(codec) {
3921
+ return codec.halfClose ? ["pipeline", "end"] : ["pipeline"];
3922
+ }
3923
+ /**
3924
+ * The capabilities in a peer's `caps` that this end speaks too, or
3925
+ * `undefined` when the peer listed none (it is then treated as an older peer).
3926
+ */
3927
+ function sharedCaps(own, listed) {
3928
+ if (!Array.isArray(listed)) return void 0;
3929
+ return new Set(own.filter((cap) => listed.includes(cap)));
3930
+ }
3649
3931
  /** Ceiling on concurrently open virtual ports. Bounds the id table only. */
3650
3932
  const DEFAULT_MAX_PORTS = 1024;
3651
3933
  /**
@@ -3709,6 +3991,8 @@ function multiplexPort(port, options) {
3709
3991
  let muxClosed = false;
3710
3992
  /** This end counts encoded bytes and reassembles frames. */
3711
3993
  const exactCodec = codec.encode != null && codec.decode != null;
3994
+ /** What this end offers on a windowed port. */
3995
+ const caps = ownCaps(codec);
3712
3996
  const post = (envelope, transfer) => {
3713
3997
  if (muxClosed) return;
3714
3998
  codecPost(envelope, transfer);
@@ -3766,13 +4050,39 @@ function multiplexPort(port, options) {
3766
4050
  port.postMessage(bytes);
3767
4051
  } catch {}
3768
4052
  };
4053
+ /**
4054
+ * Both directions have ended: free the id as a close would, with no `close`
4055
+ * on the wire (each end frees it on its own once it has seen both ends).
4056
+ * Aborting the close signal is how the port's consumer learns it is gone.
4057
+ */
4058
+ const freeIfEnded = (id, c) => {
4059
+ const entry = open.get(id);
4060
+ if (!c.endSent || !c.peerEnded || entry?.credit !== c) return;
4061
+ freePartial(entry);
4062
+ open.delete(id);
4063
+ leaks?.unregister(entry.handle);
4064
+ entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port ended in both directions"));
4065
+ };
4066
+ /** A peer that broke the port's protocol: hold none of its traffic, and close. */
4067
+ const failPort = (id, entry, error, reason) => {
4068
+ freePartial(entry);
4069
+ dropOutgoing(id);
4070
+ open.delete(id);
4071
+ releaseWaiters(entry.credit);
4072
+ entry.handle.markClosed(error);
4073
+ post({
4074
+ type: "close",
4075
+ id,
4076
+ reason
4077
+ });
4078
+ };
3769
4079
  const releaseWaiters = (c) => {
3770
4080
  for (const w of c?.waiters.splice(0) ?? []) w.resolve();
3771
4081
  };
3772
4082
  const flush = (c) => {
3773
4083
  while (c.queue.length) {
3774
4084
  const next = c.queue[0];
3775
- if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
4085
+ if (next.size > 0 && c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
3776
4086
  c.queue.shift();
3777
4087
  c.queued -= next.size;
3778
4088
  c.outstanding += next.size;
@@ -3810,15 +4120,20 @@ function multiplexPort(port, options) {
3810
4120
  enforced: false,
3811
4121
  opener,
3812
4122
  exact,
4123
+ caps: /* @__PURE__ */ new Set(),
3813
4124
  outstanding: 0,
3814
4125
  queue: [],
3815
4126
  queued: 0,
3816
4127
  unconsumed: 0,
3817
4128
  waiters: [],
3818
4129
  toGrant: 0,
3819
- grantScheduled: false
4130
+ grantScheduled: false,
4131
+ ending: false,
4132
+ endSent: false,
4133
+ peerEnded: false
3820
4134
  } : void 0;
3821
4135
  const self = newVirtualPort((payload, transfer) => {
4136
+ if (c?.ending) return;
3822
4137
  touch(id);
3823
4138
  const envelope = {
3824
4139
  type: "message",
@@ -3905,6 +4220,38 @@ function multiplexPort(port, options) {
3905
4220
  });
3906
4221
  })
3907
4222
  });
4223
+ setPortEndState(self.port, {
4224
+ end: () => {
4225
+ if (isClosed() || !c.caps.has("end")) return false;
4226
+ if (c.ending) return true;
4227
+ c.ending = true;
4228
+ c.queue.push({
4229
+ size: 0,
4230
+ send: () => sendOnPort(id, () => {
4231
+ if (open.get(id)?.credit !== c) return;
4232
+ post({
4233
+ type: "end",
4234
+ id
4235
+ });
4236
+ c.endSent = true;
4237
+ freeIfEnded(id, c);
4238
+ })
4239
+ });
4240
+ flush(c);
4241
+ return true;
4242
+ },
4243
+ watchPeerEnd: (resolve) => {
4244
+ if (!c.caps.has("end")) return false;
4245
+ if (c.peerEnded) resolve();
4246
+ else c.onPeerEnd = new WeakRef(resolve);
4247
+ return true;
4248
+ }
4249
+ });
4250
+ setPortAgreement(self.port, () => ({
4251
+ window: c.window,
4252
+ caps: c.caps,
4253
+ enforced: c.enforced
4254
+ }));
3908
4255
  }
3909
4256
  const record = {
3910
4257
  deliver: self.deliver,
@@ -3956,16 +4303,24 @@ function multiplexPort(port, options) {
3956
4303
  }
3957
4304
  const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
3958
4305
  const credit = open.get(envelope.id)?.credit;
3959
- if (credit) post(credit.exact ? {
3960
- type: "credit",
3961
- id: envelope.id,
3962
- bytes: 0,
3963
- exact: true
3964
- } : {
3965
- type: "credit",
3966
- id: envelope.id,
3967
- bytes: 0
3968
- });
4306
+ const listed = sharedCaps(caps, envelope.caps);
4307
+ if (credit) {
4308
+ const ack = credit.exact ? {
4309
+ type: "credit",
4310
+ id: envelope.id,
4311
+ bytes: 0,
4312
+ exact: true
4313
+ } : {
4314
+ type: "credit",
4315
+ id: envelope.id,
4316
+ bytes: 0
4317
+ };
4318
+ if (listed) credit.caps = listed;
4319
+ post(listed ? {
4320
+ ...ack,
4321
+ caps: [...listed]
4322
+ } : ack);
4323
+ }
3969
4324
  let accepted = false;
3970
4325
  if (onPort) try {
3971
4326
  accepted = onPort(handle.port, envelope.meta) !== false;
@@ -3993,6 +4348,7 @@ function multiplexPort(port, options) {
3993
4348
  c.exact = false;
3994
4349
  c.outstanding = 0;
3995
4350
  }
4351
+ c.caps = sharedCaps(caps, envelope.caps) ?? /* @__PURE__ */ new Set();
3996
4352
  post({
3997
4353
  type: "credit",
3998
4354
  id: envelope.id,
@@ -4004,6 +4360,10 @@ function multiplexPort(port, options) {
4004
4360
  flush(c);
4005
4361
  return;
4006
4362
  }
4363
+ if ((envelope.type === "message" || envelope.type === "frame") && entry?.credit?.peerEnded) {
4364
+ failPort(envelope.id, entry, /* @__PURE__ */ new Error("webrun-rpc: the peer sent on a port after its end"), "protocol");
4365
+ return;
4366
+ }
4007
4367
  if (envelope.type === "message") {
4008
4368
  const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
4009
4369
  if (!entry) return;
@@ -4028,16 +4388,7 @@ function multiplexPort(port, options) {
4028
4388
  const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
4029
4389
  const overrun = partial.room != null && partial.bytes + size > partial.room ? `what the window left (${partial.room} of ${c.window} bytes, ${partial.owed} still owed)` : reassembling - partial.charged + charged > maxReassembledBytes ? `maxReassembledBytes (${maxReassembledBytes} bytes past the window, across all ports)` : void 0;
4030
4390
  if (overrun) {
4031
- freePartial(entry);
4032
- dropOutgoing(envelope.id);
4033
- open.delete(envelope.id);
4034
- releaseWaiters(c);
4035
- entry.handle.markClosed(/* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`));
4036
- post({
4037
- type: "close",
4038
- id: envelope.id,
4039
- reason: "max-reassembled-bytes"
4040
- });
4391
+ failPort(envelope.id, entry, /* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`), "max-reassembled-bytes");
4041
4392
  return;
4042
4393
  }
4043
4394
  partial.bytes += size;
@@ -4057,6 +4408,19 @@ function multiplexPort(port, options) {
4057
4408
  entry.handle.deliver(inner.payload, whole.byteLength);
4058
4409
  return;
4059
4410
  }
4411
+ if (envelope.type === "end") {
4412
+ if (!entry) return;
4413
+ const c = entry.credit;
4414
+ if (!c?.caps.has("end")) {
4415
+ failPort(envelope.id, entry, /* @__PURE__ */ new Error("webrun-rpc: the peer ended a port where end was not agreed"), "protocol");
4416
+ return;
4417
+ }
4418
+ touch(envelope.id);
4419
+ c.peerEnded = true;
4420
+ c.onPeerEnd?.deref()?.();
4421
+ freeIfEnded(envelope.id, c);
4422
+ return;
4423
+ }
4060
4424
  dropOutgoing(envelope.id);
4061
4425
  if (!entry) return;
4062
4426
  freePartial(entry);
@@ -4138,12 +4502,14 @@ function multiplexPort(port, options) {
4138
4502
  id,
4139
4503
  meta,
4140
4504
  window,
4141
- exact: true
4505
+ exact: true,
4506
+ caps: [...caps]
4142
4507
  } : {
4143
4508
  type: "open",
4144
4509
  id,
4145
4510
  meta,
4146
- window
4511
+ window,
4512
+ caps: [...caps]
4147
4513
  } : {
4148
4514
  type: "open",
4149
4515
  id,
@@ -4298,7 +4664,7 @@ function isEnvelope(value) {
4298
4664
  const candidate = value;
4299
4665
  if (typeof candidate.id !== "number") return false;
4300
4666
  if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
4301
- return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
4667
+ return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong" || candidate.type === "end";
4302
4668
  }
4303
4669
  /**
4304
4670
  * For ports whose messages are structured values — a real `MessagePort`, a
@@ -4310,6 +4676,7 @@ function isEnvelope(value) {
4310
4676
  * not send anything a byte codec could not also carry.
4311
4677
  */
4312
4678
  const structuredCodec = {
4679
+ halfClose: true,
4313
4680
  post(port, envelope, transfer) {
4314
4681
  if (transfer && transfer.length > 0) port.postMessage(envelope, transfer);
4315
4682
  else port.postMessage(envelope);
@@ -4347,10 +4714,15 @@ function asyncDisposable(target, dispose) {
4347
4714
  * own `handleStreams` / `sendStream` (no backpressure); version 2 is
4348
4715
  * webrun-rpc's `connect` / `serve` over `overPipe` with webrun-http-streams'
4349
4716
  * `fetchOverDuplex` / `serveFetchOverDuplex`: one port per link, calls
4350
- * multiplexed on it, each call a stream with a one-chunk window in both
4351
- * directions, and cancellation that reaches the handler.
4717
+ * multiplexed on it, each call a stream with backpressure in both
4718
+ * directions, and cancellation that reaches the handler. A call's port with a
4719
+ * window (`HttpLinkOptions.window`) pipelines its chunks where both ends speak
4720
+ * it, and goes one confirmed chunk at a time otherwise, so the window needs
4721
+ * no new version.
4352
4722
  */
4353
4723
  const HTTP_LINK_PROTOCOL = "webrun-http-link/2";
4724
+ /** The calling end's default `HttpLinkOptions.window`: 256 KiB. */
4725
+ const DEFAULT_HTTP_LINK_WINDOW = 262144;
4354
4726
  /**
4355
4727
  * Why a request failed when its link went away under it: the far side (the
4356
4728
  * handler, or the ServiceWorker) was lost. The request may or may not have
@@ -4369,12 +4741,13 @@ var LinkLostError = class extends Error {
4369
4741
  */
4370
4742
  const LINK_CLOSE = "webrun-http:link-close";
4371
4743
  /** Opens the calling end of a link on `port` (one port per link). */
4372
- async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
4744
+ async function connectHttpLink(port, { streamTimeout, maxPorts, window = DEFAULT_HTTP_LINK_WINDOW } = {}) {
4373
4745
  const { call, close } = await connect({
4374
4746
  mux: overPipe(port, {
4375
4747
  codec: structuredCodec,
4376
4748
  side: "initiator",
4377
- maxPorts
4749
+ maxPorts,
4750
+ window
4378
4751
  }),
4379
4752
  timeout: streamTimeout
4380
4753
  });
@@ -4900,6 +5273,7 @@ var PortSocketCore = class extends EventTarget {
4900
5273
  code,
4901
5274
  reason
4902
5275
  });
5276
+ if (!Number.isFinite(this._closeTimeoutMs)) return;
4903
5277
  this._closeTimer = this._timers.setTimeout(() => this._finish(1006, "", false), this._closeTimeoutMs);
4904
5278
  }
4905
5279
  _onCloseFrame(code, reason) {
@@ -5210,7 +5584,8 @@ function errorMessage(error) {
5210
5584
  * `whenDrained(limit)` waits for it to fall: the backpressure a producer
5211
5585
  * applies.
5212
5586
  * - When the client's pings stop for three heartbeat periods (a closed tab, a
5213
- * terminated worker), the socket closes with 1006.
5587
+ * terminated worker), the socket closes with 1006. With `watch: "first-ping"`
5588
+ * the watch starts only once the client pings.
5214
5589
  */
5215
5590
  var ServerWebSocket = class extends PortSocketCore {
5216
5591
  static CONNECTING = 0;
@@ -5219,55 +5594,150 @@ var ServerWebSocket = class extends PortSocketCore {
5219
5594
  static CLOSED = 3;
5220
5595
  url;
5221
5596
  protocol;
5597
+ _binaryType = "arraybuffer";
5598
+ _options;
5222
5599
  _accepted = false;
5600
+ _detached = false;
5223
5601
  _queue = [];
5224
5602
  _heartbeatMs = DEFAULT_HEARTBEAT_MS;
5225
5603
  _lastPing = Date.now();
5604
+ /** Whether a ping (with a period a timer takes) arrived already. */
5605
+ _pinged = false;
5226
5606
  _watch;
5227
5607
  constructor(port, options = {}) {
5228
5608
  super({
5229
5609
  timers: options.timers,
5230
5610
  closeTimeoutMs: options.closeTimeoutMs
5231
5611
  });
5612
+ this._options = options;
5232
5613
  this.url = options.url ?? "";
5233
5614
  this.protocol = options.protocol ?? "";
5234
5615
  this._attach(port);
5235
5616
  this._state = 1;
5236
- this._watchHeartbeat();
5617
+ if (options.watch !== "first-ping") this._watchHeartbeat();
5237
5618
  }
5238
5619
  /** Whether `accept()` was called. */
5239
5620
  get accepted() {
5240
5621
  return this._accepted;
5241
5622
  }
5623
+ /** How binary messages arrive: `ArrayBuffer`s (the default) or `Blob`s. Other values are ignored. */
5624
+ get binaryType() {
5625
+ return this._binaryType;
5626
+ }
5627
+ set binaryType(value) {
5628
+ if (value === "blob" || value === "arraybuffer") this._binaryType = value;
5629
+ }
5630
+ /**
5631
+ * Hands this end's transport to another holder before it was accepted: the port to give the
5632
+ * client (a pair's end returned in a 101, say), with the frames that reached this end first
5633
+ * delivered ahead of the rest. This end is closed then, with no event, and answers nothing.
5634
+ */
5635
+ detach() {
5636
+ const port = this._port;
5637
+ if (!port || this._accepted || this._state !== 1) throw new DOMException("Only an open end that was not accepted can be detached.", "InvalidStateError");
5638
+ this._detached = true;
5639
+ this._port = void 0;
5640
+ port.onmessage = null;
5641
+ let owed = this.bufferedAmount;
5642
+ this._state = 3;
5643
+ this._onClosed();
5644
+ this._wakeDrainWaiters();
5645
+ const queued = this._queue.splice(0);
5646
+ if (queued.length === 0 && owed === 0) return port;
5647
+ const relay = new MessageChannel();
5648
+ const holder = relay.port2;
5649
+ const closing = /* @__PURE__ */ new Set();
5650
+ const end = () => {
5651
+ port.onmessage = null;
5652
+ holder.onmessage = null;
5653
+ port.close();
5654
+ holder.close();
5655
+ };
5656
+ const sawClose = (way) => {
5657
+ closing.add(way);
5658
+ if (closing.size === 2) end();
5659
+ };
5660
+ for (const frame of queued) {
5661
+ postFrame(holder, frame);
5662
+ if (frame.t === "c") closing.add("in");
5663
+ }
5664
+ holder.onmessage = (event) => {
5665
+ port.postMessage(event.data, transferables(event.data));
5666
+ if (event.data?.t === "c") sawClose("out");
5667
+ };
5668
+ port.onmessage = (event) => {
5669
+ const data = event.data;
5670
+ if (data?.t === "a" && typeof data.n === "number" && owed > 0) {
5671
+ const absorbed = Math.min(owed, data.n);
5672
+ owed -= absorbed;
5673
+ if (data.n > absorbed) holder.postMessage({
5674
+ t: "a",
5675
+ n: data.n - absorbed
5676
+ });
5677
+ return;
5678
+ }
5679
+ holder.postMessage(event.data, transferables(event.data));
5680
+ if (data?.t === "c") sawClose("in");
5681
+ };
5682
+ port.addEventListener?.("close", end);
5683
+ holder.addEventListener?.("close", end);
5684
+ return relay.port1;
5685
+ }
5242
5686
  /** Starts delivering events, the queued ones first. */
5243
5687
  accept() {
5688
+ if (this._detached) throw new DOMException("This end was detached; it cannot be accepted.", "InvalidStateError");
5244
5689
  if (this._accepted) return;
5245
5690
  this._accepted = true;
5246
- for (const frame of this._queue.splice(0)) super._deliver(frame);
5691
+ for (const frame of this._queue.splice(0)) this._deliver(frame);
5247
5692
  }
5248
5693
  send(data) {
5249
- if (!this._accepted) throw new DOMException("accept() the socket before sending on it.", "InvalidStateError");
5694
+ if (this._detached) throw new DOMException("This end was detached; it cannot send.", "InvalidStateError");
5695
+ if (!this._accepted && !this._options.sendBeforeAccept) throw new DOMException("accept() the socket before sending on it.", "InvalidStateError");
5250
5696
  this._sendData(isBinary(data) ? toArrayBuffer(data) : String(data));
5251
5697
  }
5252
- close(code = 1e3, reason = "") {
5253
- if (!isSendableCloseCode(code)) throw new DOMException(`${code} is not a close code an endpoint may send.`, "InvalidAccessError");
5698
+ close(code, reason = "") {
5699
+ this.checkClose(code, reason);
5700
+ this._startClose(code ?? this._options.defaultCloseCode ?? 1e3, reason);
5701
+ }
5702
+ /**
5703
+ * Refuses a `close()` the endpoint may not make: by default a code outside RFC 6455 §7.4's
5704
+ * sendable ones, or a reason over 123 bytes. A subclass with another platform's rules
5705
+ * overrides it.
5706
+ */
5707
+ checkClose(code, reason) {
5708
+ if (code !== void 0 && !isSendableCloseCode(code)) throw new DOMException(`${code} is not a close code an endpoint may send.`, "InvalidAccessError");
5254
5709
  if (utf8Length(reason) > 123) throw new DOMException("The close reason is longer than 123 bytes.", "SyntaxError");
5255
- this._startClose(code, reason);
5256
5710
  }
5257
5711
  _deliver(frame) {
5258
5712
  if (!this._accepted) {
5259
5713
  this._queue.push(frame);
5260
5714
  return;
5261
5715
  }
5716
+ if (frame.t === "m" && this._state === 2 && this._options.deliverWhileClosing) {
5717
+ this._dispatchMessage(frame.d);
5718
+ this._consumed(messageSize(frame.d));
5719
+ return;
5720
+ }
5262
5721
  super._deliver(frame);
5263
5722
  }
5264
5723
  _dispatchMessage(data) {
5265
- this.dispatchEvent(new MessageEvent("message", { data }));
5724
+ const delivered = typeof data !== "string" && this._binaryType === "blob" ? new Blob([data]) : data;
5725
+ this.dispatchEvent(new MessageEvent("message", { data: delivered }));
5266
5726
  }
5267
5727
  _onPing(hb) {
5268
5728
  this._post({ t: "q" });
5269
5729
  this._lastPing = Date.now();
5270
- if (hb !== this._heartbeatMs) {
5730
+ if (this._state === 3) return;
5731
+ const keep = this._options.heartbeatPeriod === "first" && this._pinged;
5732
+ if (!(hb > 0 && hb < 2 ** 31 - 1)) {
5733
+ if (!keep && this._watch !== void 0) {
5734
+ this._timers.clearInterval(this._watch);
5735
+ this._watch = void 0;
5736
+ }
5737
+ return;
5738
+ }
5739
+ this._pinged = true;
5740
+ if (!keep && (this._watch === void 0 || hb !== this._heartbeatMs)) {
5271
5741
  this._heartbeatMs = hb;
5272
5742
  this._watchHeartbeat();
5273
5743
  }
@@ -5289,6 +5759,11 @@ function isSendableCloseCode(code) {
5289
5759
  if (code >= 3e3 && code <= 4999) return true;
5290
5760
  return code >= 1e3 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006;
5291
5761
  }
5762
+ /** The buffers a relayed frame carries, to transfer rather than copy. */
5763
+ function transferables(data) {
5764
+ const d = data?.d;
5765
+ return d instanceof ArrayBuffer ? [d] : [];
5766
+ }
5292
5767
  //#endregion
5293
5768
  //#region src/ws/upgrade.ts
5294
5769
  /**
@@ -5320,6 +5795,7 @@ function newWebSocketUpgradeRequest(url, { protocols = [], headers } = {}) {
5320
5795
  all.set("connection", "Upgrade");
5321
5796
  all.set("sec-websocket-version", "13");
5322
5797
  if (!all.has("sec-websocket-key")) all.set("sec-websocket-key", randomKey());
5798
+ all.delete("sec-websocket-protocol");
5323
5799
  if (protocols.length > 0) all.set("sec-websocket-protocol", protocols.join(", "));
5324
5800
  if (!all.has("x-webrun-ws-version")) all.set(WS_VERSION_HEADER, `2`);
5325
5801
  return withHeaders(new Request(httpUrl, {
@@ -5379,6 +5855,23 @@ function upgradeWebSocket(request, options = {}) {
5379
5855
  url: request.url,
5380
5856
  protocol
5381
5857
  });
5858
+ const response = webSocketUpgradeResponse(channel.port2, protocol);
5859
+ Object.defineProperty(response, "webSocket", {
5860
+ value: socket,
5861
+ configurable: true
5862
+ });
5863
+ return {
5864
+ socket,
5865
+ response
5866
+ };
5867
+ }
5868
+ /**
5869
+ * A 101 for a socket whose server end the handler runs itself, on the other end of `port`
5870
+ * (a server of its own over this package's frames, say): what `upgradeWebSocket` returns, minus
5871
+ * the `ServerWebSocket`. The host hands `port` to the caller as it would for `upgradeWebSocket`
5872
+ * (`takePort`), and `protocol` (`""` for none) is the subprotocol picked.
5873
+ */
5874
+ function webSocketUpgradeResponse(port, protocol = "") {
5382
5875
  const response = new Response(null, {
5383
5876
  status: 200,
5384
5877
  headers: protocol ? { "sec-websocket-protocol": protocol } : {}
@@ -5392,23 +5885,16 @@ function upgradeWebSocket(request, options = {}) {
5392
5885
  value: "Switching Protocols",
5393
5886
  configurable: true
5394
5887
  },
5395
- webSocket: {
5396
- value: socket,
5397
- configurable: true
5398
- },
5399
5888
  [UPGRADE]: {
5400
5889
  value: {
5401
- port: channel.port2,
5890
+ port,
5402
5891
  protocol
5403
5892
  },
5404
5893
  configurable: true,
5405
5894
  writable: true
5406
5895
  }
5407
5896
  });
5408
- return {
5409
- socket,
5410
- response
5411
- };
5897
+ return response;
5412
5898
  }
5413
5899
  /**
5414
5900
  * The port to hand to the caller, for a response from `upgradeWebSocket`.
@@ -5632,7 +6118,7 @@ async function initHttpService(handler, { key, path, port, streamTimeout }) {
5632
6118
  * resolves with the corresponding `Response`, whose body streams with
5633
6119
  * backpressure.
5634
6120
  */
5635
- async function callHttpService(request, { key, port, streamTimeout }) {
6121
+ async function callHttpService(request, { key, port, streamTimeout, window }) {
5636
6122
  const channel = new MessageChannel();
5637
6123
  const reply = await callChannel(port, "CONNECT", {
5638
6124
  key,
@@ -5643,7 +6129,10 @@ async function callHttpService(request, { key, port, streamTimeout }) {
5643
6129
  channel.port1.close();
5644
6130
  throw new Error(`No service with key "${key}"`);
5645
6131
  }
5646
- return await sendOverConnection(channel.port1, reply, request, { streamTimeout });
6132
+ return await sendOverConnection(channel.port1, reply, request, {
6133
+ streamTimeout,
6134
+ window
6135
+ });
5647
6136
  }
5648
6137
  /**
5649
6138
  * Sends `request` on a port a service accepted with `reply`: over an HTTP link
@@ -5867,6 +6356,51 @@ function handleKeyedChannelCalls(target, callType, key, handler) {
5867
6356
  return () => target.removeEventListener("message", listener);
5868
6357
  }
5869
6358
  //#endregion
6359
+ //#region src/sw/protocol.ts
6360
+ /**
6361
+ * The messages between pages, Dedicated Workers and the same-origin
6362
+ * ServiceWorker. Internal: the shapes may change between versions, which is
6363
+ * why every hand-shake carries `HTTP_LINK_PROTOCOL`.
6364
+ *
6365
+ * THREE PORTS PER HANDLER LINK. A page makes the channels and hands one end of
6366
+ * each to the handler (itself, or a worker) and the other to the
6367
+ * ServiceWorker; after that it carries nothing:
6368
+ *
6369
+ * - `down`: ServiceWorker → handler, the requests the handler serves;
6370
+ * - `ctl`: a raw port next to them, for what HTTP over a stream cannot carry,
6371
+ * transferred `MessagePort`s (WebSocket hand-over);
6372
+ * - `up` (workers only): handler → ServiceWorker, the requests the handler
6373
+ * sends through the router (to other handlers).
6374
+ */
6375
+ /** Page → ServiceWorker: attach a handler link. The name is the one version 1 used. */
6376
+ const ATTACH_CALL = "UPDATE_COMMUNICATION_PORT";
6377
+ /** Page → ServiceWorker: remove a handler link now. */
6378
+ const DETACH_CALL = "webrun-http:detach";
6379
+ /** Page → ServiceWorker: stop routing to a link, let its requests finish, then remove it. */
6380
+ const DRAIN_CALL = "webrun-http:drain";
6381
+ /** Page → ServiceWorker: open a WebSocket to a handler. */
6382
+ const WS_CONNECT_CALL = "webrun-http:ws-connect";
6383
+ /** ServiceWorker → pages: attach your links again (the ServiceWorker restarted). */
6384
+ const NEED_LINKS_CALL = "webrun-http:need-links";
6385
+ /**
6386
+ * Page → ServiceWorker, plain messages (no reply). A message to the
6387
+ * ServiceWorker object is an event: it starts a stopped worker and resets its
6388
+ * idle timer. Port messages do neither.
6389
+ */
6390
+ const WAKE_MESSAGE = "webrun-http:wake";
6391
+ const KEEPALIVE_MESSAGE = "webrun-http:keepalive";
6392
+ /** Page ↔ worker: what `attachWorker` and `serveHttpInWorker` exchange. */
6393
+ const WORKER_HELLO = "webrun-http:worker-hello";
6394
+ /** `headers` as pairs a `postMessage` can carry (a `Headers` object cannot be cloned). */
6395
+ function headerPairs(headers) {
6396
+ if (headers === void 0) return void 0;
6397
+ const pairs = [];
6398
+ new Headers(headers).forEach((value, name) => {
6399
+ pairs.push([name, value]);
6400
+ });
6401
+ return pairs;
6402
+ }
6403
+ //#endregion
5870
6404
  //#region src/sw/handler-endpoint.ts
5871
6405
  /** The message calls through a lost ServiceWorker fail with. */
5872
6406
  const ROUTER_LOST_MESSAGE = "The ServiceWorker was lost while this call was in flight";
@@ -5950,8 +6484,11 @@ var HandlerEndpoint = class {
5950
6484
  this._startKeepAlive();
5951
6485
  return await link.up.send(request);
5952
6486
  }
5953
- /** Opens a socket to the handler `url` routes to, through the ServiceWorker. */
5954
- async connectWebSocket(url, protocols = []) {
6487
+ /**
6488
+ * Opens a socket to the handler `url` routes to, through the ServiceWorker. The handler sees
6489
+ * `headers` on the upgrade request, and so does a `route` function choosing it.
6490
+ */
6491
+ async connectWebSocket(url, protocols = [], headers) {
5955
6492
  const link = await this._linkWith("ctl");
5956
6493
  const id = this._sockets.newId();
5957
6494
  const answer = this._sockets.wait(id, this.options.wsConnectTimeoutMs ?? 15e3, `the socket request for ${url}`);
@@ -5959,7 +6496,8 @@ var HandlerEndpoint = class {
5959
6496
  t: "ws-open",
5960
6497
  id,
5961
6498
  url,
5962
- protocols
6499
+ protocols,
6500
+ headers: headerPairs(headers)
5963
6501
  });
5964
6502
  return await answer;
5965
6503
  }
@@ -6199,43 +6737,22 @@ function set(key, value, customStore = defaultGetStore()) {
6199
6737
  });
6200
6738
  }
6201
6739
  //#endregion
6202
- //#region src/sw/protocol.ts
6203
- /**
6204
- * The messages between pages, Dedicated Workers and the same-origin
6205
- * ServiceWorker. Internal: the shapes may change between versions, which is
6206
- * why every hand-shake carries `HTTP_LINK_PROTOCOL`.
6207
- *
6208
- * THREE PORTS PER HANDLER LINK. A page makes the channels and hands one end of
6209
- * each to the handler (itself, or a worker) and the other to the
6210
- * ServiceWorker; after that it carries nothing:
6211
- *
6212
- * - `down`: ServiceWorker → handler, the requests the handler serves;
6213
- * - `ctl`: a raw port next to them, for what HTTP over a stream cannot carry,
6214
- * transferred `MessagePort`s (WebSocket hand-over);
6215
- * - `up` (workers only): handler → ServiceWorker, the requests the handler
6216
- * sends through the router (to other handlers).
6217
- */
6218
- /** Page → ServiceWorker: attach a handler link. The name is the one version 1 used. */
6219
- const ATTACH_CALL = "UPDATE_COMMUNICATION_PORT";
6220
- /** Page → ServiceWorker: remove a handler link now. */
6221
- const DETACH_CALL = "webrun-http:detach";
6222
- /** Page → ServiceWorker: stop routing to a link, let its requests finish, then remove it. */
6223
- const DRAIN_CALL = "webrun-http:drain";
6224
- /** Page → ServiceWorker: open a WebSocket to a handler. */
6225
- const WS_CONNECT_CALL = "webrun-http:ws-connect";
6226
- /** ServiceWorker → pages: attach your links again (the ServiceWorker restarted). */
6227
- const NEED_LINKS_CALL = "webrun-http:need-links";
6740
+ //#region src/sw/http-sw-dispatcher.ts
6228
6741
  /**
6229
- * Page → ServiceWorker, plain messages (no reply). A message to the
6230
- * ServiceWorker object is an event: it starts a stopped worker and resets its
6231
- * idle timer. Port messages do neither.
6742
+ * The default key: the url's first path segment under `scope`, decoded; none outside it, at the
6743
+ * scope itself, or for a segment that does not decode.
6232
6744
  */
6233
- const WAKE_MESSAGE = "webrun-http:wake";
6234
- const KEEPALIVE_MESSAGE = "webrun-http:keepalive";
6235
- /** Page ↔ worker: what `attachWorker` and `serveHttpInWorker` exchange. */
6236
- const WORKER_HELLO = "webrun-http:worker-hello";
6237
- //#endregion
6238
- //#region src/sw/http-sw-dispatcher.ts
6745
+ function scopeKey(url, scope) {
6746
+ const href = `${url}`;
6747
+ if (!href.startsWith(scope)) return void 0;
6748
+ const key = href.substring(scope.length).replace(/[?#].*$/, "").split("/")[0];
6749
+ if (!key) return void 0;
6750
+ try {
6751
+ return decodeURIComponent(key);
6752
+ } catch {
6753
+ return;
6754
+ }
6755
+ }
6239
6756
  /** One attached handler link, as the ServiceWorker holds it. */
6240
6757
  var HandlerEntry = class {
6241
6758
  key;
@@ -6275,7 +6792,8 @@ const TIMED_OUT = Symbol("timed-out");
6275
6792
  * (`SwHttpAdapter.register`) or in Dedicated Workers
6276
6793
  * (`SwHttpAdapter.attachWorker` + `serveHttpInWorker`).
6277
6794
  *
6278
- * A url `<scope>/<key>/...` belongs to the handlers of `key`. Of those, the
6795
+ * A url `<scope>/<key>/...` belongs to the handlers of `key` (the `key`
6796
+ * option maps urls to keys some other way). Of those, the
6279
6797
  * `route` option picks one per request. Requests and responses stream with
6280
6798
  * backpressure in both directions, and an aborted fetch reaches the handler
6281
6799
  * as a cancellation (webrun-rpc + webrun-http-streams).
@@ -6294,6 +6812,8 @@ const TIMED_OUT = Symbol("timed-out");
6294
6812
  */
6295
6813
  var SwHttpDispatcher = class {
6296
6814
  self;
6815
+ /** `self`, with the WebWorker lib's types this module compiles with. */
6816
+ _sw;
6297
6817
  log;
6298
6818
  options;
6299
6819
  table = new HandlerTable();
@@ -6316,19 +6836,22 @@ var SwHttpDispatcher = class {
6316
6836
  constructor(options) {
6317
6837
  this.options = options;
6318
6838
  this.self = options.self;
6839
+ this._sw = options.self;
6319
6840
  this.log = options.log ?? (() => {});
6320
6841
  if (locksAvailable()) this.routerLock = newLockName("webrun-http-router");
6321
6842
  }
6322
6843
  get scope() {
6323
- return `${new URL("./", this.self.location.href)}`;
6844
+ return this._sw.registration?.scope ?? `${new URL("./", this._sw.location.href)}`;
6324
6845
  }
6325
- /** The key `url` belongs to: its first path segment under the scope. */
6846
+ /** The key `url` belongs to (the `key` option; by default its first path segment under the scope). */
6326
6847
  keyOf(url) {
6327
- const href = `${url}`;
6328
- const scope = this.scope;
6329
- if (!href.startsWith(scope)) return void 0;
6330
- const key = href.substring(scope.length).replace(/[?#].*$/, "").split("/")[0];
6331
- return key ? decodeURIComponent(key) : void 0;
6848
+ let parsed;
6849
+ try {
6850
+ parsed = new URL(`${url}`);
6851
+ } catch {
6852
+ return;
6853
+ }
6854
+ return (this.options.key ?? scopeKey)(parsed, this.scope);
6332
6855
  }
6333
6856
  /** Whether `key` has ever been served on this worker. */
6334
6857
  isClaimedKey(key) {
@@ -6339,7 +6862,7 @@ var SwHttpDispatcher = class {
6339
6862
  return this.claimedKeys.has(key) || this.table.all(key).length > 0;
6340
6863
  }
6341
6864
  start() {
6342
- const self = this.self;
6865
+ const self = this._sw;
6343
6866
  if (this.routerLock) this._routerHeld = holdLock(this.routerLock).then(() => {});
6344
6867
  this._restored = this._restoreClaimedKeys();
6345
6868
  const latch = () => {
@@ -6359,8 +6882,8 @@ var SwHttpDispatcher = class {
6359
6882
  return true;
6360
6883
  }), handleChannelCalls(self, WS_CONNECT_CALL, async (event, params) => {
6361
6884
  if (!this._isOwnClient(event)) throw new Error("Only this origin's pages may open sockets");
6362
- const { url, protocols } = params;
6363
- const connection = await this.connectWebSocket(url, protocols ?? []);
6885
+ const { url, protocols, headers } = params;
6886
+ const connection = await this.connectWebSocket(url, protocols ?? [], this._socketHeaders(headers, "page"));
6364
6887
  return withTransfer(connection, [connection.port]);
6365
6888
  }));
6366
6889
  const onMessage = (event) => {
@@ -6378,10 +6901,10 @@ var SwHttpDispatcher = class {
6378
6901
  this.log("Claim control over all clients.");
6379
6902
  event.waitUntil(self.clients.claim());
6380
6903
  };
6381
- const onFetch = (event) => this._onFetch(event);
6904
+ const onFetch = (event) => void this.handleFetch(event);
6382
6905
  self.addEventListener("install", onInstall);
6383
6906
  self.addEventListener("activate", onActivate);
6384
- self.addEventListener("fetch", onFetch);
6907
+ if (this.options.handleFetch !== false) self.addEventListener("fetch", onFetch);
6385
6908
  this._cleanup.push(() => {
6386
6909
  self.removeEventListener("install", onInstall);
6387
6910
  self.removeEventListener("activate", onActivate);
@@ -6396,37 +6919,60 @@ var SwHttpDispatcher = class {
6396
6919
  }
6397
6920
  /**
6398
6921
  * Answers `request` as a fetch under the scope would be: through the
6399
- * handler its key routes to. A url outside the scope, or of a key this
6400
- * worker never served, goes to the network.
6922
+ * handler its key routes to. A url with no key (by default, one outside the
6923
+ * scope), or of a key this worker never served, goes to the network.
6401
6924
  */
6402
6925
  async dispatch(request) {
6403
6926
  const key = this.keyOf(request.url);
6404
6927
  if (key === void 0) return await fetch(request);
6405
6928
  await this._restored.catch(() => {});
6406
6929
  const entry = await this._acquire(request, key);
6407
- if (entry === TIMED_OUT) return new Response(null, {
6408
- status: 503,
6409
- statusText: "Service Unavailable (no handler attached in time)"
6410
- });
6930
+ if (entry === TIMED_OUT) return await this._failed(request, { kind: "timeout" });
6411
6931
  if (!entry) {
6412
6932
  if (!this.serves(key)) return await fetch(request);
6413
- return new Response(null, {
6414
- status: 404,
6415
- statusText: "Not Found (no active handler)"
6416
- });
6933
+ return await this._failed(request, { kind: "gone" });
6417
6934
  }
6418
6935
  try {
6419
6936
  return await entry.link.send(request);
6420
6937
  } catch (error) {
6421
6938
  this.log("[dispatch] failed", key, error);
6422
- return new Response(errorMessage(error), {
6423
- status: 502,
6424
- statusText: "Bad Gateway",
6425
- headers: { "content-type": "text/plain; charset=utf-8" }
6939
+ return await this._failed(request, {
6940
+ kind: "lost",
6941
+ error
6426
6942
  });
6427
6943
  }
6428
6944
  }
6429
6945
  /**
6946
+ * After `start()`: settles once the keys this worker served before a restart are read back
6947
+ * (whatever the outcome). Until then `serves()` may answer `false` for a key it will serve.
6948
+ */
6949
+ get ready() {
6950
+ return this._restored.then(() => {}, () => {});
6951
+ }
6952
+ _socketHeaders(headers, from) {
6953
+ return this.options.socketHeaders ? this.options.socketHeaders(headers, from) : headers;
6954
+ }
6955
+ async _failed(request, failure) {
6956
+ if (this.options.failure) try {
6957
+ return await this.options.failure(request, failure);
6958
+ } catch (error) {
6959
+ this.log("[dispatch] the failure option threw; answering by default", error);
6960
+ }
6961
+ if (failure.kind === "timeout") return new Response(null, {
6962
+ status: 503,
6963
+ statusText: "Service Unavailable (no handler attached in time)"
6964
+ });
6965
+ if (failure.kind === "gone") return new Response(null, {
6966
+ status: 404,
6967
+ statusText: "Not Found (no active handler)"
6968
+ });
6969
+ return new Response(errorMessage(failure.error), {
6970
+ status: 502,
6971
+ statusText: "Bad Gateway",
6972
+ headers: { "content-type": "text/plain; charset=utf-8" }
6973
+ });
6974
+ }
6975
+ /**
6430
6976
  * Opens a socket to the handler `url` routes to: the handler accepts or
6431
6977
  * refuses it like an HTTP upgrade, and the socket's port comes back. After
6432
6978
  * that the socket runs between the caller and the handler directly; this
@@ -6438,7 +6984,7 @@ var SwHttpDispatcher = class {
6438
6984
  headers
6439
6985
  });
6440
6986
  const key = this.keyOf(request.url);
6441
- if (key === void 0) throw new Error(`${url} is outside this ServiceWorker's scope`);
6987
+ if (key === void 0) throw new Error(`${url} belongs to no key of this ServiceWorker`);
6442
6988
  await this._restored.catch(() => {});
6443
6989
  const entry = await this._acquire(request, key);
6444
6990
  if (!entry || entry === TIMED_OUT) throw new Error(`No handler serves ${url}`);
@@ -6454,20 +7000,47 @@ var SwHttpDispatcher = class {
6454
7000
  });
6455
7001
  return await answer;
6456
7002
  }
6457
- _onFetch(event) {
6458
- const request = event.request;
7003
+ /**
7004
+ * Answers `event` if it is the dispatcher's, with `request` (default `event.request`; a host
7005
+ * may pass a rewritten one, with headers it adds). Returns whether it called `respondWith`;
7006
+ * when it did not, the event is the caller's to answer or to leave to the network. For a host
7007
+ * that listens to `fetch` itself (`handleFetch: false`); call it synchronously in the event's
7008
+ * listener, as `respondWith` must be.
7009
+ *
7010
+ * It takes a same-origin request whose key (the `key` option) a handler serves, and also:
7011
+ * - a navigation under the scope whose key nobody serves (answered by `unserved`);
7012
+ * - before the stored keys are read (the first requests after a restart), every request with
7013
+ * a key, since only then is it known whose it is: one nobody serves gets what it would have
7014
+ * got after, `unserved` for a navigation and the network for anything else.
7015
+ * So a host with a page of its own for urls nobody serves gives it as `unserved`, rather than
7016
+ * deciding before calling this.
7017
+ */
7018
+ handleFetch(event, request = event.request) {
7019
+ const navigation = event.request.mode === "navigate";
7020
+ if (new URL(request.url).origin !== this._sw.location.origin) return false;
6459
7021
  const key = this.keyOf(request.url);
6460
- if (key === void 0) return;
7022
+ if (key === void 0) return false;
6461
7023
  if (this._restoreSettled) {
6462
- if (this.serves(key)) event.respondWith(this.dispatch(request));
6463
- else if (request.mode === "navigate") event.respondWith(fetch(request));
6464
- return;
7024
+ if (this.serves(key)) {
7025
+ event.respondWith(this.dispatch(request));
7026
+ return true;
7027
+ }
7028
+ if (navigation) {
7029
+ event.respondWith(this._unserved(request));
7030
+ return true;
7031
+ }
7032
+ return false;
6465
7033
  }
6466
7034
  event.respondWith((async () => {
6467
7035
  await this._restored.catch(() => {});
6468
- if (!this.serves(key)) return await fetch(request);
6469
- return await this.dispatch(request);
7036
+ if (this.serves(key)) return await this.dispatch(request);
7037
+ return navigation ? await this._unserved(request) : await fetch(request);
6470
7038
  })());
7039
+ return true;
7040
+ }
7041
+ /** What a request the dispatcher took, and no handler serves, gets: `unserved`, or the network. */
7042
+ async _unserved(request) {
7043
+ return await (this.options.unserved ?? fetch)(request);
6471
7044
  }
6472
7045
  async _attach(event, params, ports) {
6473
7046
  const clientId = event.source?.id;
@@ -6486,8 +7059,11 @@ var SwHttpDispatcher = class {
6486
7059
  }
6487
7060
  const ctl = params.ctl ? rest.shift() : void 0;
6488
7061
  const up = params.up ? rest.shift() : void 0;
6489
- const streamTimeout = this.options.streamTimeout;
6490
- const link = await connectHttpLink(down, { streamTimeout });
7062
+ const { streamTimeout, window } = this.options;
7063
+ const link = await connectHttpLink(down, {
7064
+ streamTimeout,
7065
+ window
7066
+ });
6491
7067
  const entry = new HandlerEntry(params.key, params.id ?? clientId ?? crypto.randomUUID(), params.owner ?? crypto.randomUUID(), params.meta, link, ctl, clientId, params.ownerLock);
6492
7068
  if (up) entry.closeUp = await serveHttpLink(up, (request) => this.dispatch(request), { streamTimeout });
6493
7069
  if (ctl) ctl.onmessage = (message) => this._onControl(entry, message.data);
@@ -6566,20 +7142,35 @@ var SwHttpDispatcher = class {
6566
7142
  * its key, waiting for one when there is none.
6567
7143
  */
6568
7144
  async _acquire(request, key) {
7145
+ const deadline = Date.now() + (this.options.attachTimeoutMs ?? 15e3);
6569
7146
  const picked = await this._pick(request, key);
6570
7147
  if (picked) return picked;
6571
- if (this.table.all(key).length > 0) return await this.table.waitFor(key, this.options.attachTimeoutMs ?? 15e3, this.table.live(key).length > 0) ? await this._pick(request, key) ?? TIMED_OUT : TIMED_OUT;
6572
- const vacantUntil = this._vacant.get(key) ?? 0;
6573
- if (vacantUntil > Date.now()) return await this.table.waitFor(key, vacantUntil - Date.now()) ? await this._pick(request, key) ?? TIMED_OUT : TIMED_OUT;
6574
- if (!this.serves(key)) return void 0;
6575
- await this._askForLinks(key);
6576
- return await this._pick(request, key);
7148
+ if (this.table.all(key).length === 0) {
7149
+ const vacantUntil = this._vacant.get(key) ?? 0;
7150
+ if (vacantUntil > Date.now()) return await this._waitToPick(request, key, vacantUntil);
7151
+ if (!this.serves(key)) return void 0;
7152
+ await this._askForLinks(key);
7153
+ const relinked = await this._pick(request, key);
7154
+ if (relinked) return relinked;
7155
+ if (this.table.all(key).length === 0) return void 0;
7156
+ }
7157
+ return await this._waitToPick(request, key, deadline);
7158
+ }
7159
+ /** Waits for attaches until `route` accepts a handler, or `until` passes. */
7160
+ async _waitToPick(request, key, until) {
7161
+ for (;;) {
7162
+ const left = until - Date.now();
7163
+ if (left <= 0) return TIMED_OUT;
7164
+ await this.table.waitFor(key, Math.min(left, 1e3), this.table.live(key).length > 0);
7165
+ const picked = await this._pick(request, key);
7166
+ if (picked) return picked;
7167
+ }
6577
7168
  }
6578
7169
  async _pick(request, key) {
6579
7170
  let live = this.table.live(key);
6580
7171
  for (const entry of live) {
6581
7172
  if (entry.ownerLock || !entry.clientId) continue;
6582
- if (!await this.self.clients.get(entry.clientId)) this._close(entry, new LinkLostError(HANDLER_LOST_MESSAGE));
7173
+ if (!await this._sw.clients.get(entry.clientId)) this._close(entry, new LinkLostError(HANDLER_LOST_MESSAGE));
6583
7174
  }
6584
7175
  live = this.table.live(key);
6585
7176
  if (live.length === 0) return void 0;
@@ -6603,7 +7194,7 @@ var SwHttpDispatcher = class {
6603
7194
  }
6604
7195
  async _linkRound(key) {
6605
7196
  const timeout = this.options.relinkTimeoutMs ?? 3e3;
6606
- const windows = await this.self.clients.matchAll({
7197
+ const windows = await this._sw.clients.matchAll({
6607
7198
  type: "window",
6608
7199
  includeUncontrolled: true
6609
7200
  });
@@ -6641,7 +7232,7 @@ var SwHttpDispatcher = class {
6641
7232
  entry.sockets.fail(message.id, new Error(message.message));
6642
7233
  return;
6643
7234
  case "ws-open":
6644
- this.connectWebSocket(message.url, message.protocols).then(({ port, protocol }) => entry.ctl?.postMessage({
7235
+ this.connectWebSocket(message.url, message.protocols, this._socketHeaders(message.headers, "handler")).then(({ port, protocol }) => entry.ctl?.postMessage({
6645
7236
  t: "ws-opened",
6646
7237
  id: message.id,
6647
7238
  protocol,
@@ -6658,7 +7249,7 @@ var SwHttpDispatcher = class {
6658
7249
  const source = event.source;
6659
7250
  if (!source?.url) return false;
6660
7251
  try {
6661
- return new URL(source.url).origin === this.self.location.origin;
7252
+ return new URL(source.url).origin === this._sw.location.origin;
6662
7253
  } catch {
6663
7254
  return false;
6664
7255
  }
@@ -6682,6 +7273,7 @@ var SwHttpDispatcher = class {
6682
7273
  * that stops it.
6683
7274
  */
6684
7275
  function startHttpDispatcher(options) {
7276
+ if (options.handleFetch === false) throw new Error("startHttpDispatcher: handleFetch: false needs a dispatcher to call handleFetch on; construct it instead: new SwHttpDispatcher({ self, handleFetch: false }).start()");
6685
7277
  const dispatcher = new SwHttpDispatcher(options);
6686
7278
  dispatcher.start();
6687
7279
  return () => {
@@ -6822,6 +7414,8 @@ var SwPortHandler = class {
6822
7414
  */
6823
7415
  var SwPortDispatcher = class {
6824
7416
  self;
7417
+ /** `self`, with the WebWorker lib's types this module compiles with. */
7418
+ _sw;
6825
7419
  log;
6826
7420
  handlersIndex = /* @__PURE__ */ new Map();
6827
7421
  /**
@@ -6835,10 +7429,11 @@ var SwPortDispatcher = class {
6835
7429
  _activationPromise;
6836
7430
  constructor({ self, log = () => {} }) {
6837
7431
  this.self = self;
7432
+ this._sw = self;
6838
7433
  this.log = log;
6839
7434
  }
6840
7435
  get scope() {
6841
- return `${new URL("./", this.self.location.href)}`;
7436
+ return this._sw.registration?.scope ?? `${new URL("./", this._sw.location.href)}`;
6842
7437
  }
6843
7438
  /**
6844
7439
  * Look up the active channel for a key. Returns `undefined` when there
@@ -6851,7 +7446,7 @@ var SwPortDispatcher = class {
6851
7446
  if (channelInfo.key !== key) continue;
6852
7447
  const clientId = channelInfo.clientId;
6853
7448
  if (!clientId) continue;
6854
- if (!await this.self.clients.get(clientId)) {
7449
+ if (!await this._sw.clients.get(clientId)) {
6855
7450
  this.handlersIndex.delete(clientId);
6856
7451
  continue;
6857
7452
  }
@@ -6863,8 +7458,8 @@ var SwPortDispatcher = class {
6863
7458
  return this.claimedKeys.has(key);
6864
7459
  }
6865
7460
  start() {
6866
- const stopClaims = handleClaimRequests(this.self);
6867
- const stopPortUpdates = handleChannelCalls(this.self, "UPDATE_COMMUNICATION_PORT", async (event, channelInfo, port) => {
7461
+ const stopClaims = handleClaimRequests(this._sw);
7462
+ const stopPortUpdates = handleChannelCalls(this._sw, "UPDATE_COMMUNICATION_PORT", async (event, channelInfo, port) => {
6868
7463
  const clientId = event.source?.id;
6869
7464
  this.log("[UPDATE_COMMUNICATION_PORT]", clientId, channelInfo);
6870
7465
  await this._updateChannelInfo({
@@ -6878,14 +7473,14 @@ var SwPortDispatcher = class {
6878
7473
  stopClaims();
6879
7474
  stopPortUpdates();
6880
7475
  };
6881
- this.self.addEventListener("install", (event) => {
7476
+ this._sw.addEventListener("install", (event) => {
6882
7477
  this.log("Skip waiting on install.", event);
6883
- this.self.skipWaiting();
7478
+ this._sw.skipWaiting();
6884
7479
  });
6885
- this.self.addEventListener("activate", (event) => {
7480
+ this._sw.addEventListener("activate", (event) => {
6886
7481
  this.log("Claim control over all clients.", event);
6887
7482
  event.waitUntil((async () => {
6888
- await this.self.clients.claim();
7483
+ await this._sw.clients.claim();
6889
7484
  await this._checkActivation();
6890
7485
  })());
6891
7486
  });
@@ -6935,7 +7530,7 @@ var SwPortDispatcher = class {
6935
7530
  for (const clientId of this.handlersIndex.keys()) ids.add(clientId);
6936
7531
  const index = /* @__PURE__ */ new Map();
6937
7532
  for (const clientId of ids) {
6938
- const client = await this.self.clients.get(clientId);
7533
+ const client = await this._sw.clients.get(clientId);
6939
7534
  if (client) index.set(clientId, client);
6940
7535
  else this.handlersIndex.delete(clientId);
6941
7536
  }
@@ -6988,7 +7583,10 @@ var SwHttpAdapter = class {
6988
7583
  return this.options.key;
6989
7584
  }
6990
7585
  get scope() {
6991
- return this.options.scope ?? new URL("./", this.serviceWorkerUrl).pathname;
7586
+ if (this.options.scope) return this.options.scope;
7587
+ const registration = this.options.registration;
7588
+ if (registration) return new URL(registration.scope).pathname;
7589
+ return new URL("./", this.serviceWorkerUrl).pathname;
6992
7590
  }
6993
7591
  /**
6994
7592
  * The scope as a url on the worker's origin. Resolved against the worker's
@@ -6999,6 +7597,8 @@ var SwHttpAdapter = class {
6999
7597
  * sits next to it).
7000
7598
  */
7001
7599
  get rootUrl() {
7600
+ const registration = this.options.registration;
7601
+ if (registration && !this.options.serviceWorkerUrl) return new URL(this.scope, registration.scope);
7002
7602
  const base = this.options.serviceWorkerUrl ? this.serviceWorkerUrl : import.meta.url;
7003
7603
  return new URL(this.scope, base);
7004
7604
  }
@@ -7020,7 +7620,10 @@ var SwHttpAdapter = class {
7020
7620
  */
7021
7621
  async start() {
7022
7622
  if (!this._registrationPromise) this._registrationPromise = (async () => {
7023
- const registration = await navigator.serviceWorker.register(this.serviceWorkerUrl, { scope: this.scope });
7623
+ const registration = this.options.registration ?? await navigator.serviceWorker.register(this.serviceWorkerUrl, {
7624
+ scope: this.scope,
7625
+ ...this.options.type ? { type: this.options.type } : {}
7626
+ });
7024
7627
  this._registration = registration;
7025
7628
  const stopListening = handleChannelCalls(navigator.serviceWorker, NEED_LINKS_CALL, async (_event, params) => {
7026
7629
  await this.relink(params?.routerLock);
@@ -7050,7 +7653,7 @@ var SwHttpAdapter = class {
7050
7653
  })();
7051
7654
  return this._registrationPromise;
7052
7655
  }
7053
- /** Detaches every link, and unregisters the worker. */
7656
+ /** Detaches every link, and unregisters the worker (unless the page passed its `registration`). */
7054
7657
  async stop() {
7055
7658
  for (const cleanup of this._cleanup.splice(0)) cleanup();
7056
7659
  for (const worker of [...this._workers]) await worker.detach().catch(() => {});
@@ -7067,7 +7670,7 @@ var SwHttpAdapter = class {
7067
7670
  this._registration = void 0;
7068
7671
  this._registrationPromise = void 0;
7069
7672
  try {
7070
- await registration?.unregister();
7673
+ if (!this.options.registration) await registration?.unregister();
7071
7674
  } catch (error) {
7072
7675
  console.log("Service Worker unregistration failed: ", error);
7073
7676
  }
@@ -7141,10 +7744,10 @@ var SwHttpAdapter = class {
7141
7744
  /**
7142
7745
  * Opens the transport of a socket to the handler `url` routes to: a handler
7143
7746
  * of this page directly, any other through the ServiceWorker. Resolves once
7144
- * the handler accepted. An arrow function, so it can be handed around:
7145
- * `createWebSocketClass(adapter.connectWebSocket)`.
7747
+ * the handler accepted. The handler sees `headers` on the upgrade request. An arrow function,
7748
+ * so it can be handed around: `createWebSocketClass(adapter.connectWebSocket)`.
7146
7749
  */
7147
- connectWebSocket = async (url, protocols = []) => {
7750
+ connectWebSocket = async (url, protocols = [], headers) => {
7148
7751
  const httpUrl = new URL(`${url}`, globalThis.location?.href);
7149
7752
  if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
7150
7753
  else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
@@ -7152,7 +7755,8 @@ var SwHttpAdapter = class {
7152
7755
  if (local) {
7153
7756
  const result = await serveWebSocketUpgrade(local, {
7154
7757
  url: `${httpUrl}`,
7155
- protocols
7758
+ protocols,
7759
+ headers
7156
7760
  });
7157
7761
  if (!result.ok) throw new Error(result.message);
7158
7762
  return {
@@ -7163,7 +7767,8 @@ var SwHttpAdapter = class {
7163
7767
  await this.start();
7164
7768
  return await callChannelWithin(this._callTimeout(), this._worker(), WS_CONNECT_CALL, {
7165
7769
  url: `${httpUrl}`,
7166
- protocols
7770
+ protocols,
7771
+ headers: headerPairs(headers)
7167
7772
  });
7168
7773
  };
7169
7774
  /**
@@ -7459,11 +8064,11 @@ function serveHttpInWorker(handler, options = {}) {
7459
8064
  };
7460
8065
  scope.addEventListener("message", onMessage);
7461
8066
  const baseUrl = () => globalThis.location?.href;
7462
- const connectWebSocket = (url, protocols = []) => {
8067
+ const connectWebSocket = (url, protocols = [], headers) => {
7463
8068
  const httpUrl = new URL(`${url}`, baseUrl());
7464
8069
  if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
7465
8070
  else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
7466
- return endpoint.connectWebSocket(`${httpUrl}`, protocols);
8071
+ return endpoint.connectWebSocket(`${httpUrl}`, protocols, headers);
7467
8072
  };
7468
8073
  let WebSocketClass;
7469
8074
  return {
@@ -7490,4 +8095,4 @@ function serveHttpInWorker(handler, options = {}) {
7490
8095
  };
7491
8096
  }
7492
8097
  //#endregion
7493
- export { CLAIM_CALL, DEFAULT_CLOSE_TIMEOUT_MS, DEFAULT_HEARTBEAT_MS, DEFAULT_SERVICE_WORKER_TIMEOUT, DuplexSiteBuilder, EXCHANGE_ABANDONED_MESSAGE, HANDLER_LOST_MESSAGE, HTTP_LINK_PROTOCOL, HandlerEndpoint, HandlerTable, HttpError, HttpParseError, LinkLostError, PEER_ERROR_HEADER, PortRendezvous, ROUTER_LOST_MESSAGE, ServerWebSocket, ServiceWorkerControlError, SwHttpAdapter, SwHttpDispatcher, SwPortDispatcher, SwPortHandler, TransportClosedError, WS_PORT_PROTOCOL_VERSION, WS_VERSION_HEADER, WsCloseEvent, answerConnect, awaitActiveServiceWorker, awaitServiceWorkerControl, callChannel, callChannelWithin, callHttpService, callWebSocketService, collect, collectBytes, collectString, connectHttpLink, connectWebSocketThroughServiceWorker, createWebSocketClass, decodeJsonl, decodeMessage, decodeText, defaultCodec, deserializeError, emulateMux, encodeJsonl, encodeMessage, encodeText, fetchOverDuplex, fromReadableStream, getRelayWindowMessageHandler, handleChannelCalls, handleClaimRequests, handleHttpRequests, handleStreams, holdLock, httpCodec, httpFetch, httpServe, initHttpService, initServiceWorker, initializeConnection, isWebSocketUpgrade, joinLines, jsonEnvelopeCodec, leastInflightRoute, locksAvailable, map, newAsyncGenerator, newCreditGrantor, newCreditLedger, newHttpClientStub, newHttpCodec, newHttpServerStub, newInvokationChannel, newLockName, newRegistry, newRemoteRelayChannel, newServiceWorkerPort, newSniffingCodec, newWebSocketUpgradeRequest, normalizeToUint8Array, recieveIterator, registerConnectionsHandler, relayWebSocketClass, requestedProtocols, sendHttpRequest, sendIterator, sendOverConnection, sendStream, serializeError, serveFetchOverDuplex, serveHttpInWorker, serveHttpLink, serveWebSocketUpgrade, serviceWorkerWebSocketClass, splitLines, splitServiceUrl, startHttpDispatcher, takePort, takeUpgrade, toChunks, toReadableStream, trackedLink, unwrapTransfer, upgradeWebSocket, watchLock, withHeaders, withTransfer };
8098
+ export { CLAIM_CALL, DEFAULT_CLOSE_TIMEOUT_MS, DEFAULT_HEARTBEAT_MS, DEFAULT_SERVICE_WORKER_TIMEOUT, DuplexSiteBuilder, EXCHANGE_ABANDONED_MESSAGE, HANDLER_LOST_MESSAGE, HTTP_LINK_PROTOCOL, HandlerEndpoint, HandlerTable, HttpError, HttpParseError, LinkLostError, PEER_ERROR_HEADER, PortRendezvous, ROUTER_LOST_MESSAGE, ServerWebSocket, ServiceWorkerControlError, SwHttpAdapter, SwHttpDispatcher, SwPortDispatcher, SwPortHandler, TransportClosedError, WS_PORT_PROTOCOL_VERSION, WS_VERSION_HEADER, WsCloseEvent, answerConnect, awaitActiveServiceWorker, awaitServiceWorkerControl, callChannel, callChannelWithin, callHttpService, callWebSocketService, collect, collectBytes, collectString, connectHttpLink, connectWebSocketThroughServiceWorker, createWebSocketClass, decodeJsonl, decodeMessage, decodeText, defaultCodec, deserializeError, emulateMux, encodeJsonl, encodeMessage, encodeText, fetchOverDuplex, fromReadableStream, getRelayWindowMessageHandler, handleChannelCalls, handleClaimRequests, handleHttpRequests, handleStreams, holdLock, httpCodec, httpFetch, httpServe, initHttpService, initServiceWorker, initializeConnection, isWebSocketUpgrade, joinLines, jsonEnvelopeCodec, leastInflightRoute, locksAvailable, map, newAsyncGenerator, newCreditGrantor, newCreditLedger, newHttpClientStub, newHttpCodec, newHttpServerStub, newInvokationChannel, newLockName, newRegistry, newRemoteRelayChannel, newServiceWorkerPort, newSniffingCodec, newWebSocketUpgradeRequest, normalizeToUint8Array, recieveIterator, registerConnectionsHandler, relayWebSocketClass, requestedProtocols, scopeKey, sendHttpRequest, sendIterator, sendOverConnection, sendStream, serializeError, serveFetchOverDuplex, serveHttpInWorker, serveHttpLink, serveWebSocketUpgrade, serviceWorkerWebSocketClass, splitLines, splitServiceUrl, startHttpDispatcher, takePort, takeUpgrade, toChunks, toReadableStream, trackedLink, unwrapTransfer, upgradeWebSocket, watchLock, webSocketUpgradeResponse, withHeaders, withTransfer };