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