@statewalker/webrun-http-browser 0.10.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -10
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/service-worker-control.d.ts +2 -1
- package/dist/core/service-worker-control.d.ts.map +1 -1
- package/dist/core/sw-scope.d.ts +31 -0
- package/dist/core/sw-scope.d.ts.map +1 -0
- package/dist/index.js +834 -225
- package/dist/link/http-link.d.ts +16 -3
- package/dist/link/http-link.d.ts.map +1 -1
- package/dist/relay/index-sw.d.ts +8 -2
- package/dist/relay/index-sw.d.ts.map +1 -1
- package/dist/relay/index.d.ts +1 -1
- package/dist/relay/index.d.ts.map +1 -1
- package/dist/relay-sw.js +433 -83
- package/dist/relay-worker.d.ts +1 -0
- package/dist/relay-worker.d.ts.map +1 -1
- package/dist/relay-worker.js +433 -83
- package/dist/sw/handler-endpoint.d.ts +5 -2
- package/dist/sw/handler-endpoint.d.ts.map +1 -1
- package/dist/sw/http-sw-dispatcher.d.ts +114 -7
- package/dist/sw/http-sw-dispatcher.d.ts.map +1 -1
- package/dist/sw/index.d.ts +1 -0
- package/dist/sw/index.d.ts.map +1 -1
- package/dist/sw/protocol.d.ts +3 -0
- package/dist/sw/protocol.d.ts.map +1 -1
- package/dist/sw/sw-dispatcher.d.ts +5 -2
- package/dist/sw/sw-dispatcher.d.ts.map +1 -1
- package/dist/sw/sw-http-adapter.d.ts +19 -4
- package/dist/sw/sw-http-adapter.d.ts.map +1 -1
- package/dist/sw/worker-service.d.ts +5 -2
- package/dist/sw/worker-service.d.ts.map +1 -1
- package/dist/sw-worker.js +596 -132
- package/dist/sw.js +829 -223
- package/dist/worker.js +622 -120
- package/dist/ws/frames.d.ts +6 -1
- package/dist/ws/frames.d.ts.map +1 -1
- package/dist/ws/server.d.ts +50 -2
- package/dist/ws/server.d.ts.map +1 -1
- package/dist/ws/socket-core.d.ts +5 -2
- package/dist/ws/socket-core.d.ts.map +1 -1
- package/dist/ws/upgrade.d.ts +12 -1
- package/dist/ws/upgrade.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/core/index.ts +1 -0
- package/src/core/service-worker-control.ts +5 -3
- package/src/core/sw-scope.ts +27 -0
- package/src/link/http-link.ts +20 -4
- package/src/relay/index-sw.ts +15 -2
- package/src/relay/index.ts +2 -2
- package/src/relay-worker.ts +1 -0
- package/src/sw/handler-endpoint.ts +17 -4
- package/src/sw/http-sw-dispatcher.ts +267 -67
- package/src/sw/index.ts +5 -0
- package/src/sw/protocol.ts +11 -1
- package/src/sw/sw-dispatcher.ts +17 -11
- package/src/sw/sw-http-adapter.ts +36 -10
- package/src/sw/worker-service.ts +11 -4
- package/src/ws/frames.ts +18 -12
- package/src/ws/server.ts +166 -11
- package/src/ws/socket-core.ts +7 -2
- 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
|
-
|
|
1276
|
-
|
|
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
|
});
|
|
@@ -1746,7 +1747,7 @@ const LAST_CHUNK = /* @__PURE__ */ new Uint8Array([
|
|
|
1746
1747
|
13,
|
|
1747
1748
|
10
|
|
1748
1749
|
]);
|
|
1749
|
-
const encoder = /* @__PURE__ */ new TextEncoder();
|
|
1750
|
+
const encoder$2 = /* @__PURE__ */ new TextEncoder();
|
|
1750
1751
|
/**
|
|
1751
1752
|
* Chunk sizes are written in HEXADECIMAL — a 21-byte chunk is `15`. Writing
|
|
1752
1753
|
* them in decimal is the defect note 15 found in @libp2p/http, and it is the
|
|
@@ -1759,7 +1760,7 @@ const encoder = /* @__PURE__ */ new TextEncoder();
|
|
|
1759
1760
|
async function* encodeChunked(body) {
|
|
1760
1761
|
for await (const chunk of body) {
|
|
1761
1762
|
if (chunk.byteLength === 0) continue;
|
|
1762
|
-
yield encoder.encode(`${chunk.byteLength.toString(16)}\r\n`);
|
|
1763
|
+
yield encoder$2.encode(`${chunk.byteLength.toString(16)}\r\n`);
|
|
1763
1764
|
yield chunk;
|
|
1764
1765
|
yield CRLF;
|
|
1765
1766
|
}
|
|
@@ -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$1 = "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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
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
|
|
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$1 ? 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 (
|
|
3448
|
-
|
|
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$1 || 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$1,
|
|
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
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
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
|
-
|
|
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
|
|
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
|
});
|
|
@@ -4731,20 +5104,24 @@ function postFrame(port, frame) {
|
|
|
4731
5104
|
function messageSize(data) {
|
|
4732
5105
|
return typeof data === "string" ? utf8Length(data) : data.byteLength;
|
|
4733
5106
|
}
|
|
4734
|
-
|
|
5107
|
+
const encoder = new TextEncoder();
|
|
5108
|
+
/** Characters encoded per `encodeInto` call: at most 3 bytes each, so the scratch stays at 96 KiB. */
|
|
5109
|
+
const CHUNK = 32768;
|
|
5110
|
+
const scratch = new Uint8Array(CHUNK * 3);
|
|
5111
|
+
/**
|
|
5112
|
+
* The UTF-8 length of `text`, as `TextEncoder` would encode it (a lone surrogate counts as the 3
|
|
5113
|
+
* bytes of U+FFFD). Every message is measured on both ends (`bufferedAmount` and the byte acks), so
|
|
5114
|
+
* this runs natively: `encodeInto` a reused buffer, a chunk at a time, never splitting a surrogate
|
|
5115
|
+
* pair across two chunks. A loop over the characters in JavaScript took about 30 times as long.
|
|
5116
|
+
*/
|
|
4735
5117
|
function utf8Length(text) {
|
|
4736
5118
|
let length = 0;
|
|
4737
|
-
for (let
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
if (next >= 56320 && next <= 57343) {
|
|
4744
|
-
length += 4;
|
|
4745
|
-
i++;
|
|
4746
|
-
} else length += 3;
|
|
4747
|
-
} else length += 3;
|
|
5119
|
+
for (let start = 0; start < text.length;) {
|
|
5120
|
+
let end = Math.min(start + CHUNK, text.length);
|
|
5121
|
+
const last = text.charCodeAt(end - 1);
|
|
5122
|
+
if (end < text.length && last >= 55296 && last <= 56319) end--;
|
|
5123
|
+
length += encoder.encodeInto(text.slice(start, end), scratch).written;
|
|
5124
|
+
start = end;
|
|
4748
5125
|
}
|
|
4749
5126
|
return length;
|
|
4750
5127
|
}
|
|
@@ -4900,6 +5277,7 @@ var PortSocketCore = class extends EventTarget {
|
|
|
4900
5277
|
code,
|
|
4901
5278
|
reason
|
|
4902
5279
|
});
|
|
5280
|
+
if (!Number.isFinite(this._closeTimeoutMs)) return;
|
|
4903
5281
|
this._closeTimer = this._timers.setTimeout(() => this._finish(1006, "", false), this._closeTimeoutMs);
|
|
4904
5282
|
}
|
|
4905
5283
|
_onCloseFrame(code, reason) {
|
|
@@ -5210,7 +5588,8 @@ function errorMessage(error) {
|
|
|
5210
5588
|
* `whenDrained(limit)` waits for it to fall: the backpressure a producer
|
|
5211
5589
|
* applies.
|
|
5212
5590
|
* - When the client's pings stop for three heartbeat periods (a closed tab, a
|
|
5213
|
-
* terminated worker), the socket closes with 1006.
|
|
5591
|
+
* terminated worker), the socket closes with 1006. With `watch: "first-ping"`
|
|
5592
|
+
* the watch starts only once the client pings.
|
|
5214
5593
|
*/
|
|
5215
5594
|
var ServerWebSocket = class extends PortSocketCore {
|
|
5216
5595
|
static CONNECTING = 0;
|
|
@@ -5219,55 +5598,150 @@ var ServerWebSocket = class extends PortSocketCore {
|
|
|
5219
5598
|
static CLOSED = 3;
|
|
5220
5599
|
url;
|
|
5221
5600
|
protocol;
|
|
5601
|
+
_binaryType = "arraybuffer";
|
|
5602
|
+
_options;
|
|
5222
5603
|
_accepted = false;
|
|
5604
|
+
_detached = false;
|
|
5223
5605
|
_queue = [];
|
|
5224
5606
|
_heartbeatMs = DEFAULT_HEARTBEAT_MS;
|
|
5225
5607
|
_lastPing = Date.now();
|
|
5608
|
+
/** Whether a ping (with a period a timer takes) arrived already. */
|
|
5609
|
+
_pinged = false;
|
|
5226
5610
|
_watch;
|
|
5227
5611
|
constructor(port, options = {}) {
|
|
5228
5612
|
super({
|
|
5229
5613
|
timers: options.timers,
|
|
5230
5614
|
closeTimeoutMs: options.closeTimeoutMs
|
|
5231
5615
|
});
|
|
5616
|
+
this._options = options;
|
|
5232
5617
|
this.url = options.url ?? "";
|
|
5233
5618
|
this.protocol = options.protocol ?? "";
|
|
5234
5619
|
this._attach(port);
|
|
5235
5620
|
this._state = 1;
|
|
5236
|
-
this._watchHeartbeat();
|
|
5621
|
+
if (options.watch !== "first-ping") this._watchHeartbeat();
|
|
5237
5622
|
}
|
|
5238
5623
|
/** Whether `accept()` was called. */
|
|
5239
5624
|
get accepted() {
|
|
5240
5625
|
return this._accepted;
|
|
5241
5626
|
}
|
|
5627
|
+
/** How binary messages arrive: `ArrayBuffer`s (the default) or `Blob`s. Other values are ignored. */
|
|
5628
|
+
get binaryType() {
|
|
5629
|
+
return this._binaryType;
|
|
5630
|
+
}
|
|
5631
|
+
set binaryType(value) {
|
|
5632
|
+
if (value === "blob" || value === "arraybuffer") this._binaryType = value;
|
|
5633
|
+
}
|
|
5634
|
+
/**
|
|
5635
|
+
* Hands this end's transport to another holder before it was accepted: the port to give the
|
|
5636
|
+
* client (a pair's end returned in a 101, say), with the frames that reached this end first
|
|
5637
|
+
* delivered ahead of the rest. This end is closed then, with no event, and answers nothing.
|
|
5638
|
+
*/
|
|
5639
|
+
detach() {
|
|
5640
|
+
const port = this._port;
|
|
5641
|
+
if (!port || this._accepted || this._state !== 1) throw new DOMException("Only an open end that was not accepted can be detached.", "InvalidStateError");
|
|
5642
|
+
this._detached = true;
|
|
5643
|
+
this._port = void 0;
|
|
5644
|
+
port.onmessage = null;
|
|
5645
|
+
let owed = this.bufferedAmount;
|
|
5646
|
+
this._state = 3;
|
|
5647
|
+
this._onClosed();
|
|
5648
|
+
this._wakeDrainWaiters();
|
|
5649
|
+
const queued = this._queue.splice(0);
|
|
5650
|
+
if (queued.length === 0 && owed === 0) return port;
|
|
5651
|
+
const relay = new MessageChannel();
|
|
5652
|
+
const holder = relay.port2;
|
|
5653
|
+
const closing = /* @__PURE__ */ new Set();
|
|
5654
|
+
const end = () => {
|
|
5655
|
+
port.onmessage = null;
|
|
5656
|
+
holder.onmessage = null;
|
|
5657
|
+
port.close();
|
|
5658
|
+
holder.close();
|
|
5659
|
+
};
|
|
5660
|
+
const sawClose = (way) => {
|
|
5661
|
+
closing.add(way);
|
|
5662
|
+
if (closing.size === 2) end();
|
|
5663
|
+
};
|
|
5664
|
+
for (const frame of queued) {
|
|
5665
|
+
postFrame(holder, frame);
|
|
5666
|
+
if (frame.t === "c") closing.add("in");
|
|
5667
|
+
}
|
|
5668
|
+
holder.onmessage = (event) => {
|
|
5669
|
+
port.postMessage(event.data, transferables(event.data));
|
|
5670
|
+
if (event.data?.t === "c") sawClose("out");
|
|
5671
|
+
};
|
|
5672
|
+
port.onmessage = (event) => {
|
|
5673
|
+
const data = event.data;
|
|
5674
|
+
if (data?.t === "a" && typeof data.n === "number" && owed > 0) {
|
|
5675
|
+
const absorbed = Math.min(owed, data.n);
|
|
5676
|
+
owed -= absorbed;
|
|
5677
|
+
if (data.n > absorbed) holder.postMessage({
|
|
5678
|
+
t: "a",
|
|
5679
|
+
n: data.n - absorbed
|
|
5680
|
+
});
|
|
5681
|
+
return;
|
|
5682
|
+
}
|
|
5683
|
+
holder.postMessage(event.data, transferables(event.data));
|
|
5684
|
+
if (data?.t === "c") sawClose("in");
|
|
5685
|
+
};
|
|
5686
|
+
port.addEventListener?.("close", end);
|
|
5687
|
+
holder.addEventListener?.("close", end);
|
|
5688
|
+
return relay.port1;
|
|
5689
|
+
}
|
|
5242
5690
|
/** Starts delivering events, the queued ones first. */
|
|
5243
5691
|
accept() {
|
|
5692
|
+
if (this._detached) throw new DOMException("This end was detached; it cannot be accepted.", "InvalidStateError");
|
|
5244
5693
|
if (this._accepted) return;
|
|
5245
5694
|
this._accepted = true;
|
|
5246
|
-
for (const frame of this._queue.splice(0))
|
|
5695
|
+
for (const frame of this._queue.splice(0)) this._deliver(frame);
|
|
5247
5696
|
}
|
|
5248
5697
|
send(data) {
|
|
5249
|
-
if (
|
|
5698
|
+
if (this._detached) throw new DOMException("This end was detached; it cannot send.", "InvalidStateError");
|
|
5699
|
+
if (!this._accepted && !this._options.sendBeforeAccept) throw new DOMException("accept() the socket before sending on it.", "InvalidStateError");
|
|
5250
5700
|
this._sendData(isBinary(data) ? toArrayBuffer(data) : String(data));
|
|
5251
5701
|
}
|
|
5252
|
-
close(code
|
|
5253
|
-
|
|
5702
|
+
close(code, reason = "") {
|
|
5703
|
+
this.checkClose(code, reason);
|
|
5704
|
+
this._startClose(code ?? this._options.defaultCloseCode ?? 1e3, reason);
|
|
5705
|
+
}
|
|
5706
|
+
/**
|
|
5707
|
+
* Refuses a `close()` the endpoint may not make: by default a code outside RFC 6455 §7.4's
|
|
5708
|
+
* sendable ones, or a reason over 123 bytes. A subclass with another platform's rules
|
|
5709
|
+
* overrides it.
|
|
5710
|
+
*/
|
|
5711
|
+
checkClose(code, reason) {
|
|
5712
|
+
if (code !== void 0 && !isSendableCloseCode(code)) throw new DOMException(`${code} is not a close code an endpoint may send.`, "InvalidAccessError");
|
|
5254
5713
|
if (utf8Length(reason) > 123) throw new DOMException("The close reason is longer than 123 bytes.", "SyntaxError");
|
|
5255
|
-
this._startClose(code, reason);
|
|
5256
5714
|
}
|
|
5257
5715
|
_deliver(frame) {
|
|
5258
5716
|
if (!this._accepted) {
|
|
5259
5717
|
this._queue.push(frame);
|
|
5260
5718
|
return;
|
|
5261
5719
|
}
|
|
5720
|
+
if (frame.t === "m" && this._state === 2 && this._options.deliverWhileClosing) {
|
|
5721
|
+
this._dispatchMessage(frame.d);
|
|
5722
|
+
this._consumed(messageSize(frame.d));
|
|
5723
|
+
return;
|
|
5724
|
+
}
|
|
5262
5725
|
super._deliver(frame);
|
|
5263
5726
|
}
|
|
5264
5727
|
_dispatchMessage(data) {
|
|
5265
|
-
this.
|
|
5728
|
+
const delivered = typeof data !== "string" && this._binaryType === "blob" ? new Blob([data]) : data;
|
|
5729
|
+
this.dispatchEvent(new MessageEvent("message", { data: delivered }));
|
|
5266
5730
|
}
|
|
5267
5731
|
_onPing(hb) {
|
|
5268
5732
|
this._post({ t: "q" });
|
|
5269
5733
|
this._lastPing = Date.now();
|
|
5270
|
-
if (
|
|
5734
|
+
if (this._state === 3) return;
|
|
5735
|
+
const keep = this._options.heartbeatPeriod === "first" && this._pinged;
|
|
5736
|
+
if (!(hb > 0 && hb < 2 ** 31 - 1)) {
|
|
5737
|
+
if (!keep && this._watch !== void 0) {
|
|
5738
|
+
this._timers.clearInterval(this._watch);
|
|
5739
|
+
this._watch = void 0;
|
|
5740
|
+
}
|
|
5741
|
+
return;
|
|
5742
|
+
}
|
|
5743
|
+
this._pinged = true;
|
|
5744
|
+
if (!keep && (this._watch === void 0 || hb !== this._heartbeatMs)) {
|
|
5271
5745
|
this._heartbeatMs = hb;
|
|
5272
5746
|
this._watchHeartbeat();
|
|
5273
5747
|
}
|
|
@@ -5289,6 +5763,11 @@ function isSendableCloseCode(code) {
|
|
|
5289
5763
|
if (code >= 3e3 && code <= 4999) return true;
|
|
5290
5764
|
return code >= 1e3 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006;
|
|
5291
5765
|
}
|
|
5766
|
+
/** The buffers a relayed frame carries, to transfer rather than copy. */
|
|
5767
|
+
function transferables(data) {
|
|
5768
|
+
const d = data?.d;
|
|
5769
|
+
return d instanceof ArrayBuffer ? [d] : [];
|
|
5770
|
+
}
|
|
5292
5771
|
//#endregion
|
|
5293
5772
|
//#region src/ws/upgrade.ts
|
|
5294
5773
|
/**
|
|
@@ -5320,6 +5799,7 @@ function newWebSocketUpgradeRequest(url, { protocols = [], headers } = {}) {
|
|
|
5320
5799
|
all.set("connection", "Upgrade");
|
|
5321
5800
|
all.set("sec-websocket-version", "13");
|
|
5322
5801
|
if (!all.has("sec-websocket-key")) all.set("sec-websocket-key", randomKey());
|
|
5802
|
+
all.delete("sec-websocket-protocol");
|
|
5323
5803
|
if (protocols.length > 0) all.set("sec-websocket-protocol", protocols.join(", "));
|
|
5324
5804
|
if (!all.has("x-webrun-ws-version")) all.set(WS_VERSION_HEADER, `2`);
|
|
5325
5805
|
return withHeaders(new Request(httpUrl, {
|
|
@@ -5379,6 +5859,23 @@ function upgradeWebSocket(request, options = {}) {
|
|
|
5379
5859
|
url: request.url,
|
|
5380
5860
|
protocol
|
|
5381
5861
|
});
|
|
5862
|
+
const response = webSocketUpgradeResponse(channel.port2, protocol);
|
|
5863
|
+
Object.defineProperty(response, "webSocket", {
|
|
5864
|
+
value: socket,
|
|
5865
|
+
configurable: true
|
|
5866
|
+
});
|
|
5867
|
+
return {
|
|
5868
|
+
socket,
|
|
5869
|
+
response
|
|
5870
|
+
};
|
|
5871
|
+
}
|
|
5872
|
+
/**
|
|
5873
|
+
* A 101 for a socket whose server end the handler runs itself, on the other end of `port`
|
|
5874
|
+
* (a server of its own over this package's frames, say): what `upgradeWebSocket` returns, minus
|
|
5875
|
+
* the `ServerWebSocket`. The host hands `port` to the caller as it would for `upgradeWebSocket`
|
|
5876
|
+
* (`takePort`), and `protocol` (`""` for none) is the subprotocol picked.
|
|
5877
|
+
*/
|
|
5878
|
+
function webSocketUpgradeResponse(port, protocol = "") {
|
|
5382
5879
|
const response = new Response(null, {
|
|
5383
5880
|
status: 200,
|
|
5384
5881
|
headers: protocol ? { "sec-websocket-protocol": protocol } : {}
|
|
@@ -5392,23 +5889,16 @@ function upgradeWebSocket(request, options = {}) {
|
|
|
5392
5889
|
value: "Switching Protocols",
|
|
5393
5890
|
configurable: true
|
|
5394
5891
|
},
|
|
5395
|
-
webSocket: {
|
|
5396
|
-
value: socket,
|
|
5397
|
-
configurable: true
|
|
5398
|
-
},
|
|
5399
5892
|
[UPGRADE]: {
|
|
5400
5893
|
value: {
|
|
5401
|
-
port
|
|
5894
|
+
port,
|
|
5402
5895
|
protocol
|
|
5403
5896
|
},
|
|
5404
5897
|
configurable: true,
|
|
5405
5898
|
writable: true
|
|
5406
5899
|
}
|
|
5407
5900
|
});
|
|
5408
|
-
return
|
|
5409
|
-
socket,
|
|
5410
|
-
response
|
|
5411
|
-
};
|
|
5901
|
+
return response;
|
|
5412
5902
|
}
|
|
5413
5903
|
/**
|
|
5414
5904
|
* The port to hand to the caller, for a response from `upgradeWebSocket`.
|
|
@@ -5632,7 +6122,7 @@ async function initHttpService(handler, { key, path, port, streamTimeout }) {
|
|
|
5632
6122
|
* resolves with the corresponding `Response`, whose body streams with
|
|
5633
6123
|
* backpressure.
|
|
5634
6124
|
*/
|
|
5635
|
-
async function callHttpService(request, { key, port, streamTimeout }) {
|
|
6125
|
+
async function callHttpService(request, { key, port, streamTimeout, window }) {
|
|
5636
6126
|
const channel = new MessageChannel();
|
|
5637
6127
|
const reply = await callChannel(port, "CONNECT", {
|
|
5638
6128
|
key,
|
|
@@ -5643,7 +6133,10 @@ async function callHttpService(request, { key, port, streamTimeout }) {
|
|
|
5643
6133
|
channel.port1.close();
|
|
5644
6134
|
throw new Error(`No service with key "${key}"`);
|
|
5645
6135
|
}
|
|
5646
|
-
return await sendOverConnection(channel.port1, reply, request, {
|
|
6136
|
+
return await sendOverConnection(channel.port1, reply, request, {
|
|
6137
|
+
streamTimeout,
|
|
6138
|
+
window
|
|
6139
|
+
});
|
|
5647
6140
|
}
|
|
5648
6141
|
/**
|
|
5649
6142
|
* Sends `request` on a port a service accepted with `reply`: over an HTTP link
|
|
@@ -5867,6 +6360,51 @@ function handleKeyedChannelCalls(target, callType, key, handler) {
|
|
|
5867
6360
|
return () => target.removeEventListener("message", listener);
|
|
5868
6361
|
}
|
|
5869
6362
|
//#endregion
|
|
6363
|
+
//#region src/sw/protocol.ts
|
|
6364
|
+
/**
|
|
6365
|
+
* The messages between pages, Dedicated Workers and the same-origin
|
|
6366
|
+
* ServiceWorker. Internal: the shapes may change between versions, which is
|
|
6367
|
+
* why every hand-shake carries `HTTP_LINK_PROTOCOL`.
|
|
6368
|
+
*
|
|
6369
|
+
* THREE PORTS PER HANDLER LINK. A page makes the channels and hands one end of
|
|
6370
|
+
* each to the handler (itself, or a worker) and the other to the
|
|
6371
|
+
* ServiceWorker; after that it carries nothing:
|
|
6372
|
+
*
|
|
6373
|
+
* - `down`: ServiceWorker → handler, the requests the handler serves;
|
|
6374
|
+
* - `ctl`: a raw port next to them, for what HTTP over a stream cannot carry,
|
|
6375
|
+
* transferred `MessagePort`s (WebSocket hand-over);
|
|
6376
|
+
* - `up` (workers only): handler → ServiceWorker, the requests the handler
|
|
6377
|
+
* sends through the router (to other handlers).
|
|
6378
|
+
*/
|
|
6379
|
+
/** Page → ServiceWorker: attach a handler link. The name is the one version 1 used. */
|
|
6380
|
+
const ATTACH_CALL = "UPDATE_COMMUNICATION_PORT";
|
|
6381
|
+
/** Page → ServiceWorker: remove a handler link now. */
|
|
6382
|
+
const DETACH_CALL = "webrun-http:detach";
|
|
6383
|
+
/** Page → ServiceWorker: stop routing to a link, let its requests finish, then remove it. */
|
|
6384
|
+
const DRAIN_CALL = "webrun-http:drain";
|
|
6385
|
+
/** Page → ServiceWorker: open a WebSocket to a handler. */
|
|
6386
|
+
const WS_CONNECT_CALL = "webrun-http:ws-connect";
|
|
6387
|
+
/** ServiceWorker → pages: attach your links again (the ServiceWorker restarted). */
|
|
6388
|
+
const NEED_LINKS_CALL = "webrun-http:need-links";
|
|
6389
|
+
/**
|
|
6390
|
+
* Page → ServiceWorker, plain messages (no reply). A message to the
|
|
6391
|
+
* ServiceWorker object is an event: it starts a stopped worker and resets its
|
|
6392
|
+
* idle timer. Port messages do neither.
|
|
6393
|
+
*/
|
|
6394
|
+
const WAKE_MESSAGE = "webrun-http:wake";
|
|
6395
|
+
const KEEPALIVE_MESSAGE = "webrun-http:keepalive";
|
|
6396
|
+
/** Page ↔ worker: what `attachWorker` and `serveHttpInWorker` exchange. */
|
|
6397
|
+
const WORKER_HELLO = "webrun-http:worker-hello";
|
|
6398
|
+
/** `headers` as pairs a `postMessage` can carry (a `Headers` object cannot be cloned). */
|
|
6399
|
+
function headerPairs(headers) {
|
|
6400
|
+
if (headers === void 0) return void 0;
|
|
6401
|
+
const pairs = [];
|
|
6402
|
+
new Headers(headers).forEach((value, name) => {
|
|
6403
|
+
pairs.push([name, value]);
|
|
6404
|
+
});
|
|
6405
|
+
return pairs;
|
|
6406
|
+
}
|
|
6407
|
+
//#endregion
|
|
5870
6408
|
//#region src/sw/handler-endpoint.ts
|
|
5871
6409
|
/** The message calls through a lost ServiceWorker fail with. */
|
|
5872
6410
|
const ROUTER_LOST_MESSAGE = "The ServiceWorker was lost while this call was in flight";
|
|
@@ -5950,8 +6488,11 @@ var HandlerEndpoint = class {
|
|
|
5950
6488
|
this._startKeepAlive();
|
|
5951
6489
|
return await link.up.send(request);
|
|
5952
6490
|
}
|
|
5953
|
-
/**
|
|
5954
|
-
|
|
6491
|
+
/**
|
|
6492
|
+
* Opens a socket to the handler `url` routes to, through the ServiceWorker. The handler sees
|
|
6493
|
+
* `headers` on the upgrade request, and so does a `route` function choosing it.
|
|
6494
|
+
*/
|
|
6495
|
+
async connectWebSocket(url, protocols = [], headers) {
|
|
5955
6496
|
const link = await this._linkWith("ctl");
|
|
5956
6497
|
const id = this._sockets.newId();
|
|
5957
6498
|
const answer = this._sockets.wait(id, this.options.wsConnectTimeoutMs ?? 15e3, `the socket request for ${url}`);
|
|
@@ -5959,7 +6500,8 @@ var HandlerEndpoint = class {
|
|
|
5959
6500
|
t: "ws-open",
|
|
5960
6501
|
id,
|
|
5961
6502
|
url,
|
|
5962
|
-
protocols
|
|
6503
|
+
protocols,
|
|
6504
|
+
headers: headerPairs(headers)
|
|
5963
6505
|
});
|
|
5964
6506
|
return await answer;
|
|
5965
6507
|
}
|
|
@@ -6199,43 +6741,22 @@ function set(key, value, customStore = defaultGetStore()) {
|
|
|
6199
6741
|
});
|
|
6200
6742
|
}
|
|
6201
6743
|
//#endregion
|
|
6202
|
-
//#region src/sw/
|
|
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";
|
|
6744
|
+
//#region src/sw/http-sw-dispatcher.ts
|
|
6228
6745
|
/**
|
|
6229
|
-
*
|
|
6230
|
-
*
|
|
6231
|
-
* idle timer. Port messages do neither.
|
|
6746
|
+
* The default key: the url's first path segment under `scope`, decoded; none outside it, at the
|
|
6747
|
+
* scope itself, or for a segment that does not decode.
|
|
6232
6748
|
*/
|
|
6233
|
-
|
|
6234
|
-
const
|
|
6235
|
-
|
|
6236
|
-
const
|
|
6237
|
-
|
|
6238
|
-
|
|
6749
|
+
function scopeKey(url, scope) {
|
|
6750
|
+
const href = `${url}`;
|
|
6751
|
+
if (!href.startsWith(scope)) return void 0;
|
|
6752
|
+
const key = href.substring(scope.length).replace(/[?#].*$/, "").split("/")[0];
|
|
6753
|
+
if (!key) return void 0;
|
|
6754
|
+
try {
|
|
6755
|
+
return decodeURIComponent(key);
|
|
6756
|
+
} catch {
|
|
6757
|
+
return;
|
|
6758
|
+
}
|
|
6759
|
+
}
|
|
6239
6760
|
/** One attached handler link, as the ServiceWorker holds it. */
|
|
6240
6761
|
var HandlerEntry = class {
|
|
6241
6762
|
key;
|
|
@@ -6275,7 +6796,8 @@ const TIMED_OUT = Symbol("timed-out");
|
|
|
6275
6796
|
* (`SwHttpAdapter.register`) or in Dedicated Workers
|
|
6276
6797
|
* (`SwHttpAdapter.attachWorker` + `serveHttpInWorker`).
|
|
6277
6798
|
*
|
|
6278
|
-
* A url `<scope>/<key>/...` belongs to the handlers of `key
|
|
6799
|
+
* A url `<scope>/<key>/...` belongs to the handlers of `key` (the `key`
|
|
6800
|
+
* option maps urls to keys some other way). Of those, the
|
|
6279
6801
|
* `route` option picks one per request. Requests and responses stream with
|
|
6280
6802
|
* backpressure in both directions, and an aborted fetch reaches the handler
|
|
6281
6803
|
* as a cancellation (webrun-rpc + webrun-http-streams).
|
|
@@ -6294,6 +6816,8 @@ const TIMED_OUT = Symbol("timed-out");
|
|
|
6294
6816
|
*/
|
|
6295
6817
|
var SwHttpDispatcher = class {
|
|
6296
6818
|
self;
|
|
6819
|
+
/** `self`, with the WebWorker lib's types this module compiles with. */
|
|
6820
|
+
_sw;
|
|
6297
6821
|
log;
|
|
6298
6822
|
options;
|
|
6299
6823
|
table = new HandlerTable();
|
|
@@ -6316,19 +6840,22 @@ var SwHttpDispatcher = class {
|
|
|
6316
6840
|
constructor(options) {
|
|
6317
6841
|
this.options = options;
|
|
6318
6842
|
this.self = options.self;
|
|
6843
|
+
this._sw = options.self;
|
|
6319
6844
|
this.log = options.log ?? (() => {});
|
|
6320
6845
|
if (locksAvailable()) this.routerLock = newLockName("webrun-http-router");
|
|
6321
6846
|
}
|
|
6322
6847
|
get scope() {
|
|
6323
|
-
return `${new URL("./", this.
|
|
6848
|
+
return this._sw.registration?.scope ?? `${new URL("./", this._sw.location.href)}`;
|
|
6324
6849
|
}
|
|
6325
|
-
/** The key `url` belongs to
|
|
6850
|
+
/** The key `url` belongs to (the `key` option; by default its first path segment under the scope). */
|
|
6326
6851
|
keyOf(url) {
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6852
|
+
let parsed;
|
|
6853
|
+
try {
|
|
6854
|
+
parsed = new URL(`${url}`);
|
|
6855
|
+
} catch {
|
|
6856
|
+
return;
|
|
6857
|
+
}
|
|
6858
|
+
return (this.options.key ?? scopeKey)(parsed, this.scope);
|
|
6332
6859
|
}
|
|
6333
6860
|
/** Whether `key` has ever been served on this worker. */
|
|
6334
6861
|
isClaimedKey(key) {
|
|
@@ -6339,7 +6866,7 @@ var SwHttpDispatcher = class {
|
|
|
6339
6866
|
return this.claimedKeys.has(key) || this.table.all(key).length > 0;
|
|
6340
6867
|
}
|
|
6341
6868
|
start() {
|
|
6342
|
-
const self = this.
|
|
6869
|
+
const self = this._sw;
|
|
6343
6870
|
if (this.routerLock) this._routerHeld = holdLock(this.routerLock).then(() => {});
|
|
6344
6871
|
this._restored = this._restoreClaimedKeys();
|
|
6345
6872
|
const latch = () => {
|
|
@@ -6359,8 +6886,8 @@ var SwHttpDispatcher = class {
|
|
|
6359
6886
|
return true;
|
|
6360
6887
|
}), handleChannelCalls(self, WS_CONNECT_CALL, async (event, params) => {
|
|
6361
6888
|
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 ?? []);
|
|
6889
|
+
const { url, protocols, headers } = params;
|
|
6890
|
+
const connection = await this.connectWebSocket(url, protocols ?? [], this._socketHeaders(headers, "page"));
|
|
6364
6891
|
return withTransfer(connection, [connection.port]);
|
|
6365
6892
|
}));
|
|
6366
6893
|
const onMessage = (event) => {
|
|
@@ -6378,10 +6905,10 @@ var SwHttpDispatcher = class {
|
|
|
6378
6905
|
this.log("Claim control over all clients.");
|
|
6379
6906
|
event.waitUntil(self.clients.claim());
|
|
6380
6907
|
};
|
|
6381
|
-
const onFetch = (event) => this.
|
|
6908
|
+
const onFetch = (event) => void this.handleFetch(event);
|
|
6382
6909
|
self.addEventListener("install", onInstall);
|
|
6383
6910
|
self.addEventListener("activate", onActivate);
|
|
6384
|
-
self.addEventListener("fetch", onFetch);
|
|
6911
|
+
if (this.options.handleFetch !== false) self.addEventListener("fetch", onFetch);
|
|
6385
6912
|
this._cleanup.push(() => {
|
|
6386
6913
|
self.removeEventListener("install", onInstall);
|
|
6387
6914
|
self.removeEventListener("activate", onActivate);
|
|
@@ -6396,37 +6923,60 @@ var SwHttpDispatcher = class {
|
|
|
6396
6923
|
}
|
|
6397
6924
|
/**
|
|
6398
6925
|
* Answers `request` as a fetch under the scope would be: through the
|
|
6399
|
-
* handler its key routes to. A url
|
|
6400
|
-
* worker never served, goes to the network.
|
|
6926
|
+
* handler its key routes to. A url with no key (by default, one outside the
|
|
6927
|
+
* scope), or of a key this worker never served, goes to the network.
|
|
6401
6928
|
*/
|
|
6402
6929
|
async dispatch(request) {
|
|
6403
6930
|
const key = this.keyOf(request.url);
|
|
6404
6931
|
if (key === void 0) return await fetch(request);
|
|
6405
6932
|
await this._restored.catch(() => {});
|
|
6406
6933
|
const entry = await this._acquire(request, key);
|
|
6407
|
-
if (entry === TIMED_OUT) return
|
|
6408
|
-
status: 503,
|
|
6409
|
-
statusText: "Service Unavailable (no handler attached in time)"
|
|
6410
|
-
});
|
|
6934
|
+
if (entry === TIMED_OUT) return await this._failed(request, { kind: "timeout" });
|
|
6411
6935
|
if (!entry) {
|
|
6412
6936
|
if (!this.serves(key)) return await fetch(request);
|
|
6413
|
-
return
|
|
6414
|
-
status: 404,
|
|
6415
|
-
statusText: "Not Found (no active handler)"
|
|
6416
|
-
});
|
|
6937
|
+
return await this._failed(request, { kind: "gone" });
|
|
6417
6938
|
}
|
|
6418
6939
|
try {
|
|
6419
6940
|
return await entry.link.send(request);
|
|
6420
6941
|
} catch (error) {
|
|
6421
6942
|
this.log("[dispatch] failed", key, error);
|
|
6422
|
-
return
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
6943
|
+
return await this._failed(request, {
|
|
6944
|
+
kind: "lost",
|
|
6945
|
+
error
|
|
6426
6946
|
});
|
|
6427
6947
|
}
|
|
6428
6948
|
}
|
|
6429
6949
|
/**
|
|
6950
|
+
* After `start()`: settles once the keys this worker served before a restart are read back
|
|
6951
|
+
* (whatever the outcome). Until then `serves()` may answer `false` for a key it will serve.
|
|
6952
|
+
*/
|
|
6953
|
+
get ready() {
|
|
6954
|
+
return this._restored.then(() => {}, () => {});
|
|
6955
|
+
}
|
|
6956
|
+
_socketHeaders(headers, from) {
|
|
6957
|
+
return this.options.socketHeaders ? this.options.socketHeaders(headers, from) : headers;
|
|
6958
|
+
}
|
|
6959
|
+
async _failed(request, failure) {
|
|
6960
|
+
if (this.options.failure) try {
|
|
6961
|
+
return await this.options.failure(request, failure);
|
|
6962
|
+
} catch (error) {
|
|
6963
|
+
this.log("[dispatch] the failure option threw; answering by default", error);
|
|
6964
|
+
}
|
|
6965
|
+
if (failure.kind === "timeout") return new Response(null, {
|
|
6966
|
+
status: 503,
|
|
6967
|
+
statusText: "Service Unavailable (no handler attached in time)"
|
|
6968
|
+
});
|
|
6969
|
+
if (failure.kind === "gone") return new Response(null, {
|
|
6970
|
+
status: 404,
|
|
6971
|
+
statusText: "Not Found (no active handler)"
|
|
6972
|
+
});
|
|
6973
|
+
return new Response(errorMessage(failure.error), {
|
|
6974
|
+
status: 502,
|
|
6975
|
+
statusText: "Bad Gateway",
|
|
6976
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
6977
|
+
});
|
|
6978
|
+
}
|
|
6979
|
+
/**
|
|
6430
6980
|
* Opens a socket to the handler `url` routes to: the handler accepts or
|
|
6431
6981
|
* refuses it like an HTTP upgrade, and the socket's port comes back. After
|
|
6432
6982
|
* that the socket runs between the caller and the handler directly; this
|
|
@@ -6438,7 +6988,7 @@ var SwHttpDispatcher = class {
|
|
|
6438
6988
|
headers
|
|
6439
6989
|
});
|
|
6440
6990
|
const key = this.keyOf(request.url);
|
|
6441
|
-
if (key === void 0) throw new Error(`${url}
|
|
6991
|
+
if (key === void 0) throw new Error(`${url} belongs to no key of this ServiceWorker`);
|
|
6442
6992
|
await this._restored.catch(() => {});
|
|
6443
6993
|
const entry = await this._acquire(request, key);
|
|
6444
6994
|
if (!entry || entry === TIMED_OUT) throw new Error(`No handler serves ${url}`);
|
|
@@ -6454,20 +7004,47 @@ var SwHttpDispatcher = class {
|
|
|
6454
7004
|
});
|
|
6455
7005
|
return await answer;
|
|
6456
7006
|
}
|
|
6457
|
-
|
|
6458
|
-
|
|
7007
|
+
/**
|
|
7008
|
+
* Answers `event` if it is the dispatcher's, with `request` (default `event.request`; a host
|
|
7009
|
+
* may pass a rewritten one, with headers it adds). Returns whether it called `respondWith`;
|
|
7010
|
+
* when it did not, the event is the caller's to answer or to leave to the network. For a host
|
|
7011
|
+
* that listens to `fetch` itself (`handleFetch: false`); call it synchronously in the event's
|
|
7012
|
+
* listener, as `respondWith` must be.
|
|
7013
|
+
*
|
|
7014
|
+
* It takes a same-origin request whose key (the `key` option) a handler serves, and also:
|
|
7015
|
+
* - a navigation under the scope whose key nobody serves (answered by `unserved`);
|
|
7016
|
+
* - before the stored keys are read (the first requests after a restart), every request with
|
|
7017
|
+
* a key, since only then is it known whose it is: one nobody serves gets what it would have
|
|
7018
|
+
* got after, `unserved` for a navigation and the network for anything else.
|
|
7019
|
+
* So a host with a page of its own for urls nobody serves gives it as `unserved`, rather than
|
|
7020
|
+
* deciding before calling this.
|
|
7021
|
+
*/
|
|
7022
|
+
handleFetch(event, request = event.request) {
|
|
7023
|
+
const navigation = event.request.mode === "navigate";
|
|
7024
|
+
if (new URL(request.url).origin !== this._sw.location.origin) return false;
|
|
6459
7025
|
const key = this.keyOf(request.url);
|
|
6460
|
-
if (key === void 0) return;
|
|
7026
|
+
if (key === void 0) return false;
|
|
6461
7027
|
if (this._restoreSettled) {
|
|
6462
|
-
if (this.serves(key))
|
|
6463
|
-
|
|
6464
|
-
|
|
7028
|
+
if (this.serves(key)) {
|
|
7029
|
+
event.respondWith(this.dispatch(request));
|
|
7030
|
+
return true;
|
|
7031
|
+
}
|
|
7032
|
+
if (navigation) {
|
|
7033
|
+
event.respondWith(this._unserved(request));
|
|
7034
|
+
return true;
|
|
7035
|
+
}
|
|
7036
|
+
return false;
|
|
6465
7037
|
}
|
|
6466
7038
|
event.respondWith((async () => {
|
|
6467
7039
|
await this._restored.catch(() => {});
|
|
6468
|
-
if (
|
|
6469
|
-
return await this.
|
|
7040
|
+
if (this.serves(key)) return await this.dispatch(request);
|
|
7041
|
+
return navigation ? await this._unserved(request) : await fetch(request);
|
|
6470
7042
|
})());
|
|
7043
|
+
return true;
|
|
7044
|
+
}
|
|
7045
|
+
/** What a request the dispatcher took, and no handler serves, gets: `unserved`, or the network. */
|
|
7046
|
+
async _unserved(request) {
|
|
7047
|
+
return await (this.options.unserved ?? fetch)(request);
|
|
6471
7048
|
}
|
|
6472
7049
|
async _attach(event, params, ports) {
|
|
6473
7050
|
const clientId = event.source?.id;
|
|
@@ -6486,8 +7063,11 @@ var SwHttpDispatcher = class {
|
|
|
6486
7063
|
}
|
|
6487
7064
|
const ctl = params.ctl ? rest.shift() : void 0;
|
|
6488
7065
|
const up = params.up ? rest.shift() : void 0;
|
|
6489
|
-
const streamTimeout = this.options
|
|
6490
|
-
const link = await connectHttpLink(down, {
|
|
7066
|
+
const { streamTimeout, window } = this.options;
|
|
7067
|
+
const link = await connectHttpLink(down, {
|
|
7068
|
+
streamTimeout,
|
|
7069
|
+
window
|
|
7070
|
+
});
|
|
6491
7071
|
const entry = new HandlerEntry(params.key, params.id ?? clientId ?? crypto.randomUUID(), params.owner ?? crypto.randomUUID(), params.meta, link, ctl, clientId, params.ownerLock);
|
|
6492
7072
|
if (up) entry.closeUp = await serveHttpLink(up, (request) => this.dispatch(request), { streamTimeout });
|
|
6493
7073
|
if (ctl) ctl.onmessage = (message) => this._onControl(entry, message.data);
|
|
@@ -6566,20 +7146,35 @@ var SwHttpDispatcher = class {
|
|
|
6566
7146
|
* its key, waiting for one when there is none.
|
|
6567
7147
|
*/
|
|
6568
7148
|
async _acquire(request, key) {
|
|
7149
|
+
const deadline = Date.now() + (this.options.attachTimeoutMs ?? 15e3);
|
|
6569
7150
|
const picked = await this._pick(request, key);
|
|
6570
7151
|
if (picked) return picked;
|
|
6571
|
-
if (this.table.all(key).length
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
7152
|
+
if (this.table.all(key).length === 0) {
|
|
7153
|
+
const vacantUntil = this._vacant.get(key) ?? 0;
|
|
7154
|
+
if (vacantUntil > Date.now()) return await this._waitToPick(request, key, vacantUntil);
|
|
7155
|
+
if (!this.serves(key)) return void 0;
|
|
7156
|
+
await this._askForLinks(key);
|
|
7157
|
+
const relinked = await this._pick(request, key);
|
|
7158
|
+
if (relinked) return relinked;
|
|
7159
|
+
if (this.table.all(key).length === 0) return void 0;
|
|
7160
|
+
}
|
|
7161
|
+
return await this._waitToPick(request, key, deadline);
|
|
7162
|
+
}
|
|
7163
|
+
/** Waits for attaches until `route` accepts a handler, or `until` passes. */
|
|
7164
|
+
async _waitToPick(request, key, until) {
|
|
7165
|
+
for (;;) {
|
|
7166
|
+
const left = until - Date.now();
|
|
7167
|
+
if (left <= 0) return TIMED_OUT;
|
|
7168
|
+
await this.table.waitFor(key, Math.min(left, 1e3), this.table.live(key).length > 0);
|
|
7169
|
+
const picked = await this._pick(request, key);
|
|
7170
|
+
if (picked) return picked;
|
|
7171
|
+
}
|
|
6577
7172
|
}
|
|
6578
7173
|
async _pick(request, key) {
|
|
6579
7174
|
let live = this.table.live(key);
|
|
6580
7175
|
for (const entry of live) {
|
|
6581
7176
|
if (entry.ownerLock || !entry.clientId) continue;
|
|
6582
|
-
if (!await this.
|
|
7177
|
+
if (!await this._sw.clients.get(entry.clientId)) this._close(entry, new LinkLostError(HANDLER_LOST_MESSAGE));
|
|
6583
7178
|
}
|
|
6584
7179
|
live = this.table.live(key);
|
|
6585
7180
|
if (live.length === 0) return void 0;
|
|
@@ -6603,7 +7198,7 @@ var SwHttpDispatcher = class {
|
|
|
6603
7198
|
}
|
|
6604
7199
|
async _linkRound(key) {
|
|
6605
7200
|
const timeout = this.options.relinkTimeoutMs ?? 3e3;
|
|
6606
|
-
const windows = await this.
|
|
7201
|
+
const windows = await this._sw.clients.matchAll({
|
|
6607
7202
|
type: "window",
|
|
6608
7203
|
includeUncontrolled: true
|
|
6609
7204
|
});
|
|
@@ -6641,7 +7236,7 @@ var SwHttpDispatcher = class {
|
|
|
6641
7236
|
entry.sockets.fail(message.id, new Error(message.message));
|
|
6642
7237
|
return;
|
|
6643
7238
|
case "ws-open":
|
|
6644
|
-
this.connectWebSocket(message.url, message.protocols).then(({ port, protocol }) => entry.ctl?.postMessage({
|
|
7239
|
+
this.connectWebSocket(message.url, message.protocols, this._socketHeaders(message.headers, "handler")).then(({ port, protocol }) => entry.ctl?.postMessage({
|
|
6645
7240
|
t: "ws-opened",
|
|
6646
7241
|
id: message.id,
|
|
6647
7242
|
protocol,
|
|
@@ -6658,7 +7253,7 @@ var SwHttpDispatcher = class {
|
|
|
6658
7253
|
const source = event.source;
|
|
6659
7254
|
if (!source?.url) return false;
|
|
6660
7255
|
try {
|
|
6661
|
-
return new URL(source.url).origin === this.
|
|
7256
|
+
return new URL(source.url).origin === this._sw.location.origin;
|
|
6662
7257
|
} catch {
|
|
6663
7258
|
return false;
|
|
6664
7259
|
}
|
|
@@ -6682,6 +7277,7 @@ var SwHttpDispatcher = class {
|
|
|
6682
7277
|
* that stops it.
|
|
6683
7278
|
*/
|
|
6684
7279
|
function startHttpDispatcher(options) {
|
|
7280
|
+
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
7281
|
const dispatcher = new SwHttpDispatcher(options);
|
|
6686
7282
|
dispatcher.start();
|
|
6687
7283
|
return () => {
|
|
@@ -6822,6 +7418,8 @@ var SwPortHandler = class {
|
|
|
6822
7418
|
*/
|
|
6823
7419
|
var SwPortDispatcher = class {
|
|
6824
7420
|
self;
|
|
7421
|
+
/** `self`, with the WebWorker lib's types this module compiles with. */
|
|
7422
|
+
_sw;
|
|
6825
7423
|
log;
|
|
6826
7424
|
handlersIndex = /* @__PURE__ */ new Map();
|
|
6827
7425
|
/**
|
|
@@ -6835,10 +7433,11 @@ var SwPortDispatcher = class {
|
|
|
6835
7433
|
_activationPromise;
|
|
6836
7434
|
constructor({ self, log = () => {} }) {
|
|
6837
7435
|
this.self = self;
|
|
7436
|
+
this._sw = self;
|
|
6838
7437
|
this.log = log;
|
|
6839
7438
|
}
|
|
6840
7439
|
get scope() {
|
|
6841
|
-
return `${new URL("./", this.
|
|
7440
|
+
return this._sw.registration?.scope ?? `${new URL("./", this._sw.location.href)}`;
|
|
6842
7441
|
}
|
|
6843
7442
|
/**
|
|
6844
7443
|
* Look up the active channel for a key. Returns `undefined` when there
|
|
@@ -6851,7 +7450,7 @@ var SwPortDispatcher = class {
|
|
|
6851
7450
|
if (channelInfo.key !== key) continue;
|
|
6852
7451
|
const clientId = channelInfo.clientId;
|
|
6853
7452
|
if (!clientId) continue;
|
|
6854
|
-
if (!await this.
|
|
7453
|
+
if (!await this._sw.clients.get(clientId)) {
|
|
6855
7454
|
this.handlersIndex.delete(clientId);
|
|
6856
7455
|
continue;
|
|
6857
7456
|
}
|
|
@@ -6863,8 +7462,8 @@ var SwPortDispatcher = class {
|
|
|
6863
7462
|
return this.claimedKeys.has(key);
|
|
6864
7463
|
}
|
|
6865
7464
|
start() {
|
|
6866
|
-
const stopClaims = handleClaimRequests(this.
|
|
6867
|
-
const stopPortUpdates = handleChannelCalls(this.
|
|
7465
|
+
const stopClaims = handleClaimRequests(this._sw);
|
|
7466
|
+
const stopPortUpdates = handleChannelCalls(this._sw, "UPDATE_COMMUNICATION_PORT", async (event, channelInfo, port) => {
|
|
6868
7467
|
const clientId = event.source?.id;
|
|
6869
7468
|
this.log("[UPDATE_COMMUNICATION_PORT]", clientId, channelInfo);
|
|
6870
7469
|
await this._updateChannelInfo({
|
|
@@ -6878,14 +7477,14 @@ var SwPortDispatcher = class {
|
|
|
6878
7477
|
stopClaims();
|
|
6879
7478
|
stopPortUpdates();
|
|
6880
7479
|
};
|
|
6881
|
-
this.
|
|
7480
|
+
this._sw.addEventListener("install", (event) => {
|
|
6882
7481
|
this.log("Skip waiting on install.", event);
|
|
6883
|
-
this.
|
|
7482
|
+
this._sw.skipWaiting();
|
|
6884
7483
|
});
|
|
6885
|
-
this.
|
|
7484
|
+
this._sw.addEventListener("activate", (event) => {
|
|
6886
7485
|
this.log("Claim control over all clients.", event);
|
|
6887
7486
|
event.waitUntil((async () => {
|
|
6888
|
-
await this.
|
|
7487
|
+
await this._sw.clients.claim();
|
|
6889
7488
|
await this._checkActivation();
|
|
6890
7489
|
})());
|
|
6891
7490
|
});
|
|
@@ -6935,7 +7534,7 @@ var SwPortDispatcher = class {
|
|
|
6935
7534
|
for (const clientId of this.handlersIndex.keys()) ids.add(clientId);
|
|
6936
7535
|
const index = /* @__PURE__ */ new Map();
|
|
6937
7536
|
for (const clientId of ids) {
|
|
6938
|
-
const client = await this.
|
|
7537
|
+
const client = await this._sw.clients.get(clientId);
|
|
6939
7538
|
if (client) index.set(clientId, client);
|
|
6940
7539
|
else this.handlersIndex.delete(clientId);
|
|
6941
7540
|
}
|
|
@@ -6988,7 +7587,10 @@ var SwHttpAdapter = class {
|
|
|
6988
7587
|
return this.options.key;
|
|
6989
7588
|
}
|
|
6990
7589
|
get scope() {
|
|
6991
|
-
|
|
7590
|
+
if (this.options.scope) return this.options.scope;
|
|
7591
|
+
const registration = this.options.registration;
|
|
7592
|
+
if (registration) return new URL(registration.scope).pathname;
|
|
7593
|
+
return new URL("./", this.serviceWorkerUrl).pathname;
|
|
6992
7594
|
}
|
|
6993
7595
|
/**
|
|
6994
7596
|
* The scope as a url on the worker's origin. Resolved against the worker's
|
|
@@ -6999,6 +7601,8 @@ var SwHttpAdapter = class {
|
|
|
6999
7601
|
* sits next to it).
|
|
7000
7602
|
*/
|
|
7001
7603
|
get rootUrl() {
|
|
7604
|
+
const registration = this.options.registration;
|
|
7605
|
+
if (registration && !this.options.serviceWorkerUrl) return new URL(this.scope, registration.scope);
|
|
7002
7606
|
const base = this.options.serviceWorkerUrl ? this.serviceWorkerUrl : import.meta.url;
|
|
7003
7607
|
return new URL(this.scope, base);
|
|
7004
7608
|
}
|
|
@@ -7020,7 +7624,10 @@ var SwHttpAdapter = class {
|
|
|
7020
7624
|
*/
|
|
7021
7625
|
async start() {
|
|
7022
7626
|
if (!this._registrationPromise) this._registrationPromise = (async () => {
|
|
7023
|
-
const registration = await navigator.serviceWorker.register(this.serviceWorkerUrl, {
|
|
7627
|
+
const registration = this.options.registration ?? await navigator.serviceWorker.register(this.serviceWorkerUrl, {
|
|
7628
|
+
scope: this.scope,
|
|
7629
|
+
...this.options.type ? { type: this.options.type } : {}
|
|
7630
|
+
});
|
|
7024
7631
|
this._registration = registration;
|
|
7025
7632
|
const stopListening = handleChannelCalls(navigator.serviceWorker, NEED_LINKS_CALL, async (_event, params) => {
|
|
7026
7633
|
await this.relink(params?.routerLock);
|
|
@@ -7050,7 +7657,7 @@ var SwHttpAdapter = class {
|
|
|
7050
7657
|
})();
|
|
7051
7658
|
return this._registrationPromise;
|
|
7052
7659
|
}
|
|
7053
|
-
/** Detaches every link, and unregisters the worker. */
|
|
7660
|
+
/** Detaches every link, and unregisters the worker (unless the page passed its `registration`). */
|
|
7054
7661
|
async stop() {
|
|
7055
7662
|
for (const cleanup of this._cleanup.splice(0)) cleanup();
|
|
7056
7663
|
for (const worker of [...this._workers]) await worker.detach().catch(() => {});
|
|
@@ -7067,7 +7674,7 @@ var SwHttpAdapter = class {
|
|
|
7067
7674
|
this._registration = void 0;
|
|
7068
7675
|
this._registrationPromise = void 0;
|
|
7069
7676
|
try {
|
|
7070
|
-
await registration?.unregister();
|
|
7677
|
+
if (!this.options.registration) await registration?.unregister();
|
|
7071
7678
|
} catch (error) {
|
|
7072
7679
|
console.log("Service Worker unregistration failed: ", error);
|
|
7073
7680
|
}
|
|
@@ -7141,10 +7748,10 @@ var SwHttpAdapter = class {
|
|
|
7141
7748
|
/**
|
|
7142
7749
|
* Opens the transport of a socket to the handler `url` routes to: a handler
|
|
7143
7750
|
* of this page directly, any other through the ServiceWorker. Resolves once
|
|
7144
|
-
* the handler accepted.
|
|
7145
|
-
* `createWebSocketClass(adapter.connectWebSocket)`.
|
|
7751
|
+
* the handler accepted. The handler sees `headers` on the upgrade request. An arrow function,
|
|
7752
|
+
* so it can be handed around: `createWebSocketClass(adapter.connectWebSocket)`.
|
|
7146
7753
|
*/
|
|
7147
|
-
connectWebSocket = async (url, protocols = []) => {
|
|
7754
|
+
connectWebSocket = async (url, protocols = [], headers) => {
|
|
7148
7755
|
const httpUrl = new URL(`${url}`, globalThis.location?.href);
|
|
7149
7756
|
if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
|
|
7150
7757
|
else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
|
|
@@ -7152,7 +7759,8 @@ var SwHttpAdapter = class {
|
|
|
7152
7759
|
if (local) {
|
|
7153
7760
|
const result = await serveWebSocketUpgrade(local, {
|
|
7154
7761
|
url: `${httpUrl}`,
|
|
7155
|
-
protocols
|
|
7762
|
+
protocols,
|
|
7763
|
+
headers
|
|
7156
7764
|
});
|
|
7157
7765
|
if (!result.ok) throw new Error(result.message);
|
|
7158
7766
|
return {
|
|
@@ -7163,7 +7771,8 @@ var SwHttpAdapter = class {
|
|
|
7163
7771
|
await this.start();
|
|
7164
7772
|
return await callChannelWithin(this._callTimeout(), this._worker(), WS_CONNECT_CALL, {
|
|
7165
7773
|
url: `${httpUrl}`,
|
|
7166
|
-
protocols
|
|
7774
|
+
protocols,
|
|
7775
|
+
headers: headerPairs(headers)
|
|
7167
7776
|
});
|
|
7168
7777
|
};
|
|
7169
7778
|
/**
|
|
@@ -7459,11 +8068,11 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
7459
8068
|
};
|
|
7460
8069
|
scope.addEventListener("message", onMessage);
|
|
7461
8070
|
const baseUrl = () => globalThis.location?.href;
|
|
7462
|
-
const connectWebSocket = (url, protocols = []) => {
|
|
8071
|
+
const connectWebSocket = (url, protocols = [], headers) => {
|
|
7463
8072
|
const httpUrl = new URL(`${url}`, baseUrl());
|
|
7464
8073
|
if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
|
|
7465
8074
|
else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
|
|
7466
|
-
return endpoint.connectWebSocket(`${httpUrl}`, protocols);
|
|
8075
|
+
return endpoint.connectWebSocket(`${httpUrl}`, protocols, headers);
|
|
7467
8076
|
};
|
|
7468
8077
|
let WebSocketClass;
|
|
7469
8078
|
return {
|
|
@@ -7490,4 +8099,4 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
7490
8099
|
};
|
|
7491
8100
|
}
|
|
7492
8101
|
//#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 };
|
|
8102
|
+
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 };
|