@statewalker/webrun-http-browser 0.7.1 → 0.9.0
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/dist/index.js +620 -70
- package/dist/relay-sw.js +606 -88
- package/dist/relay-worker.js +606 -88
- package/dist/sw/protocol.d.ts +2 -0
- package/dist/sw/protocol.d.ts.map +1 -1
- package/dist/sw/sw-http-adapter.d.ts +2 -0
- package/dist/sw/sw-http-adapter.d.ts.map +1 -1
- package/dist/sw/worker-service.d.ts.map +1 -1
- package/dist/sw-worker.js +606 -68
- package/dist/sw.js +620 -70
- package/dist/worker.js +615 -69
- package/package.json +3 -3
- package/src/link/http-link.ts +42 -6
- package/src/sw/protocol.ts +4 -3
- package/src/sw/sw-http-adapter.ts +7 -0
- package/src/sw/worker-service.ts +14 -1
package/dist/relay-worker.js
CHANGED
|
@@ -1367,26 +1367,6 @@ async function* readableToAsyncIterable(stream) {
|
|
|
1367
1367
|
} catch {}
|
|
1368
1368
|
}
|
|
1369
1369
|
}
|
|
1370
|
-
function asyncIterableToReadable(iter) {
|
|
1371
|
-
const it = iter[Symbol.asyncIterator]();
|
|
1372
|
-
return new ReadableStream({
|
|
1373
|
-
async pull(controller) {
|
|
1374
|
-
try {
|
|
1375
|
-
const { done, value } = await it.next();
|
|
1376
|
-
if (done) {
|
|
1377
|
-
controller.close();
|
|
1378
|
-
return;
|
|
1379
|
-
}
|
|
1380
|
-
controller.enqueue(value);
|
|
1381
|
-
} catch (err) {
|
|
1382
|
-
controller.error(err);
|
|
1383
|
-
}
|
|
1384
|
-
},
|
|
1385
|
-
async cancel() {
|
|
1386
|
-
await it.return?.();
|
|
1387
|
-
}
|
|
1388
|
-
});
|
|
1389
|
-
}
|
|
1390
1370
|
/**
|
|
1391
1371
|
* Run a `Request` through a `Duplex` call and reconstruct the `Response` on
|
|
1392
1372
|
* the other side. The request's `signal` is plumbed into the body iteration —
|
|
@@ -1419,17 +1399,80 @@ async function fetchOverDuplex(call, request, options = {}) {
|
|
|
1419
1399
|
else await release?.();
|
|
1420
1400
|
return new Response(null, responseInit);
|
|
1421
1401
|
}
|
|
1422
|
-
return new Response(
|
|
1402
|
+
return new Response(responseBody(respBody, request.signal, release), responseInit);
|
|
1423
1403
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1404
|
+
/**
|
|
1405
|
+
* How long a cancelled body waits for the read in flight before it abandons the call anyway.
|
|
1406
|
+
* The read's answer tells a body that has ended (the call is finished: an upload the handler
|
|
1407
|
+
* reads after answering goes on) from one that has not; a peer that sends nothing more never
|
|
1408
|
+
* answers it. A body whose end comes later than this after the cancel is abandoned too, so a
|
|
1409
|
+
* handler still streaming its response and still reading the upload then loses the upload.
|
|
1410
|
+
*/
|
|
1411
|
+
const CANCEL_GRACE_MS = 1e3;
|
|
1412
|
+
/**
|
|
1413
|
+
* The response body as a stream that the request's signal can end at any time.
|
|
1414
|
+
*
|
|
1415
|
+
* An abort errors the stream with the abort's reason at once — a read waiting
|
|
1416
|
+
* for a peer that sends nothing more rejects then, not when a next chunk
|
|
1417
|
+
* arrives, which may be never — and abandons the call (`release`), so on a
|
|
1418
|
+
* multiplexed transport its port is given back whether or not anyone still
|
|
1419
|
+
* reads the body. The listener is removed once the body ends, however it ends:
|
|
1420
|
+
* read to its end, cancelled, failed or aborted. A body nobody reads or cancels
|
|
1421
|
+
* keeps it until the signal aborts (and keeps the call open until then anyway).
|
|
1422
|
+
* A cancel abandons the call too — unless the read in flight comes back with the
|
|
1423
|
+
* body's end, which finishes it instead (see `cancel`).
|
|
1424
|
+
*/
|
|
1425
|
+
function responseBody(body, signal, release) {
|
|
1426
|
+
const it = body[Symbol.asyncIterator]();
|
|
1427
|
+
let controller;
|
|
1428
|
+
let ended = false;
|
|
1429
|
+
let reading = false;
|
|
1430
|
+
const end = () => {
|
|
1431
|
+
ended = true;
|
|
1432
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1433
|
+
};
|
|
1434
|
+
const onAbort = () => {
|
|
1435
|
+
if (ended) return;
|
|
1436
|
+
end();
|
|
1437
|
+
try {
|
|
1438
|
+
controller.error(abortReason$1(signal));
|
|
1439
|
+
} catch {}
|
|
1440
|
+
release?.();
|
|
1441
|
+
};
|
|
1442
|
+
return new ReadableStream({
|
|
1443
|
+
start(c) {
|
|
1444
|
+
controller = c;
|
|
1445
|
+
if (signal?.aborted) onAbort();
|
|
1446
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
1447
|
+
},
|
|
1448
|
+
async pull(c) {
|
|
1449
|
+
try {
|
|
1450
|
+
reading = true;
|
|
1451
|
+
const { done, value } = await it.next().finally(() => {
|
|
1452
|
+
reading = false;
|
|
1453
|
+
});
|
|
1454
|
+
if (ended) return;
|
|
1455
|
+
if (done) {
|
|
1456
|
+
end();
|
|
1457
|
+
c.close();
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
c.enqueue(value);
|
|
1461
|
+
} catch (err) {
|
|
1462
|
+
if (ended) return;
|
|
1463
|
+
end();
|
|
1464
|
+
c.error(err);
|
|
1465
|
+
}
|
|
1466
|
+
},
|
|
1467
|
+
cancel() {
|
|
1468
|
+
end();
|
|
1469
|
+
const returned = Promise.resolve(it.return?.()).catch(() => {});
|
|
1470
|
+
if (!reading) return;
|
|
1471
|
+
const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
|
|
1472
|
+
grace.unref?.();
|
|
1473
|
+
returned.finally(() => clearTimeout(grace));
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1433
1476
|
}
|
|
1434
1477
|
var HttpError = class HttpError extends Error {
|
|
1435
1478
|
status;
|
|
@@ -1774,8 +1817,11 @@ function listenPort(port, handler, { channelName = "", log = () => {} } = {}) {
|
|
|
1774
1817
|
error
|
|
1775
1818
|
});
|
|
1776
1819
|
};
|
|
1777
|
-
|
|
1778
|
-
|
|
1820
|
+
const listener = (event) => {
|
|
1821
|
+
onMessage(event);
|
|
1822
|
+
};
|
|
1823
|
+
port.addEventListener("message", listener);
|
|
1824
|
+
return () => port.removeEventListener("message", listener);
|
|
1779
1825
|
}
|
|
1780
1826
|
/**
|
|
1781
1827
|
* Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
|
|
@@ -2121,6 +2167,33 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
|
|
|
2121
2167
|
}
|
|
2122
2168
|
}
|
|
2123
2169
|
/**
|
|
2170
|
+
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
2171
|
+
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
2172
|
+
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
2173
|
+
* are clamped to 4 ms in browsers.
|
|
2174
|
+
*/
|
|
2175
|
+
const setImmediateFn = globalThis.setImmediate;
|
|
2176
|
+
/** @internal */
|
|
2177
|
+
const nextTask = setImmediateFn ? (fn) => {
|
|
2178
|
+
setImmediateFn(fn);
|
|
2179
|
+
} : (() => {
|
|
2180
|
+
let channel;
|
|
2181
|
+
const queue = [];
|
|
2182
|
+
return (fn) => {
|
|
2183
|
+
if (!channel) {
|
|
2184
|
+
channel = new MessageChannel();
|
|
2185
|
+
channel.port1.onmessage = () => queue.shift()?.();
|
|
2186
|
+
}
|
|
2187
|
+
queue.push(fn);
|
|
2188
|
+
channel.port2.postMessage(0);
|
|
2189
|
+
};
|
|
2190
|
+
})();
|
|
2191
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
2192
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
2193
|
+
function setPortSendState(port, state) {
|
|
2194
|
+
states.set(port, state);
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2124
2197
|
* One virtual port.
|
|
2125
2198
|
*
|
|
2126
2199
|
* `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
|
|
@@ -2135,6 +2208,14 @@ function newVirtualPort(send, requestClose) {
|
|
|
2135
2208
|
if (closing.signal.aborted) return;
|
|
2136
2209
|
closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
|
|
2137
2210
|
};
|
|
2211
|
+
const close = (reason) => {
|
|
2212
|
+
if (closed) return;
|
|
2213
|
+
closed = true;
|
|
2214
|
+
const notify = requestClose;
|
|
2215
|
+
listeners.clear();
|
|
2216
|
+
notify(reason);
|
|
2217
|
+
abort(reason);
|
|
2218
|
+
};
|
|
2138
2219
|
const port = {
|
|
2139
2220
|
addEventListener(_type, listener) {
|
|
2140
2221
|
listeners.add(listener);
|
|
@@ -2146,24 +2227,28 @@ function newVirtualPort(send, requestClose) {
|
|
|
2146
2227
|
if (closed) return;
|
|
2147
2228
|
send(message, transfer);
|
|
2148
2229
|
},
|
|
2149
|
-
close
|
|
2150
|
-
if (closed) return;
|
|
2151
|
-
closed = true;
|
|
2152
|
-
const notify = requestClose;
|
|
2153
|
-
listeners.clear();
|
|
2154
|
-
notify();
|
|
2155
|
-
abort();
|
|
2156
|
-
}
|
|
2230
|
+
close
|
|
2157
2231
|
};
|
|
2158
2232
|
setPortCloseSignal(port, closing.signal);
|
|
2233
|
+
const dispose = Symbol.dispose;
|
|
2234
|
+
if (dispose) Object.defineProperty(port, dispose, {
|
|
2235
|
+
value: () => close(),
|
|
2236
|
+
configurable: true
|
|
2237
|
+
});
|
|
2159
2238
|
return {
|
|
2160
2239
|
port,
|
|
2161
2240
|
deliver(payload) {
|
|
2162
2241
|
if (closed) return;
|
|
2163
2242
|
const event = new MessageEvent("message", { data: payload });
|
|
2243
|
+
let pending;
|
|
2164
2244
|
for (const listener of [...listeners]) try {
|
|
2165
|
-
listener(event);
|
|
2245
|
+
const result = listener(event);
|
|
2246
|
+
if (result && typeof result.then === "function") {
|
|
2247
|
+
pending ??= [];
|
|
2248
|
+
pending.push(result);
|
|
2249
|
+
}
|
|
2166
2250
|
} catch {}
|
|
2251
|
+
return pending ? Promise.allSettled(pending) : void 0;
|
|
2167
2252
|
},
|
|
2168
2253
|
markClosed(reason) {
|
|
2169
2254
|
closed = true;
|
|
@@ -2172,53 +2257,309 @@ function newVirtualPort(send, requestClose) {
|
|
|
2172
2257
|
},
|
|
2173
2258
|
isClosed() {
|
|
2174
2259
|
return closed;
|
|
2260
|
+
},
|
|
2261
|
+
hasListeners() {
|
|
2262
|
+
return listeners.size > 0;
|
|
2175
2263
|
}
|
|
2176
2264
|
};
|
|
2177
2265
|
}
|
|
2178
2266
|
/**
|
|
2179
|
-
* The
|
|
2180
|
-
*
|
|
2267
|
+
* The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
|
|
2268
|
+
* whose codec passes unknown envelopes through reads them as a close of that
|
|
2269
|
+
* id, which must therefore not be a real port (0 is the initiator's first).
|
|
2181
2270
|
*/
|
|
2182
|
-
const
|
|
2271
|
+
const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
2272
|
+
/** The longest delay a timer holds; past it `setInterval` fires at once. */
|
|
2273
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
2183
2274
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
2184
2275
|
const DEFAULT_MAX_PORTS = 1024;
|
|
2185
2276
|
/**
|
|
2277
|
+
* What a message costs a port's window when the codec cannot say
|
|
2278
|
+
* (`PortCodec.encode`): its bytes and string lengths, found in its values up
|
|
2279
|
+
* to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
|
|
2280
|
+
* from the same structured value, so they agree.
|
|
2281
|
+
*/
|
|
2282
|
+
function estimateMessageSize(payload) {
|
|
2283
|
+
let budget = 256;
|
|
2284
|
+
const walk = (v, depth) => {
|
|
2285
|
+
if (budget-- <= 0) return 0;
|
|
2286
|
+
if (typeof v === "string") return v.length;
|
|
2287
|
+
if (v instanceof ArrayBuffer) return v.byteLength;
|
|
2288
|
+
if (ArrayBuffer.isView(v)) return v.byteLength;
|
|
2289
|
+
if (v === null || typeof v !== "object" || depth >= 4) return 8;
|
|
2290
|
+
let n = 16;
|
|
2291
|
+
if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
|
|
2292
|
+
else for (const k in v) n += k.length + walk(v[k], depth + 1);
|
|
2293
|
+
return n;
|
|
2294
|
+
};
|
|
2295
|
+
return Math.max(64, walk(payload, 0));
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2186
2298
|
* The default `PortMux`: emulates multiplexing over a single port.
|
|
2187
2299
|
*
|
|
2188
2300
|
* A transport that already multiplexes natively supplies its own `PortMux`
|
|
2189
2301
|
* instead — this implementation is for transports that offer one pipe.
|
|
2190
2302
|
*/
|
|
2191
2303
|
function multiplexPort(port, options) {
|
|
2192
|
-
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
|
|
2304
|
+
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
|
|
2305
|
+
const misses = keepalive?.misses ?? 3;
|
|
2306
|
+
if (keepalive && !(Number.isFinite(keepalive.intervalMs) && keepalive.intervalMs > 0 && keepalive.intervalMs <= MAX_TIMER_MS && Number.isSafeInteger(misses) && misses > 0)) throw new RangeError(`webrun-rpc: keepalive needs an intervalMs in (0, ${MAX_TIMER_MS}] and a positive integer misses`);
|
|
2307
|
+
if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
|
|
2193
2308
|
const open = /* @__PURE__ */ new Map();
|
|
2309
|
+
const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
|
|
2310
|
+
const entry = open.get(id);
|
|
2311
|
+
if (entry?.handle !== record) return;
|
|
2312
|
+
const { handle: _, credit: __, partial: ___, ...info } = entry;
|
|
2313
|
+
if (!record.hasListeners()) {
|
|
2314
|
+
release("leaked");
|
|
2315
|
+
record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
|
|
2316
|
+
}
|
|
2317
|
+
try {
|
|
2318
|
+
onLeak?.(info);
|
|
2319
|
+
} catch {}
|
|
2320
|
+
}) : void 0;
|
|
2321
|
+
const touch = (id) => {
|
|
2322
|
+
const entry = open.get(id);
|
|
2323
|
+
if (entry) entry.lastActivity = Date.now();
|
|
2324
|
+
};
|
|
2194
2325
|
let nextId = side === "initiator" ? 0 : 1;
|
|
2195
2326
|
let muxClosed = false;
|
|
2327
|
+
/** This end counts encoded bytes and reassembles frames. */
|
|
2328
|
+
const exactCodec = codec.encode != null && codec.decode != null;
|
|
2196
2329
|
const post = (envelope, transfer) => {
|
|
2197
2330
|
if (muxClosed) return;
|
|
2331
|
+
codecPost(envelope, transfer);
|
|
2332
|
+
};
|
|
2333
|
+
/** Like `post`, also while closing: the mux's own close notices. */
|
|
2334
|
+
const codecPost = (envelope, transfer) => {
|
|
2198
2335
|
try {
|
|
2199
2336
|
codec.post(port, envelope, transfer);
|
|
2200
2337
|
} catch {}
|
|
2201
2338
|
};
|
|
2202
|
-
const
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2339
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
2340
|
+
const ring = [];
|
|
2341
|
+
let pumping = false;
|
|
2342
|
+
const pump = async () => {
|
|
2343
|
+
pumping = true;
|
|
2344
|
+
try {
|
|
2345
|
+
while (ring.length && !muxClosed) {
|
|
2346
|
+
if (port.whenDrained) await port.whenDrained();
|
|
2347
|
+
if (muxClosed) break;
|
|
2348
|
+
const id = ring.shift();
|
|
2349
|
+
const queue = outgoing.get(id);
|
|
2350
|
+
const item = queue?.shift();
|
|
2351
|
+
if (queue?.length) ring.push(id);
|
|
2352
|
+
else outgoing.delete(id);
|
|
2353
|
+
item?.();
|
|
2354
|
+
}
|
|
2355
|
+
} catch (err) {
|
|
2356
|
+
close(err).catch(() => {});
|
|
2357
|
+
} finally {
|
|
2358
|
+
pumping = false;
|
|
2359
|
+
}
|
|
2360
|
+
};
|
|
2361
|
+
const sendOnPort = (id, item) => {
|
|
2362
|
+
if (frameSize == null) {
|
|
2363
|
+
item();
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
let queue = outgoing.get(id);
|
|
2367
|
+
if (!queue) {
|
|
2368
|
+
queue = [];
|
|
2369
|
+
outgoing.set(id, queue);
|
|
2370
|
+
ring.push(id);
|
|
2371
|
+
}
|
|
2372
|
+
queue.push(item);
|
|
2373
|
+
if (!pumping) pump();
|
|
2374
|
+
};
|
|
2375
|
+
const dropOutgoing = (id) => {
|
|
2376
|
+
if (!outgoing.delete(id)) return;
|
|
2377
|
+
const at = ring.indexOf(id);
|
|
2378
|
+
if (at >= 0) ring.splice(at, 1);
|
|
2379
|
+
};
|
|
2380
|
+
const postBytes = (bytes) => {
|
|
2381
|
+
if (muxClosed) return;
|
|
2382
|
+
try {
|
|
2383
|
+
port.postMessage(bytes);
|
|
2384
|
+
} catch {}
|
|
2385
|
+
};
|
|
2386
|
+
const releaseWaiters = (c) => {
|
|
2387
|
+
for (const w of c?.waiters.splice(0) ?? []) w.resolve();
|
|
2388
|
+
};
|
|
2389
|
+
const flush = (c) => {
|
|
2390
|
+
while (c.queue.length) {
|
|
2391
|
+
const next = c.queue[0];
|
|
2392
|
+
if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
2393
|
+
c.queue.shift();
|
|
2394
|
+
c.queued -= next.size;
|
|
2395
|
+
c.outstanding += next.size;
|
|
2396
|
+
next.send();
|
|
2397
|
+
}
|
|
2398
|
+
for (const w of [...c.waiters]) if (c.queued <= w.below) {
|
|
2399
|
+
c.waiters.splice(c.waiters.indexOf(w), 1);
|
|
2400
|
+
w.resolve();
|
|
2401
|
+
}
|
|
2402
|
+
};
|
|
2403
|
+
const grant = (id, c, bytes) => {
|
|
2404
|
+
c.unconsumed -= bytes;
|
|
2405
|
+
c.toGrant += bytes;
|
|
2406
|
+
const send = () => {
|
|
2407
|
+
c.grantScheduled = false;
|
|
2408
|
+
if (!c.toGrant || !open.has(id)) return;
|
|
2209
2409
|
post({
|
|
2410
|
+
type: "credit",
|
|
2411
|
+
id,
|
|
2412
|
+
bytes: c.toGrant
|
|
2413
|
+
});
|
|
2414
|
+
c.toGrant = 0;
|
|
2415
|
+
};
|
|
2416
|
+
if (c.toGrant >= c.window / 4) {
|
|
2417
|
+
send();
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
if (c.grantScheduled) return;
|
|
2421
|
+
c.grantScheduled = true;
|
|
2422
|
+
setTimeout(send, 0);
|
|
2423
|
+
};
|
|
2424
|
+
const attach = (id, meta, portWindow, opener, exact) => {
|
|
2425
|
+
const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
|
|
2426
|
+
window: portWindow,
|
|
2427
|
+
enforced: false,
|
|
2428
|
+
opener,
|
|
2429
|
+
exact,
|
|
2430
|
+
outstanding: 0,
|
|
2431
|
+
queue: [],
|
|
2432
|
+
queued: 0,
|
|
2433
|
+
unconsumed: 0,
|
|
2434
|
+
waiters: [],
|
|
2435
|
+
toGrant: 0,
|
|
2436
|
+
grantScheduled: false
|
|
2437
|
+
} : void 0;
|
|
2438
|
+
const self = newVirtualPort((payload, transfer) => {
|
|
2439
|
+
touch(id);
|
|
2440
|
+
const envelope = {
|
|
2441
|
+
type: "message",
|
|
2442
|
+
id,
|
|
2443
|
+
payload
|
|
2444
|
+
};
|
|
2445
|
+
if (!c) {
|
|
2446
|
+
post(envelope, transfer);
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
const bytes = c.exact ? codec.encode?.(envelope) : void 0;
|
|
2450
|
+
const size = bytes?.byteLength ?? estimateMessageSize(payload);
|
|
2451
|
+
const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
|
|
2452
|
+
const held = !bytes && waits ? {
|
|
2453
|
+
...envelope,
|
|
2454
|
+
payload: structuredClone(payload, { transfer })
|
|
2455
|
+
} : void 0;
|
|
2456
|
+
const send = () => {
|
|
2457
|
+
if (!bytes) {
|
|
2458
|
+
sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
if (frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
2462
|
+
for (let at = 0; at < bytes.byteLength; at += frameSize) {
|
|
2463
|
+
const data = bytes.subarray(at, at + frameSize);
|
|
2464
|
+
const frame = at + frameSize >= bytes.byteLength ? {
|
|
2465
|
+
type: "frame",
|
|
2466
|
+
id,
|
|
2467
|
+
data,
|
|
2468
|
+
last: true
|
|
2469
|
+
} : {
|
|
2470
|
+
type: "frame",
|
|
2471
|
+
id,
|
|
2472
|
+
data
|
|
2473
|
+
};
|
|
2474
|
+
sendOnPort(id, () => post(frame));
|
|
2475
|
+
}
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
sendOnPort(id, () => postBytes(bytes));
|
|
2479
|
+
};
|
|
2480
|
+
c.queue.push({
|
|
2481
|
+
send,
|
|
2482
|
+
size
|
|
2483
|
+
});
|
|
2484
|
+
c.queued += size;
|
|
2485
|
+
flush(c);
|
|
2486
|
+
}, (reason) => {
|
|
2487
|
+
release(reason);
|
|
2488
|
+
record.markClosed(reason);
|
|
2489
|
+
});
|
|
2490
|
+
function release(reason) {
|
|
2491
|
+
open.delete(id);
|
|
2492
|
+
leaks?.unregister(record);
|
|
2493
|
+
if (c?.queue.length) {
|
|
2494
|
+
for (const item of c.queue.splice(0)) item.send();
|
|
2495
|
+
c.queued = 0;
|
|
2496
|
+
}
|
|
2497
|
+
sendOnPort(id, () => post({
|
|
2210
2498
|
type: "close",
|
|
2211
2499
|
id,
|
|
2212
2500
|
reason
|
|
2501
|
+
}));
|
|
2502
|
+
releaseWaiters(c);
|
|
2503
|
+
}
|
|
2504
|
+
const { markClosed, isClosed, hasListeners } = self;
|
|
2505
|
+
if (c) {
|
|
2506
|
+
const deliver = self.deliver;
|
|
2507
|
+
self.deliver = (payload, size = estimateMessageSize(payload)) => {
|
|
2508
|
+
c.unconsumed += size;
|
|
2509
|
+
const done = deliver(payload);
|
|
2510
|
+
if (done) done.then(() => grant(id, c, size));
|
|
2511
|
+
else grant(id, c, size);
|
|
2512
|
+
return done;
|
|
2513
|
+
};
|
|
2514
|
+
setPortSendState(self.port, {
|
|
2515
|
+
bufferedAmount: () => c.queued,
|
|
2516
|
+
ready: (below = 0) => new Promise((resolve) => {
|
|
2517
|
+
if (c.queued <= below || isClosed()) nextTask(resolve);
|
|
2518
|
+
else c.waiters.push({
|
|
2519
|
+
below,
|
|
2520
|
+
resolve: () => nextTask(resolve)
|
|
2521
|
+
});
|
|
2522
|
+
})
|
|
2213
2523
|
});
|
|
2214
|
-
|
|
2215
|
-
|
|
2524
|
+
}
|
|
2525
|
+
const record = {
|
|
2526
|
+
deliver: self.deliver,
|
|
2527
|
+
markClosed,
|
|
2528
|
+
isClosed,
|
|
2529
|
+
hasListeners,
|
|
2530
|
+
close: (reason) => {
|
|
2531
|
+
if (isClosed()) return;
|
|
2532
|
+
release(reason);
|
|
2533
|
+
markClosed(reason);
|
|
2534
|
+
}
|
|
2535
|
+
};
|
|
2536
|
+
leaks?.register(self.port, {
|
|
2537
|
+
id,
|
|
2538
|
+
record,
|
|
2539
|
+
release
|
|
2540
|
+
}, record);
|
|
2541
|
+
const now = Date.now();
|
|
2542
|
+
open.set(id, {
|
|
2543
|
+
id,
|
|
2544
|
+
meta,
|
|
2545
|
+
openedAt: now,
|
|
2546
|
+
lastActivity: now,
|
|
2547
|
+
handle: record,
|
|
2548
|
+
credit: c
|
|
2216
2549
|
});
|
|
2217
|
-
open.set(id, self);
|
|
2218
2550
|
return self;
|
|
2219
2551
|
};
|
|
2220
|
-
const handleEnvelope = (envelope) => {
|
|
2552
|
+
const handleEnvelope = (envelope, event) => {
|
|
2221
2553
|
if (muxClosed) return;
|
|
2554
|
+
heardSincePing = true;
|
|
2555
|
+
if (envelope.type === "ping" || envelope.type === "pong") {
|
|
2556
|
+
peerKeepsAlive = true;
|
|
2557
|
+
if (envelope.type === "ping") post({
|
|
2558
|
+
type: "pong",
|
|
2559
|
+
id: KEEPALIVE_ID
|
|
2560
|
+
});
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2222
2563
|
if (envelope.type === "open") {
|
|
2223
2564
|
if (open.has(envelope.id)) return;
|
|
2224
2565
|
if (open.size >= maxPorts) {
|
|
@@ -2229,7 +2570,18 @@ function multiplexPort(port, options) {
|
|
|
2229
2570
|
});
|
|
2230
2571
|
return;
|
|
2231
2572
|
}
|
|
2232
|
-
const handle = attach(envelope.id);
|
|
2573
|
+
const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
|
|
2574
|
+
const credit = open.get(envelope.id)?.credit;
|
|
2575
|
+
if (credit) post(credit.exact ? {
|
|
2576
|
+
type: "credit",
|
|
2577
|
+
id: envelope.id,
|
|
2578
|
+
bytes: 0,
|
|
2579
|
+
exact: true
|
|
2580
|
+
} : {
|
|
2581
|
+
type: "credit",
|
|
2582
|
+
id: envelope.id,
|
|
2583
|
+
bytes: 0
|
|
2584
|
+
});
|
|
2233
2585
|
let accepted = false;
|
|
2234
2586
|
if (onPort) try {
|
|
2235
2587
|
accepted = onPort(handle.port, envelope.meta) !== false;
|
|
@@ -2244,57 +2596,191 @@ function multiplexPort(port, options) {
|
|
|
2244
2596
|
id: envelope.id,
|
|
2245
2597
|
reason: "rejected"
|
|
2246
2598
|
});
|
|
2599
|
+
return;
|
|
2247
2600
|
}
|
|
2248
2601
|
return;
|
|
2249
2602
|
}
|
|
2250
|
-
const
|
|
2603
|
+
const entry = open.get(envelope.id);
|
|
2604
|
+
if (envelope.type === "credit") {
|
|
2605
|
+
const c = entry?.credit;
|
|
2606
|
+
if (!c || !(envelope.bytes >= 0)) return;
|
|
2607
|
+
if (!c.enforced && c.opener) {
|
|
2608
|
+
if (c.exact && envelope.exact !== true) {
|
|
2609
|
+
c.exact = false;
|
|
2610
|
+
c.outstanding = 0;
|
|
2611
|
+
}
|
|
2612
|
+
post({
|
|
2613
|
+
type: "credit",
|
|
2614
|
+
id: envelope.id,
|
|
2615
|
+
bytes: 0
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
c.enforced = true;
|
|
2619
|
+
c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
|
|
2620
|
+
flush(c);
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2251
2623
|
if (envelope.type === "message") {
|
|
2252
|
-
|
|
2253
|
-
|
|
2624
|
+
const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
|
|
2625
|
+
if (!entry) return;
|
|
2626
|
+
touch(envelope.id);
|
|
2627
|
+
entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
|
|
2254
2628
|
return;
|
|
2255
2629
|
}
|
|
2256
|
-
if (
|
|
2630
|
+
if (envelope.type === "frame") {
|
|
2631
|
+
if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
|
|
2632
|
+
touch(envelope.id);
|
|
2633
|
+
const partial = entry.partial ?? {
|
|
2634
|
+
chunks: [],
|
|
2635
|
+
bytes: 0
|
|
2636
|
+
};
|
|
2637
|
+
entry.partial = partial;
|
|
2638
|
+
partial.chunks.push(envelope.data);
|
|
2639
|
+
partial.bytes += envelope.data.byteLength;
|
|
2640
|
+
if (!envelope.last) return;
|
|
2641
|
+
entry.partial = void 0;
|
|
2642
|
+
const whole = new Uint8Array(partial.bytes);
|
|
2643
|
+
let at = 0;
|
|
2644
|
+
for (const chunk of partial.chunks) {
|
|
2645
|
+
whole.set(chunk, at);
|
|
2646
|
+
at += chunk.byteLength;
|
|
2647
|
+
}
|
|
2648
|
+
const inner = codec.decode(whole);
|
|
2649
|
+
if (inner?.type !== "message" || inner.id !== envelope.id) return;
|
|
2650
|
+
entry.handle.deliver(inner.payload, whole.byteLength);
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
dropOutgoing(envelope.id);
|
|
2654
|
+
if (!entry) return;
|
|
2257
2655
|
open.delete(envelope.id);
|
|
2258
|
-
|
|
2656
|
+
releaseWaiters(entry.credit);
|
|
2657
|
+
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
|
|
2259
2658
|
};
|
|
2260
2659
|
const listener = (event) => {
|
|
2261
2660
|
const envelope = codec.read(event);
|
|
2262
|
-
if (envelope) handleEnvelope(envelope);
|
|
2661
|
+
if (envelope) handleEnvelope(envelope, event);
|
|
2662
|
+
};
|
|
2663
|
+
let peerKeepsAlive = false;
|
|
2664
|
+
let heardSincePing = true;
|
|
2665
|
+
let unanswered = 0;
|
|
2666
|
+
let pinger;
|
|
2667
|
+
const tick = () => {
|
|
2668
|
+
if (!keepalive) return;
|
|
2669
|
+
const backlogged = (port.bufferedAmount ?? 0) > 0;
|
|
2670
|
+
if (heardSincePing) unanswered = 0;
|
|
2671
|
+
else if (peerKeepsAlive && !backlogged) unanswered++;
|
|
2672
|
+
if (unanswered >= misses) {
|
|
2673
|
+
close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
heardSincePing = false;
|
|
2677
|
+
post({
|
|
2678
|
+
type: "ping",
|
|
2679
|
+
id: KEEPALIVE_ID
|
|
2680
|
+
});
|
|
2681
|
+
};
|
|
2682
|
+
const close = async (reason) => {
|
|
2683
|
+
if (muxClosed) return;
|
|
2684
|
+
clearInterval(pinger);
|
|
2685
|
+
muxClosed = true;
|
|
2686
|
+
signal?.removeEventListener("abort", onTransportAbort);
|
|
2687
|
+
port.removeEventListener("message", listener);
|
|
2688
|
+
const closedNow = /* @__PURE__ */ new Set();
|
|
2689
|
+
for (const [id, { handle, credit }] of [...open]) {
|
|
2690
|
+
closedNow.add(id);
|
|
2691
|
+
codecPost({
|
|
2692
|
+
type: "close",
|
|
2693
|
+
id
|
|
2694
|
+
});
|
|
2695
|
+
open.delete(id);
|
|
2696
|
+
releaseWaiters(credit);
|
|
2697
|
+
handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
|
|
2698
|
+
}
|
|
2699
|
+
for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
|
|
2700
|
+
type: "close",
|
|
2701
|
+
id
|
|
2702
|
+
});
|
|
2703
|
+
outgoing.clear();
|
|
2704
|
+
ring.length = 0;
|
|
2705
|
+
await port.close?.();
|
|
2706
|
+
};
|
|
2707
|
+
const onTransportAbort = () => {
|
|
2708
|
+
close(signal?.reason).catch(() => {});
|
|
2263
2709
|
};
|
|
2264
2710
|
port.addEventListener("message", listener);
|
|
2265
2711
|
port.start?.();
|
|
2712
|
+
if (keepalive) {
|
|
2713
|
+
pinger = setInterval(tick, keepalive.intervalMs);
|
|
2714
|
+
pinger.unref?.();
|
|
2715
|
+
}
|
|
2716
|
+
if (signal?.aborted) close(signal.reason).catch(() => {});
|
|
2717
|
+
else signal?.addEventListener("abort", onTransportAbort, { once: true });
|
|
2266
2718
|
return {
|
|
2267
2719
|
maxMessageSize,
|
|
2268
|
-
async openPort(meta) {
|
|
2720
|
+
async openPort(meta, { signal: portSignal } = {}) {
|
|
2269
2721
|
if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
|
|
2270
|
-
if (
|
|
2722
|
+
if (portSignal?.aborted) throw portSignal.reason;
|
|
2723
|
+
if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
|
|
2271
2724
|
while (open.has(nextId)) nextId += 2;
|
|
2272
2725
|
const id = nextId;
|
|
2273
2726
|
nextId += 2;
|
|
2274
|
-
const handle = attach(id);
|
|
2275
|
-
post({
|
|
2727
|
+
const handle = attach(id, meta, window, true, exactCodec);
|
|
2728
|
+
post(window ? exactCodec ? {
|
|
2729
|
+
type: "open",
|
|
2730
|
+
id,
|
|
2731
|
+
meta,
|
|
2732
|
+
window,
|
|
2733
|
+
exact: true
|
|
2734
|
+
} : {
|
|
2735
|
+
type: "open",
|
|
2736
|
+
id,
|
|
2737
|
+
meta,
|
|
2738
|
+
window
|
|
2739
|
+
} : {
|
|
2276
2740
|
type: "open",
|
|
2277
2741
|
id,
|
|
2278
2742
|
meta
|
|
2279
2743
|
});
|
|
2744
|
+
if (portSignal) {
|
|
2745
|
+
const record = open.get(id)?.handle;
|
|
2746
|
+
const onAbort = () => {
|
|
2747
|
+
if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
|
|
2748
|
+
};
|
|
2749
|
+
portSignal.addEventListener("abort", onAbort, { once: true });
|
|
2750
|
+
getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
|
|
2751
|
+
}
|
|
2280
2752
|
return handle.port;
|
|
2281
2753
|
},
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
}
|
|
2292
|
-
muxClosed = true;
|
|
2293
|
-
port.removeEventListener("message", listener);
|
|
2294
|
-
await port.close?.();
|
|
2754
|
+
close,
|
|
2755
|
+
ports() {
|
|
2756
|
+
return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
|
|
2757
|
+
...info,
|
|
2758
|
+
...credit.enforced ? { window: credit.window } : {},
|
|
2759
|
+
queued: credit.queued,
|
|
2760
|
+
outstanding: credit.outstanding,
|
|
2761
|
+
unconsumed: credit.unconsumed
|
|
2762
|
+
} : info);
|
|
2295
2763
|
}
|
|
2296
2764
|
};
|
|
2297
2765
|
}
|
|
2766
|
+
/** The byte length of a received message's data, or `undefined` if it is not bytes. */
|
|
2767
|
+
function byteLengthOf(data) {
|
|
2768
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
|
|
2769
|
+
}
|
|
2770
|
+
/**
|
|
2771
|
+
* `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
|
|
2772
|
+
* a full table names who filled it. A port without a kind counts as `"?"`.
|
|
2773
|
+
*/
|
|
2774
|
+
function describeKinds(entries) {
|
|
2775
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2776
|
+
for (const { meta } of entries) {
|
|
2777
|
+
const kind = meta?.kind;
|
|
2778
|
+
const name = typeof kind === "string" ? kind : "?";
|
|
2779
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
2780
|
+
}
|
|
2781
|
+
if (!counts.size) return "none";
|
|
2782
|
+
return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
|
|
2783
|
+
}
|
|
2298
2784
|
/**
|
|
2299
2785
|
* Announced with every port this pair opens. Layer 1 never reads `meta` — it is
|
|
2300
2786
|
* here so a peer running something else over the same multiplexer can tell a
|
|
@@ -2356,7 +2842,7 @@ function isEnvelope(value) {
|
|
|
2356
2842
|
const candidate = value;
|
|
2357
2843
|
if (typeof candidate.id !== "number") return false;
|
|
2358
2844
|
if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
|
|
2359
|
-
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
|
|
2845
|
+
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
|
|
2360
2846
|
}
|
|
2361
2847
|
/**
|
|
2362
2848
|
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
@@ -2494,7 +2980,7 @@ function trackedLink(send, release) {
|
|
|
2494
2980
|
finish();
|
|
2495
2981
|
return response;
|
|
2496
2982
|
}
|
|
2497
|
-
return new Response(guardBody(response.body, lost, () => lostReason, finish), {
|
|
2983
|
+
return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
|
|
2498
2984
|
status: response.status,
|
|
2499
2985
|
statusText: response.statusText,
|
|
2500
2986
|
headers: response.headers
|
|
@@ -2513,33 +2999,65 @@ function trackedLink(send, release) {
|
|
|
2513
2999
|
return asyncDisposable(link, () => link.close());
|
|
2514
3000
|
}
|
|
2515
3001
|
/**
|
|
2516
|
-
* A response body that reports its end (done, cancelled or
|
|
2517
|
-
* with the link's reason when the link is lost mid-stream.
|
|
3002
|
+
* A response body that reports its end (done, cancelled, failed or aborted) and
|
|
3003
|
+
* fails with the link's reason when the link is lost mid-stream. An abort of the
|
|
3004
|
+
* request's signal ends it at once, read or not: the exchange is over (the layer
|
|
3005
|
+
* below has given its port back), so the request leaves `inflight` then.
|
|
2518
3006
|
*/
|
|
2519
|
-
function guardBody(body, lost, reason, finish) {
|
|
3007
|
+
function guardBody(body, lost, reason, finish, signal) {
|
|
2520
3008
|
const reader = body.getReader();
|
|
3009
|
+
let controller;
|
|
3010
|
+
let ended = false;
|
|
3011
|
+
const end = () => {
|
|
3012
|
+
ended = true;
|
|
3013
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3014
|
+
finish();
|
|
3015
|
+
};
|
|
3016
|
+
const onAbort = () => {
|
|
3017
|
+
if (ended) return;
|
|
3018
|
+
end();
|
|
3019
|
+
reader.cancel().catch(() => {});
|
|
3020
|
+
try {
|
|
3021
|
+
controller.error(abortError(signal));
|
|
3022
|
+
} catch {}
|
|
3023
|
+
};
|
|
2521
3024
|
return new ReadableStream({
|
|
3025
|
+
start(c) {
|
|
3026
|
+
controller = c;
|
|
3027
|
+
if (signal?.aborted) onAbort();
|
|
3028
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
3029
|
+
},
|
|
2522
3030
|
async pull(controller) {
|
|
2523
3031
|
try {
|
|
2524
3032
|
const { done, value } = await Promise.race([reader.read(), lost]);
|
|
3033
|
+
if (ended) return;
|
|
2525
3034
|
if (done) {
|
|
2526
|
-
|
|
3035
|
+
end();
|
|
2527
3036
|
controller.close();
|
|
2528
3037
|
return;
|
|
2529
3038
|
}
|
|
2530
3039
|
controller.enqueue(value);
|
|
2531
3040
|
} catch (error) {
|
|
2532
|
-
|
|
3041
|
+
if (ended) return;
|
|
3042
|
+
end();
|
|
2533
3043
|
reader.cancel().catch(() => {});
|
|
2534
3044
|
controller.error(reason() ?? error);
|
|
2535
3045
|
}
|
|
2536
3046
|
},
|
|
2537
3047
|
async cancel(why) {
|
|
2538
|
-
|
|
3048
|
+
end();
|
|
2539
3049
|
await reader.cancel(why).catch(() => {});
|
|
2540
3050
|
}
|
|
2541
3051
|
});
|
|
2542
3052
|
}
|
|
3053
|
+
/** The signal's reason as an `Error` (`AbortError` when it gave none). */
|
|
3054
|
+
function abortError(signal) {
|
|
3055
|
+
const why = signal.reason;
|
|
3056
|
+
if (why instanceof Error) return why;
|
|
3057
|
+
const err = new Error(why === void 0 ? "Aborted" : String(why));
|
|
3058
|
+
err.name = "AbortError";
|
|
3059
|
+
return err;
|
|
3060
|
+
}
|
|
2543
3061
|
//#endregion
|
|
2544
3062
|
//#region src/sw/protocol.ts
|
|
2545
3063
|
/** Page → ServiceWorker: open a WebSocket to a handler. */
|