@statewalker/webrun-http-browser 0.7.0 → 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 +946 -120
- package/dist/link/async-dispose.d.ts +10 -0
- package/dist/link/async-dispose.d.ts.map +1 -0
- package/dist/link/http-link.d.ts +16 -2
- package/dist/link/http-link.d.ts.map +1 -1
- package/dist/relay-sw.js +809 -132
- package/dist/relay-worker.js +809 -132
- 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 +929 -118
- package/dist/sw.js +942 -119
- package/dist/worker.js +938 -119
- package/package.json +4 -4
- package/src/link/async-dispose.ts +22 -0
- package/src/link/http-link.ts +64 -12
- 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/sw-worker.js
CHANGED
|
@@ -74,8 +74,6 @@
|
|
|
74
74
|
const payload = typeof error === "string" ? { message: error } : error;
|
|
75
75
|
return Object.assign(new Error(payload.message), payload);
|
|
76
76
|
}
|
|
77
|
-
new TextEncoder();
|
|
78
|
-
new TextDecoder();
|
|
79
77
|
/**
|
|
80
78
|
* The newAsyncGenerator function creates async generators from callback-based initialization
|
|
81
79
|
* functions, providing a bridge between imperative event handling and declarative async
|
|
@@ -228,7 +226,6 @@
|
|
|
228
226
|
drainQueue();
|
|
229
227
|
}
|
|
230
228
|
}
|
|
231
|
-
new TextEncoder();
|
|
232
229
|
/**
|
|
233
230
|
* The iterator ↔ `ReadableStream` boundary.
|
|
234
231
|
*
|
|
@@ -574,8 +571,8 @@
|
|
|
574
571
|
const NEWLINE = 10;
|
|
575
572
|
/** Mirrors the HTTP/1.1 codec's default `maxHeaderBytes`. */
|
|
576
573
|
const MAX_ENVELOPE_BYTES = 65536;
|
|
577
|
-
const encoder$1 = new TextEncoder();
|
|
578
|
-
const decoder = new TextDecoder();
|
|
574
|
+
const encoder$1 = /* @__PURE__ */ new TextEncoder();
|
|
575
|
+
const decoder = /* @__PURE__ */ new TextDecoder();
|
|
579
576
|
/**
|
|
580
577
|
* Encode an HTTP envelope plus optional body as one continuous byte stream:
|
|
581
578
|
*
|
|
@@ -829,15 +826,15 @@
|
|
|
829
826
|
};
|
|
830
827
|
return { kind: "none" };
|
|
831
828
|
}
|
|
832
|
-
const CRLF = new Uint8Array([13, 10]);
|
|
833
|
-
const LAST_CHUNK = new Uint8Array([
|
|
829
|
+
const CRLF = /* @__PURE__ */ new Uint8Array([13, 10]);
|
|
830
|
+
const LAST_CHUNK = /* @__PURE__ */ new Uint8Array([
|
|
834
831
|
48,
|
|
835
832
|
13,
|
|
836
833
|
10,
|
|
837
834
|
13,
|
|
838
835
|
10
|
|
839
836
|
]);
|
|
840
|
-
const encoder = new TextEncoder();
|
|
837
|
+
const encoder = /* @__PURE__ */ new TextEncoder();
|
|
841
838
|
/**
|
|
842
839
|
* Chunk sizes are written in HEXADECIMAL — a 21-byte chunk is `15`. Writing
|
|
843
840
|
* them in decimal is the defect note 15 found in @libp2p/http, and it is the
|
|
@@ -1142,7 +1139,7 @@
|
|
|
1142
1139
|
decodeResponse: (input, o) => decodeResponse(input, opts, o.method)
|
|
1143
1140
|
};
|
|
1144
1141
|
}
|
|
1145
|
-
const httpCodec = newHttpCodec();
|
|
1142
|
+
const httpCodec = /* @__PURE__ */ newHttpCodec();
|
|
1146
1143
|
/**
|
|
1147
1144
|
* Dispatches on byte 0. The formats are self-identifying — a JSON envelope
|
|
1148
1145
|
* always begins `{`, which is not a token character and so can never begin an
|
|
@@ -1192,10 +1189,29 @@
|
|
|
1192
1189
|
* Writes HTTP/1.1; accepts HTTP/1.1 or the legacy JSON envelope. Used whenever
|
|
1193
1190
|
* no `codec` option is supplied.
|
|
1194
1191
|
*/
|
|
1195
|
-
const defaultCodec = newSniffingCodec({
|
|
1192
|
+
const defaultCodec = /* @__PURE__ */ newSniffingCodec({
|
|
1196
1193
|
write: httpCodec,
|
|
1197
1194
|
accept: [httpCodec, jsonEnvelopeCodec]
|
|
1198
1195
|
});
|
|
1196
|
+
/**
|
|
1197
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
1198
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
1199
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
1200
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
1201
|
+
* declared types, which would require consumers to compile with the
|
|
1202
|
+
* `esnext.disposable` lib.
|
|
1203
|
+
*/
|
|
1204
|
+
function asyncDisposable$2(target, dispose) {
|
|
1205
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
1206
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
1207
|
+
value: async () => {
|
|
1208
|
+
await dispose();
|
|
1209
|
+
},
|
|
1210
|
+
configurable: true,
|
|
1211
|
+
writable: true
|
|
1212
|
+
});
|
|
1213
|
+
return target;
|
|
1214
|
+
}
|
|
1199
1215
|
/** Carries the serialized JS error between two webrun peers. */
|
|
1200
1216
|
const PEER_ERROR_HEADER = "x-webrun-error";
|
|
1201
1217
|
/** Hard cap on the peer-error header (M3): see `encodeErrorResponse`. */
|
|
@@ -1273,41 +1289,105 @@
|
|
|
1273
1289
|
*/
|
|
1274
1290
|
async function httpFetch(call, env, body, options = {}) {
|
|
1275
1291
|
const codec = options.codec ?? defaultCodec;
|
|
1292
|
+
const { signal } = options;
|
|
1293
|
+
if (signal?.aborted) throw abortReason$1(signal);
|
|
1276
1294
|
const output = call(codec.encodeRequest(env, body));
|
|
1277
|
-
|
|
1278
|
-
const release =
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1295
|
+
let released;
|
|
1296
|
+
const release = () => {
|
|
1297
|
+
released ??= (async () => {
|
|
1298
|
+
try {
|
|
1299
|
+
await output.return?.(void 0);
|
|
1300
|
+
} catch {}
|
|
1301
|
+
})();
|
|
1302
|
+
return released;
|
|
1303
|
+
};
|
|
1304
|
+
let finished = false;
|
|
1305
|
+
const finish = () => {
|
|
1306
|
+
if (finished || released) return;
|
|
1307
|
+
finished = true;
|
|
1308
|
+
if (signal?.aborted) {
|
|
1309
|
+
release();
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
const onAbort = () => void release();
|
|
1313
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1314
|
+
drain(output).finally(() => signal?.removeEventListener("abort", onAbort));
|
|
1282
1315
|
};
|
|
1316
|
+
let decoded;
|
|
1317
|
+
try {
|
|
1318
|
+
decoded = await untilAborted(codec.decodeResponse(output, { method: env.method }), signal);
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
if (signal?.aborted) release();
|
|
1321
|
+
else await release();
|
|
1322
|
+
throw error;
|
|
1323
|
+
}
|
|
1283
1324
|
const result = {
|
|
1284
1325
|
envelope: decoded.envelope,
|
|
1285
|
-
body: releasingOnExit(decoded.body, release),
|
|
1286
|
-
release
|
|
1326
|
+
body: releasingOnExit(decoded.body, finish, release),
|
|
1327
|
+
release,
|
|
1328
|
+
finish
|
|
1287
1329
|
};
|
|
1288
1330
|
await throwIfPeerError(result, output);
|
|
1289
1331
|
return result;
|
|
1290
1332
|
}
|
|
1333
|
+
/** `promise`, rejected with the signal's reason if it aborts first. */
|
|
1334
|
+
function untilAborted(promise, signal) {
|
|
1335
|
+
if (!signal) return promise;
|
|
1336
|
+
return new Promise((resolve, reject) => {
|
|
1337
|
+
const onAbort = () => reject(abortReason$1(signal));
|
|
1338
|
+
if (signal.aborted) return onAbort();
|
|
1339
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1340
|
+
promise.then((value) => {
|
|
1341
|
+
signal.removeEventListener("abort", onAbort);
|
|
1342
|
+
resolve(value);
|
|
1343
|
+
}, (error) => {
|
|
1344
|
+
signal.removeEventListener("abort", onAbort);
|
|
1345
|
+
reject(error);
|
|
1346
|
+
});
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
/** The signal's reason as an `Error` (`AbortError` when it gave none). */
|
|
1350
|
+
function abortReason$1(signal) {
|
|
1351
|
+
const reason = signal.reason;
|
|
1352
|
+
if (reason instanceof Error) return reason;
|
|
1353
|
+
const err = new Error(reason === void 0 ? "Aborted" : String(reason));
|
|
1354
|
+
err.name = "AbortError";
|
|
1355
|
+
return err;
|
|
1356
|
+
}
|
|
1291
1357
|
/**
|
|
1292
|
-
* `body`,
|
|
1358
|
+
* `body`, ending the call however it is left: read to its end, or left early.
|
|
1293
1359
|
*
|
|
1294
1360
|
* The body the codec hands back is a generator of its own over a `ByteReader`,
|
|
1295
|
-
* and
|
|
1296
|
-
*
|
|
1297
|
-
*
|
|
1298
|
-
*
|
|
1299
|
-
*
|
|
1300
|
-
*
|
|
1301
|
-
|
|
1302
|
-
|
|
1361
|
+
* and its ending never reaches the call's `output` (see `throwIfPeerError` for
|
|
1362
|
+
* why the codec must not do that itself). A caller that stopped reading — an
|
|
1363
|
+
* aborted `fetch`, a cancelled response stream — left the peer's producer
|
|
1364
|
+
* parked forever; and one that read the body to its end left the call itself
|
|
1365
|
+
* open, which on a multiplexed transport is one port per exchange until the
|
|
1366
|
+
* multiplexer refuses to open more (`maxPorts`).
|
|
1367
|
+
*
|
|
1368
|
+
* Left early, the call is abandoned (`.return()`): the peer's producer is
|
|
1369
|
+
* released. Read to its end, the call is not cut short but read to ITS end, in
|
|
1370
|
+
* the background: the peer ends it once its side of the exchange is over,
|
|
1371
|
+
* which may be later than the response — a handler that answered before it
|
|
1372
|
+
* read the request body is still receiving it (`HttpDataHandlerResult.settled`),
|
|
1373
|
+
* and abandoning the call would abort that upload.
|
|
1374
|
+
*/
|
|
1375
|
+
async function* releasingOnExit(body, finish, release) {
|
|
1303
1376
|
let completed = false;
|
|
1304
1377
|
try {
|
|
1305
1378
|
yield* body;
|
|
1306
1379
|
completed = true;
|
|
1307
1380
|
} finally {
|
|
1308
|
-
if (
|
|
1381
|
+
if (completed) finish();
|
|
1382
|
+
else await release();
|
|
1309
1383
|
}
|
|
1310
1384
|
}
|
|
1385
|
+
/** Reads `output` to its end, discarding what is left, ignoring a failure. */
|
|
1386
|
+
async function drain(output) {
|
|
1387
|
+
try {
|
|
1388
|
+
for (let r = await output.next(); !r.done; r = await output.next());
|
|
1389
|
+
} catch {}
|
|
1390
|
+
}
|
|
1311
1391
|
/**
|
|
1312
1392
|
* Wrap an HTTP handler as a `Duplex` so it can be registered with any
|
|
1313
1393
|
* `webrun-streams-*` adapter's `serve`.
|
|
@@ -1335,6 +1415,7 @@
|
|
|
1335
1415
|
try {
|
|
1336
1416
|
yield* replyCodec.encodeResponse(result.envelope, result.body, { method: decoded.envelope.method });
|
|
1337
1417
|
completed = true;
|
|
1418
|
+
await result.settled?.();
|
|
1338
1419
|
} finally {
|
|
1339
1420
|
if (!completed) try {
|
|
1340
1421
|
await result.release?.();
|
|
@@ -1558,17 +1639,21 @@
|
|
|
1558
1639
|
const buffered = new Uint8Array(await request.arrayBuffer());
|
|
1559
1640
|
if (buffered.byteLength > 0) body = [buffered];
|
|
1560
1641
|
}
|
|
1561
|
-
const { envelope, body: respBody, release } = await httpFetch(call, env, body,
|
|
1642
|
+
const { envelope, body: respBody, release, finish } = await httpFetch(call, env, body, {
|
|
1643
|
+
...options,
|
|
1644
|
+
signal: request.signal ?? void 0
|
|
1645
|
+
});
|
|
1562
1646
|
const responseInit = {
|
|
1563
1647
|
status: envelope.status,
|
|
1564
1648
|
statusText: envelope.statusText,
|
|
1565
1649
|
headers: forwardableHeaders(envelope.headers)
|
|
1566
1650
|
};
|
|
1567
1651
|
if (request.method === "HEAD" || request.method === "OPTIONS" || NULL_BODY_STATUSES.has(envelope.status)) {
|
|
1568
|
-
|
|
1652
|
+
if (finish) finish();
|
|
1653
|
+
else await release?.();
|
|
1569
1654
|
return new Response(null, responseInit);
|
|
1570
1655
|
}
|
|
1571
|
-
return new Response(
|
|
1656
|
+
return new Response(responseBody(respBody, request.signal, release), responseInit);
|
|
1572
1657
|
}
|
|
1573
1658
|
/**
|
|
1574
1659
|
* Wrap a `(Request) => Promise<Response>` handler as a `Duplex` so it can be
|
|
@@ -1580,13 +1665,21 @@
|
|
|
1580
1665
|
method: env.method,
|
|
1581
1666
|
headers: forwardableHeaders(env.headers)
|
|
1582
1667
|
};
|
|
1668
|
+
let upload;
|
|
1583
1669
|
if (env.method !== "GET" && env.method !== "HEAD") {
|
|
1584
1670
|
if (supportsRequestStreams()) {
|
|
1585
|
-
|
|
1671
|
+
upload = trackingEnd(body);
|
|
1672
|
+
reqInit.body = asyncIterableToReadable(upload.source);
|
|
1586
1673
|
reqInit.duplex = "half";
|
|
1587
1674
|
} else reqInit.body = await collectBytes(body);
|
|
1588
1675
|
}
|
|
1589
|
-
const
|
|
1676
|
+
const request = new Request(env.url, reqInit);
|
|
1677
|
+
const response = await handler(request);
|
|
1678
|
+
const settled = async () => {
|
|
1679
|
+
if (!upload) return;
|
|
1680
|
+
if (!request.bodyUsed && !request.body?.locked) request.body?.cancel().catch(() => {});
|
|
1681
|
+
await upload.settle(options.requestIdleMs ?? 3e4);
|
|
1682
|
+
};
|
|
1590
1683
|
const respEnv = {
|
|
1591
1684
|
status: response.status,
|
|
1592
1685
|
statusText: response.statusText,
|
|
@@ -1596,32 +1689,173 @@
|
|
|
1596
1689
|
await response.body?.cancel();
|
|
1597
1690
|
return {
|
|
1598
1691
|
envelope: respEnv,
|
|
1599
|
-
body: void 0
|
|
1692
|
+
body: void 0,
|
|
1693
|
+
settled
|
|
1600
1694
|
};
|
|
1601
1695
|
}
|
|
1602
1696
|
return {
|
|
1603
1697
|
envelope: respEnv,
|
|
1604
1698
|
body: response.body ? readableToAsyncIterable(response.body) : void 0,
|
|
1605
|
-
release: () => response.body?.cancel()
|
|
1699
|
+
release: () => response.body?.cancel(),
|
|
1700
|
+
settled
|
|
1606
1701
|
};
|
|
1607
1702
|
}, options);
|
|
1608
1703
|
}
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1704
|
+
/**
|
|
1705
|
+
* `source`, and `settle(idleMs)`: resolves once it is done — read to its end,
|
|
1706
|
+
* failed, or abandoned (`return()` included before the first `next()`, which a
|
|
1707
|
+
* generator's `finally` would not see) — or once nobody has asked it for a
|
|
1708
|
+
* chunk for `idleMs`, in which case it is abandoned: later reads fail.
|
|
1709
|
+
*
|
|
1710
|
+
* Idle counts only while no `next()` is pending: a reader waiting for a slow
|
|
1711
|
+
* sender is still reading.
|
|
1712
|
+
*/
|
|
1713
|
+
function trackingEnd(source) {
|
|
1714
|
+
let end = () => {};
|
|
1715
|
+
let isEnded = false;
|
|
1716
|
+
const ended = new Promise((resolve) => {
|
|
1717
|
+
end = () => {
|
|
1718
|
+
isEnded = true;
|
|
1719
|
+
resolve();
|
|
1720
|
+
};
|
|
1721
|
+
});
|
|
1722
|
+
let abandoned;
|
|
1723
|
+
let pending = 0;
|
|
1724
|
+
let idleSince = Date.now();
|
|
1725
|
+
const it = source[Symbol.asyncIterator]();
|
|
1726
|
+
const abandon = (reason) => {
|
|
1727
|
+
abandoned ??= reason;
|
|
1728
|
+
end();
|
|
1729
|
+
Promise.resolve(it.return?.(void 0)).catch(() => {});
|
|
1730
|
+
};
|
|
1731
|
+
const tracked = {
|
|
1732
|
+
[Symbol.asyncIterator]() {
|
|
1733
|
+
return tracked;
|
|
1734
|
+
},
|
|
1735
|
+
async next() {
|
|
1736
|
+
if (abandoned) throw abandoned;
|
|
1737
|
+
pending++;
|
|
1738
|
+
try {
|
|
1739
|
+
const r = await it.next();
|
|
1740
|
+
if (r.done) end();
|
|
1741
|
+
return r;
|
|
1742
|
+
} catch (error) {
|
|
1743
|
+
end();
|
|
1744
|
+
throw error;
|
|
1745
|
+
} finally {
|
|
1746
|
+
if (--pending === 0) idleSince = Date.now();
|
|
1747
|
+
}
|
|
1748
|
+
},
|
|
1749
|
+
async return(value) {
|
|
1750
|
+
end();
|
|
1751
|
+
try {
|
|
1752
|
+
await it.return?.(value);
|
|
1753
|
+
} catch {}
|
|
1754
|
+
return {
|
|
1755
|
+
done: true,
|
|
1756
|
+
value: void 0
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
asyncDisposable$2(tracked, () => tracked.return?.() ?? Promise.resolve());
|
|
1761
|
+
const settle = (idleMs) => {
|
|
1762
|
+
if (isEnded || !Number.isFinite(idleMs)) return ended;
|
|
1763
|
+
return new Promise((resolve) => {
|
|
1764
|
+
let timer;
|
|
1765
|
+
const check = () => {
|
|
1766
|
+
if (isEnded) return resolve();
|
|
1767
|
+
const idle = pending === 0 ? Date.now() - idleSince : 0;
|
|
1768
|
+
if (idle >= idleMs) {
|
|
1769
|
+
abandon(/* @__PURE__ */ new Error(`webrun-http-streams: the request body was left unread for ${idleMs} ms after the response; abandoned`));
|
|
1770
|
+
return resolve();
|
|
1771
|
+
}
|
|
1772
|
+
timer = setTimeout(check, Math.max(1, Math.min(idleMs - idle, 1e3)));
|
|
1773
|
+
timer.unref?.();
|
|
1774
|
+
};
|
|
1775
|
+
ended.then(() => {
|
|
1776
|
+
clearTimeout(timer);
|
|
1777
|
+
resolve();
|
|
1778
|
+
});
|
|
1779
|
+
check();
|
|
1780
|
+
});
|
|
1781
|
+
};
|
|
1782
|
+
return {
|
|
1783
|
+
source: tracked,
|
|
1784
|
+
settle
|
|
1785
|
+
};
|
|
1618
1786
|
}
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1787
|
+
/**
|
|
1788
|
+
* How long a cancelled body waits for the read in flight before it abandons the call anyway.
|
|
1789
|
+
* The read's answer tells a body that has ended (the call is finished: an upload the handler
|
|
1790
|
+
* reads after answering goes on) from one that has not; a peer that sends nothing more never
|
|
1791
|
+
* answers it. A body whose end comes later than this after the cancel is abandoned too, so a
|
|
1792
|
+
* handler still streaming its response and still reading the upload then loses the upload.
|
|
1793
|
+
*/
|
|
1794
|
+
const CANCEL_GRACE_MS = 1e3;
|
|
1795
|
+
/**
|
|
1796
|
+
* The response body as a stream that the request's signal can end at any time.
|
|
1797
|
+
*
|
|
1798
|
+
* An abort errors the stream with the abort's reason at once — a read waiting
|
|
1799
|
+
* for a peer that sends nothing more rejects then, not when a next chunk
|
|
1800
|
+
* arrives, which may be never — and abandons the call (`release`), so on a
|
|
1801
|
+
* multiplexed transport its port is given back whether or not anyone still
|
|
1802
|
+
* reads the body. The listener is removed once the body ends, however it ends:
|
|
1803
|
+
* read to its end, cancelled, failed or aborted. A body nobody reads or cancels
|
|
1804
|
+
* keeps it until the signal aborts (and keeps the call open until then anyway).
|
|
1805
|
+
* A cancel abandons the call too — unless the read in flight comes back with the
|
|
1806
|
+
* body's end, which finishes it instead (see `cancel`).
|
|
1807
|
+
*/
|
|
1808
|
+
function responseBody(body, signal, release) {
|
|
1809
|
+
const it = body[Symbol.asyncIterator]();
|
|
1810
|
+
let controller;
|
|
1811
|
+
let ended = false;
|
|
1812
|
+
let reading = false;
|
|
1813
|
+
const end = () => {
|
|
1814
|
+
ended = true;
|
|
1815
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1816
|
+
};
|
|
1817
|
+
const onAbort = () => {
|
|
1818
|
+
if (ended) return;
|
|
1819
|
+
end();
|
|
1820
|
+
try {
|
|
1821
|
+
controller.error(abortReason$1(signal));
|
|
1822
|
+
} catch {}
|
|
1823
|
+
release?.();
|
|
1824
|
+
};
|
|
1825
|
+
return new ReadableStream({
|
|
1826
|
+
start(c) {
|
|
1827
|
+
controller = c;
|
|
1828
|
+
if (signal?.aborted) onAbort();
|
|
1829
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
1830
|
+
},
|
|
1831
|
+
async pull(c) {
|
|
1832
|
+
try {
|
|
1833
|
+
reading = true;
|
|
1834
|
+
const { done, value } = await it.next().finally(() => {
|
|
1835
|
+
reading = false;
|
|
1836
|
+
});
|
|
1837
|
+
if (ended) return;
|
|
1838
|
+
if (done) {
|
|
1839
|
+
end();
|
|
1840
|
+
c.close();
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
c.enqueue(value);
|
|
1844
|
+
} catch (err) {
|
|
1845
|
+
if (ended) return;
|
|
1846
|
+
end();
|
|
1847
|
+
c.error(err);
|
|
1848
|
+
}
|
|
1849
|
+
},
|
|
1850
|
+
cancel() {
|
|
1851
|
+
end();
|
|
1852
|
+
const returned = Promise.resolve(it.return?.()).catch(() => {});
|
|
1853
|
+
if (!reading) return;
|
|
1854
|
+
const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
|
|
1855
|
+
grace.unref?.();
|
|
1856
|
+
returned.finally(() => clearTimeout(grace));
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1625
1859
|
}
|
|
1626
1860
|
//#endregion
|
|
1627
1861
|
//#region src/core/data-channels.ts
|
|
@@ -1949,8 +2183,11 @@
|
|
|
1949
2183
|
error
|
|
1950
2184
|
});
|
|
1951
2185
|
};
|
|
1952
|
-
|
|
1953
|
-
|
|
2186
|
+
const listener = (event) => {
|
|
2187
|
+
onMessage(event);
|
|
2188
|
+
};
|
|
2189
|
+
port.addEventListener("message", listener);
|
|
2190
|
+
return () => port.removeEventListener("message", listener);
|
|
1954
2191
|
}
|
|
1955
2192
|
/**
|
|
1956
2193
|
* Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
|
|
@@ -1980,6 +2217,69 @@
|
|
|
1980
2217
|
}
|
|
1981
2218
|
}
|
|
1982
2219
|
/**
|
|
2220
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
2221
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
2222
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
2223
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
2224
|
+
* declared types, which would require consumers to compile with the
|
|
2225
|
+
* `esnext.disposable` lib.
|
|
2226
|
+
*/
|
|
2227
|
+
function asyncDisposable$1(target, dispose) {
|
|
2228
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
2229
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
2230
|
+
value: async () => {
|
|
2231
|
+
await dispose();
|
|
2232
|
+
},
|
|
2233
|
+
configurable: true,
|
|
2234
|
+
writable: true
|
|
2235
|
+
});
|
|
2236
|
+
return target;
|
|
2237
|
+
}
|
|
2238
|
+
/**
|
|
2239
|
+
* `gen`, except that `return()` and `throw()` take effect at once.
|
|
2240
|
+
*
|
|
2241
|
+
* On an async generator both are queued behind a `next()` that is still
|
|
2242
|
+
* pending: a consumer that gives up on a stream while it waits for the peer's
|
|
2243
|
+
* next chunk — an aborted fetch whose response head has not come, say — cannot
|
|
2244
|
+
* end it until that chunk arrives, which for a peer that never answers is
|
|
2245
|
+
* never. The stream's `finally` (which closes its port and tells the peer)
|
|
2246
|
+
* then never runs, and on a multiplexer the port is kept for good.
|
|
2247
|
+
*
|
|
2248
|
+
* So `abandon` runs first, synchronously: it must make the pending `next()`
|
|
2249
|
+
* settle (abort the stream), which then resolves done, after which the
|
|
2250
|
+
* generator's own `return()` / `throw()` proceeds as usual. On a generator
|
|
2251
|
+
* suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
|
|
2252
|
+
*/
|
|
2253
|
+
function returningAtOnce(gen, abandon) {
|
|
2254
|
+
let abandoned = false;
|
|
2255
|
+
const giveUp = (reason) => {
|
|
2256
|
+
if (abandoned) return;
|
|
2257
|
+
abandoned = true;
|
|
2258
|
+
abandon(reason);
|
|
2259
|
+
};
|
|
2260
|
+
const wrapped = {
|
|
2261
|
+
next: (...args) => gen.next(...args).catch((error) => {
|
|
2262
|
+
if (abandoned) return {
|
|
2263
|
+
done: true,
|
|
2264
|
+
value: void 0
|
|
2265
|
+
};
|
|
2266
|
+
throw error;
|
|
2267
|
+
}),
|
|
2268
|
+
return(value) {
|
|
2269
|
+
giveUp();
|
|
2270
|
+
return gen.return(value);
|
|
2271
|
+
},
|
|
2272
|
+
throw(error) {
|
|
2273
|
+
giveUp(error);
|
|
2274
|
+
return gen.throw(error);
|
|
2275
|
+
},
|
|
2276
|
+
[Symbol.asyncIterator]() {
|
|
2277
|
+
return wrapped;
|
|
2278
|
+
}
|
|
2279
|
+
};
|
|
2280
|
+
return asyncDisposable$1(wrapped, () => wrapped.return(void 0));
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
1983
2283
|
* The `type` of the out-of-band notice a side posts when it abandons a stream.
|
|
1984
2284
|
*
|
|
1985
2285
|
* Layer 1's `close` is not observable to layer 2 — a closed virtual port drops
|
|
@@ -2075,7 +2375,7 @@
|
|
|
2075
2375
|
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
2076
2376
|
const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
|
|
2077
2377
|
pump.catch(() => {});
|
|
2078
|
-
return (async function* () {
|
|
2378
|
+
return returningAtOnce((async function* () {
|
|
2079
2379
|
try {
|
|
2080
2380
|
yield* inbound.stream;
|
|
2081
2381
|
} finally {
|
|
@@ -2089,7 +2389,9 @@
|
|
|
2089
2389
|
await port.close?.();
|
|
2090
2390
|
} catch {}
|
|
2091
2391
|
}
|
|
2092
|
-
})()
|
|
2392
|
+
})(), () => {
|
|
2393
|
+
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
2394
|
+
});
|
|
2093
2395
|
}
|
|
2094
2396
|
/**
|
|
2095
2397
|
* Listens for the peer's abort notice, and can post our own. The notice is a
|
|
@@ -2290,6 +2592,33 @@
|
|
|
2290
2592
|
}
|
|
2291
2593
|
}
|
|
2292
2594
|
/**
|
|
2595
|
+
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
2596
|
+
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
2597
|
+
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
2598
|
+
* are clamped to 4 ms in browsers.
|
|
2599
|
+
*/
|
|
2600
|
+
const setImmediateFn = globalThis.setImmediate;
|
|
2601
|
+
/** @internal */
|
|
2602
|
+
const nextTask = setImmediateFn ? (fn) => {
|
|
2603
|
+
setImmediateFn(fn);
|
|
2604
|
+
} : (() => {
|
|
2605
|
+
let channel;
|
|
2606
|
+
const queue = [];
|
|
2607
|
+
return (fn) => {
|
|
2608
|
+
if (!channel) {
|
|
2609
|
+
channel = new MessageChannel();
|
|
2610
|
+
channel.port1.onmessage = () => queue.shift()?.();
|
|
2611
|
+
}
|
|
2612
|
+
queue.push(fn);
|
|
2613
|
+
channel.port2.postMessage(0);
|
|
2614
|
+
};
|
|
2615
|
+
})();
|
|
2616
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
2617
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
2618
|
+
function setPortSendState(port, state) {
|
|
2619
|
+
states.set(port, state);
|
|
2620
|
+
}
|
|
2621
|
+
/**
|
|
2293
2622
|
* One virtual port.
|
|
2294
2623
|
*
|
|
2295
2624
|
* `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
|
|
@@ -2304,6 +2633,14 @@
|
|
|
2304
2633
|
if (closing.signal.aborted) return;
|
|
2305
2634
|
closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
|
|
2306
2635
|
};
|
|
2636
|
+
const close = (reason) => {
|
|
2637
|
+
if (closed) return;
|
|
2638
|
+
closed = true;
|
|
2639
|
+
const notify = requestClose;
|
|
2640
|
+
listeners.clear();
|
|
2641
|
+
notify(reason);
|
|
2642
|
+
abort(reason);
|
|
2643
|
+
};
|
|
2307
2644
|
const port = {
|
|
2308
2645
|
addEventListener(_type, listener) {
|
|
2309
2646
|
listeners.add(listener);
|
|
@@ -2315,24 +2652,28 @@
|
|
|
2315
2652
|
if (closed) return;
|
|
2316
2653
|
send(message, transfer);
|
|
2317
2654
|
},
|
|
2318
|
-
close
|
|
2319
|
-
if (closed) return;
|
|
2320
|
-
closed = true;
|
|
2321
|
-
const notify = requestClose;
|
|
2322
|
-
listeners.clear();
|
|
2323
|
-
notify();
|
|
2324
|
-
abort();
|
|
2325
|
-
}
|
|
2655
|
+
close
|
|
2326
2656
|
};
|
|
2327
2657
|
setPortCloseSignal(port, closing.signal);
|
|
2658
|
+
const dispose = Symbol.dispose;
|
|
2659
|
+
if (dispose) Object.defineProperty(port, dispose, {
|
|
2660
|
+
value: () => close(),
|
|
2661
|
+
configurable: true
|
|
2662
|
+
});
|
|
2328
2663
|
return {
|
|
2329
2664
|
port,
|
|
2330
2665
|
deliver(payload) {
|
|
2331
2666
|
if (closed) return;
|
|
2332
2667
|
const event = new MessageEvent("message", { data: payload });
|
|
2668
|
+
let pending;
|
|
2333
2669
|
for (const listener of [...listeners]) try {
|
|
2334
|
-
listener(event);
|
|
2670
|
+
const result = listener(event);
|
|
2671
|
+
if (result && typeof result.then === "function") {
|
|
2672
|
+
pending ??= [];
|
|
2673
|
+
pending.push(result);
|
|
2674
|
+
}
|
|
2335
2675
|
} catch {}
|
|
2676
|
+
return pending ? Promise.allSettled(pending) : void 0;
|
|
2336
2677
|
},
|
|
2337
2678
|
markClosed(reason) {
|
|
2338
2679
|
closed = true;
|
|
@@ -2341,53 +2682,309 @@
|
|
|
2341
2682
|
},
|
|
2342
2683
|
isClosed() {
|
|
2343
2684
|
return closed;
|
|
2685
|
+
},
|
|
2686
|
+
hasListeners() {
|
|
2687
|
+
return listeners.size > 0;
|
|
2344
2688
|
}
|
|
2345
2689
|
};
|
|
2346
2690
|
}
|
|
2347
2691
|
/**
|
|
2348
|
-
* The
|
|
2349
|
-
*
|
|
2692
|
+
* The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
|
|
2693
|
+
* whose codec passes unknown envelopes through reads them as a close of that
|
|
2694
|
+
* id, which must therefore not be a real port (0 is the initiator's first).
|
|
2350
2695
|
*/
|
|
2351
|
-
const
|
|
2696
|
+
const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
2697
|
+
/** The longest delay a timer holds; past it `setInterval` fires at once. */
|
|
2698
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
2352
2699
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
2353
2700
|
const DEFAULT_MAX_PORTS = 1024;
|
|
2354
2701
|
/**
|
|
2702
|
+
* What a message costs a port's window when the codec cannot say
|
|
2703
|
+
* (`PortCodec.encode`): its bytes and string lengths, found in its values up
|
|
2704
|
+
* to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
|
|
2705
|
+
* from the same structured value, so they agree.
|
|
2706
|
+
*/
|
|
2707
|
+
function estimateMessageSize(payload) {
|
|
2708
|
+
let budget = 256;
|
|
2709
|
+
const walk = (v, depth) => {
|
|
2710
|
+
if (budget-- <= 0) return 0;
|
|
2711
|
+
if (typeof v === "string") return v.length;
|
|
2712
|
+
if (v instanceof ArrayBuffer) return v.byteLength;
|
|
2713
|
+
if (ArrayBuffer.isView(v)) return v.byteLength;
|
|
2714
|
+
if (v === null || typeof v !== "object" || depth >= 4) return 8;
|
|
2715
|
+
let n = 16;
|
|
2716
|
+
if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
|
|
2717
|
+
else for (const k in v) n += k.length + walk(v[k], depth + 1);
|
|
2718
|
+
return n;
|
|
2719
|
+
};
|
|
2720
|
+
return Math.max(64, walk(payload, 0));
|
|
2721
|
+
}
|
|
2722
|
+
/**
|
|
2355
2723
|
* The default `PortMux`: emulates multiplexing over a single port.
|
|
2356
2724
|
*
|
|
2357
2725
|
* A transport that already multiplexes natively supplies its own `PortMux`
|
|
2358
2726
|
* instead — this implementation is for transports that offer one pipe.
|
|
2359
2727
|
*/
|
|
2360
2728
|
function multiplexPort(port, options) {
|
|
2361
|
-
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
|
|
2729
|
+
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
|
|
2730
|
+
const misses = keepalive?.misses ?? 3;
|
|
2731
|
+
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`);
|
|
2732
|
+
if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
|
|
2362
2733
|
const open = /* @__PURE__ */ new Map();
|
|
2734
|
+
const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
|
|
2735
|
+
const entry = open.get(id);
|
|
2736
|
+
if (entry?.handle !== record) return;
|
|
2737
|
+
const { handle: _, credit: __, partial: ___, ...info } = entry;
|
|
2738
|
+
if (!record.hasListeners()) {
|
|
2739
|
+
release("leaked");
|
|
2740
|
+
record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
|
|
2741
|
+
}
|
|
2742
|
+
try {
|
|
2743
|
+
onLeak?.(info);
|
|
2744
|
+
} catch {}
|
|
2745
|
+
}) : void 0;
|
|
2746
|
+
const touch = (id) => {
|
|
2747
|
+
const entry = open.get(id);
|
|
2748
|
+
if (entry) entry.lastActivity = Date.now();
|
|
2749
|
+
};
|
|
2363
2750
|
let nextId = side === "initiator" ? 0 : 1;
|
|
2364
2751
|
let muxClosed = false;
|
|
2752
|
+
/** This end counts encoded bytes and reassembles frames. */
|
|
2753
|
+
const exactCodec = codec.encode != null && codec.decode != null;
|
|
2365
2754
|
const post = (envelope, transfer) => {
|
|
2366
2755
|
if (muxClosed) return;
|
|
2756
|
+
codecPost(envelope, transfer);
|
|
2757
|
+
};
|
|
2758
|
+
/** Like `post`, also while closing: the mux's own close notices. */
|
|
2759
|
+
const codecPost = (envelope, transfer) => {
|
|
2367
2760
|
try {
|
|
2368
2761
|
codec.post(port, envelope, transfer);
|
|
2369
2762
|
} catch {}
|
|
2370
2763
|
};
|
|
2371
|
-
const
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2764
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
2765
|
+
const ring = [];
|
|
2766
|
+
let pumping = false;
|
|
2767
|
+
const pump = async () => {
|
|
2768
|
+
pumping = true;
|
|
2769
|
+
try {
|
|
2770
|
+
while (ring.length && !muxClosed) {
|
|
2771
|
+
if (port.whenDrained) await port.whenDrained();
|
|
2772
|
+
if (muxClosed) break;
|
|
2773
|
+
const id = ring.shift();
|
|
2774
|
+
const queue = outgoing.get(id);
|
|
2775
|
+
const item = queue?.shift();
|
|
2776
|
+
if (queue?.length) ring.push(id);
|
|
2777
|
+
else outgoing.delete(id);
|
|
2778
|
+
item?.();
|
|
2779
|
+
}
|
|
2780
|
+
} catch (err) {
|
|
2781
|
+
close(err).catch(() => {});
|
|
2782
|
+
} finally {
|
|
2783
|
+
pumping = false;
|
|
2784
|
+
}
|
|
2785
|
+
};
|
|
2786
|
+
const sendOnPort = (id, item) => {
|
|
2787
|
+
if (frameSize == null) {
|
|
2788
|
+
item();
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
let queue = outgoing.get(id);
|
|
2792
|
+
if (!queue) {
|
|
2793
|
+
queue = [];
|
|
2794
|
+
outgoing.set(id, queue);
|
|
2795
|
+
ring.push(id);
|
|
2796
|
+
}
|
|
2797
|
+
queue.push(item);
|
|
2798
|
+
if (!pumping) pump();
|
|
2799
|
+
};
|
|
2800
|
+
const dropOutgoing = (id) => {
|
|
2801
|
+
if (!outgoing.delete(id)) return;
|
|
2802
|
+
const at = ring.indexOf(id);
|
|
2803
|
+
if (at >= 0) ring.splice(at, 1);
|
|
2804
|
+
};
|
|
2805
|
+
const postBytes = (bytes) => {
|
|
2806
|
+
if (muxClosed) return;
|
|
2807
|
+
try {
|
|
2808
|
+
port.postMessage(bytes);
|
|
2809
|
+
} catch {}
|
|
2810
|
+
};
|
|
2811
|
+
const releaseWaiters = (c) => {
|
|
2812
|
+
for (const w of c?.waiters.splice(0) ?? []) w.resolve();
|
|
2813
|
+
};
|
|
2814
|
+
const flush = (c) => {
|
|
2815
|
+
while (c.queue.length) {
|
|
2816
|
+
const next = c.queue[0];
|
|
2817
|
+
if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
2818
|
+
c.queue.shift();
|
|
2819
|
+
c.queued -= next.size;
|
|
2820
|
+
c.outstanding += next.size;
|
|
2821
|
+
next.send();
|
|
2822
|
+
}
|
|
2823
|
+
for (const w of [...c.waiters]) if (c.queued <= w.below) {
|
|
2824
|
+
c.waiters.splice(c.waiters.indexOf(w), 1);
|
|
2825
|
+
w.resolve();
|
|
2826
|
+
}
|
|
2827
|
+
};
|
|
2828
|
+
const grant = (id, c, bytes) => {
|
|
2829
|
+
c.unconsumed -= bytes;
|
|
2830
|
+
c.toGrant += bytes;
|
|
2831
|
+
const send = () => {
|
|
2832
|
+
c.grantScheduled = false;
|
|
2833
|
+
if (!c.toGrant || !open.has(id)) return;
|
|
2378
2834
|
post({
|
|
2835
|
+
type: "credit",
|
|
2836
|
+
id,
|
|
2837
|
+
bytes: c.toGrant
|
|
2838
|
+
});
|
|
2839
|
+
c.toGrant = 0;
|
|
2840
|
+
};
|
|
2841
|
+
if (c.toGrant >= c.window / 4) {
|
|
2842
|
+
send();
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
if (c.grantScheduled) return;
|
|
2846
|
+
c.grantScheduled = true;
|
|
2847
|
+
setTimeout(send, 0);
|
|
2848
|
+
};
|
|
2849
|
+
const attach = (id, meta, portWindow, opener, exact) => {
|
|
2850
|
+
const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
|
|
2851
|
+
window: portWindow,
|
|
2852
|
+
enforced: false,
|
|
2853
|
+
opener,
|
|
2854
|
+
exact,
|
|
2855
|
+
outstanding: 0,
|
|
2856
|
+
queue: [],
|
|
2857
|
+
queued: 0,
|
|
2858
|
+
unconsumed: 0,
|
|
2859
|
+
waiters: [],
|
|
2860
|
+
toGrant: 0,
|
|
2861
|
+
grantScheduled: false
|
|
2862
|
+
} : void 0;
|
|
2863
|
+
const self = newVirtualPort((payload, transfer) => {
|
|
2864
|
+
touch(id);
|
|
2865
|
+
const envelope = {
|
|
2866
|
+
type: "message",
|
|
2867
|
+
id,
|
|
2868
|
+
payload
|
|
2869
|
+
};
|
|
2870
|
+
if (!c) {
|
|
2871
|
+
post(envelope, transfer);
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
2874
|
+
const bytes = c.exact ? codec.encode?.(envelope) : void 0;
|
|
2875
|
+
const size = bytes?.byteLength ?? estimateMessageSize(payload);
|
|
2876
|
+
const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
|
|
2877
|
+
const held = !bytes && waits ? {
|
|
2878
|
+
...envelope,
|
|
2879
|
+
payload: structuredClone(payload, { transfer })
|
|
2880
|
+
} : void 0;
|
|
2881
|
+
const send = () => {
|
|
2882
|
+
if (!bytes) {
|
|
2883
|
+
sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
if (frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
2887
|
+
for (let at = 0; at < bytes.byteLength; at += frameSize) {
|
|
2888
|
+
const data = bytes.subarray(at, at + frameSize);
|
|
2889
|
+
const frame = at + frameSize >= bytes.byteLength ? {
|
|
2890
|
+
type: "frame",
|
|
2891
|
+
id,
|
|
2892
|
+
data,
|
|
2893
|
+
last: true
|
|
2894
|
+
} : {
|
|
2895
|
+
type: "frame",
|
|
2896
|
+
id,
|
|
2897
|
+
data
|
|
2898
|
+
};
|
|
2899
|
+
sendOnPort(id, () => post(frame));
|
|
2900
|
+
}
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
sendOnPort(id, () => postBytes(bytes));
|
|
2904
|
+
};
|
|
2905
|
+
c.queue.push({
|
|
2906
|
+
send,
|
|
2907
|
+
size
|
|
2908
|
+
});
|
|
2909
|
+
c.queued += size;
|
|
2910
|
+
flush(c);
|
|
2911
|
+
}, (reason) => {
|
|
2912
|
+
release(reason);
|
|
2913
|
+
record.markClosed(reason);
|
|
2914
|
+
});
|
|
2915
|
+
function release(reason) {
|
|
2916
|
+
open.delete(id);
|
|
2917
|
+
leaks?.unregister(record);
|
|
2918
|
+
if (c?.queue.length) {
|
|
2919
|
+
for (const item of c.queue.splice(0)) item.send();
|
|
2920
|
+
c.queued = 0;
|
|
2921
|
+
}
|
|
2922
|
+
sendOnPort(id, () => post({
|
|
2379
2923
|
type: "close",
|
|
2380
2924
|
id,
|
|
2381
2925
|
reason
|
|
2926
|
+
}));
|
|
2927
|
+
releaseWaiters(c);
|
|
2928
|
+
}
|
|
2929
|
+
const { markClosed, isClosed, hasListeners } = self;
|
|
2930
|
+
if (c) {
|
|
2931
|
+
const deliver = self.deliver;
|
|
2932
|
+
self.deliver = (payload, size = estimateMessageSize(payload)) => {
|
|
2933
|
+
c.unconsumed += size;
|
|
2934
|
+
const done = deliver(payload);
|
|
2935
|
+
if (done) done.then(() => grant(id, c, size));
|
|
2936
|
+
else grant(id, c, size);
|
|
2937
|
+
return done;
|
|
2938
|
+
};
|
|
2939
|
+
setPortSendState(self.port, {
|
|
2940
|
+
bufferedAmount: () => c.queued,
|
|
2941
|
+
ready: (below = 0) => new Promise((resolve) => {
|
|
2942
|
+
if (c.queued <= below || isClosed()) nextTask(resolve);
|
|
2943
|
+
else c.waiters.push({
|
|
2944
|
+
below,
|
|
2945
|
+
resolve: () => nextTask(resolve)
|
|
2946
|
+
});
|
|
2947
|
+
})
|
|
2382
2948
|
});
|
|
2383
|
-
|
|
2384
|
-
|
|
2949
|
+
}
|
|
2950
|
+
const record = {
|
|
2951
|
+
deliver: self.deliver,
|
|
2952
|
+
markClosed,
|
|
2953
|
+
isClosed,
|
|
2954
|
+
hasListeners,
|
|
2955
|
+
close: (reason) => {
|
|
2956
|
+
if (isClosed()) return;
|
|
2957
|
+
release(reason);
|
|
2958
|
+
markClosed(reason);
|
|
2959
|
+
}
|
|
2960
|
+
};
|
|
2961
|
+
leaks?.register(self.port, {
|
|
2962
|
+
id,
|
|
2963
|
+
record,
|
|
2964
|
+
release
|
|
2965
|
+
}, record);
|
|
2966
|
+
const now = Date.now();
|
|
2967
|
+
open.set(id, {
|
|
2968
|
+
id,
|
|
2969
|
+
meta,
|
|
2970
|
+
openedAt: now,
|
|
2971
|
+
lastActivity: now,
|
|
2972
|
+
handle: record,
|
|
2973
|
+
credit: c
|
|
2385
2974
|
});
|
|
2386
|
-
open.set(id, self);
|
|
2387
2975
|
return self;
|
|
2388
2976
|
};
|
|
2389
|
-
const handleEnvelope = (envelope) => {
|
|
2977
|
+
const handleEnvelope = (envelope, event) => {
|
|
2390
2978
|
if (muxClosed) return;
|
|
2979
|
+
heardSincePing = true;
|
|
2980
|
+
if (envelope.type === "ping" || envelope.type === "pong") {
|
|
2981
|
+
peerKeepsAlive = true;
|
|
2982
|
+
if (envelope.type === "ping") post({
|
|
2983
|
+
type: "pong",
|
|
2984
|
+
id: KEEPALIVE_ID
|
|
2985
|
+
});
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2391
2988
|
if (envelope.type === "open") {
|
|
2392
2989
|
if (open.has(envelope.id)) return;
|
|
2393
2990
|
if (open.size >= maxPorts) {
|
|
@@ -2398,7 +2995,18 @@
|
|
|
2398
2995
|
});
|
|
2399
2996
|
return;
|
|
2400
2997
|
}
|
|
2401
|
-
const handle = attach(envelope.id);
|
|
2998
|
+
const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
|
|
2999
|
+
const credit = open.get(envelope.id)?.credit;
|
|
3000
|
+
if (credit) post(credit.exact ? {
|
|
3001
|
+
type: "credit",
|
|
3002
|
+
id: envelope.id,
|
|
3003
|
+
bytes: 0,
|
|
3004
|
+
exact: true
|
|
3005
|
+
} : {
|
|
3006
|
+
type: "credit",
|
|
3007
|
+
id: envelope.id,
|
|
3008
|
+
bytes: 0
|
|
3009
|
+
});
|
|
2402
3010
|
let accepted = false;
|
|
2403
3011
|
if (onPort) try {
|
|
2404
3012
|
accepted = onPort(handle.port, envelope.meta) !== false;
|
|
@@ -2413,57 +3021,191 @@
|
|
|
2413
3021
|
id: envelope.id,
|
|
2414
3022
|
reason: "rejected"
|
|
2415
3023
|
});
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
const entry = open.get(envelope.id);
|
|
3029
|
+
if (envelope.type === "credit") {
|
|
3030
|
+
const c = entry?.credit;
|
|
3031
|
+
if (!c || !(envelope.bytes >= 0)) return;
|
|
3032
|
+
if (!c.enforced && c.opener) {
|
|
3033
|
+
if (c.exact && envelope.exact !== true) {
|
|
3034
|
+
c.exact = false;
|
|
3035
|
+
c.outstanding = 0;
|
|
3036
|
+
}
|
|
3037
|
+
post({
|
|
3038
|
+
type: "credit",
|
|
3039
|
+
id: envelope.id,
|
|
3040
|
+
bytes: 0
|
|
3041
|
+
});
|
|
2416
3042
|
}
|
|
3043
|
+
c.enforced = true;
|
|
3044
|
+
c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
|
|
3045
|
+
flush(c);
|
|
2417
3046
|
return;
|
|
2418
3047
|
}
|
|
2419
|
-
const handle = open.get(envelope.id);
|
|
2420
3048
|
if (envelope.type === "message") {
|
|
2421
|
-
|
|
2422
|
-
|
|
3049
|
+
const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
|
|
3050
|
+
if (!entry) return;
|
|
3051
|
+
touch(envelope.id);
|
|
3052
|
+
entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
|
|
2423
3053
|
return;
|
|
2424
3054
|
}
|
|
2425
|
-
if (
|
|
3055
|
+
if (envelope.type === "frame") {
|
|
3056
|
+
if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
|
|
3057
|
+
touch(envelope.id);
|
|
3058
|
+
const partial = entry.partial ?? {
|
|
3059
|
+
chunks: [],
|
|
3060
|
+
bytes: 0
|
|
3061
|
+
};
|
|
3062
|
+
entry.partial = partial;
|
|
3063
|
+
partial.chunks.push(envelope.data);
|
|
3064
|
+
partial.bytes += envelope.data.byteLength;
|
|
3065
|
+
if (!envelope.last) return;
|
|
3066
|
+
entry.partial = void 0;
|
|
3067
|
+
const whole = new Uint8Array(partial.bytes);
|
|
3068
|
+
let at = 0;
|
|
3069
|
+
for (const chunk of partial.chunks) {
|
|
3070
|
+
whole.set(chunk, at);
|
|
3071
|
+
at += chunk.byteLength;
|
|
3072
|
+
}
|
|
3073
|
+
const inner = codec.decode(whole);
|
|
3074
|
+
if (inner?.type !== "message" || inner.id !== envelope.id) return;
|
|
3075
|
+
entry.handle.deliver(inner.payload, whole.byteLength);
|
|
3076
|
+
return;
|
|
3077
|
+
}
|
|
3078
|
+
dropOutgoing(envelope.id);
|
|
3079
|
+
if (!entry) return;
|
|
2426
3080
|
open.delete(envelope.id);
|
|
2427
|
-
|
|
3081
|
+
releaseWaiters(entry.credit);
|
|
3082
|
+
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
|
|
2428
3083
|
};
|
|
2429
3084
|
const listener = (event) => {
|
|
2430
3085
|
const envelope = codec.read(event);
|
|
2431
|
-
if (envelope) handleEnvelope(envelope);
|
|
3086
|
+
if (envelope) handleEnvelope(envelope, event);
|
|
3087
|
+
};
|
|
3088
|
+
let peerKeepsAlive = false;
|
|
3089
|
+
let heardSincePing = true;
|
|
3090
|
+
let unanswered = 0;
|
|
3091
|
+
let pinger;
|
|
3092
|
+
const tick = () => {
|
|
3093
|
+
if (!keepalive) return;
|
|
3094
|
+
const backlogged = (port.bufferedAmount ?? 0) > 0;
|
|
3095
|
+
if (heardSincePing) unanswered = 0;
|
|
3096
|
+
else if (peerKeepsAlive && !backlogged) unanswered++;
|
|
3097
|
+
if (unanswered >= misses) {
|
|
3098
|
+
close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
heardSincePing = false;
|
|
3102
|
+
post({
|
|
3103
|
+
type: "ping",
|
|
3104
|
+
id: KEEPALIVE_ID
|
|
3105
|
+
});
|
|
3106
|
+
};
|
|
3107
|
+
const close = async (reason) => {
|
|
3108
|
+
if (muxClosed) return;
|
|
3109
|
+
clearInterval(pinger);
|
|
3110
|
+
muxClosed = true;
|
|
3111
|
+
signal?.removeEventListener("abort", onTransportAbort);
|
|
3112
|
+
port.removeEventListener("message", listener);
|
|
3113
|
+
const closedNow = /* @__PURE__ */ new Set();
|
|
3114
|
+
for (const [id, { handle, credit }] of [...open]) {
|
|
3115
|
+
closedNow.add(id);
|
|
3116
|
+
codecPost({
|
|
3117
|
+
type: "close",
|
|
3118
|
+
id
|
|
3119
|
+
});
|
|
3120
|
+
open.delete(id);
|
|
3121
|
+
releaseWaiters(credit);
|
|
3122
|
+
handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
|
|
3123
|
+
}
|
|
3124
|
+
for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
|
|
3125
|
+
type: "close",
|
|
3126
|
+
id
|
|
3127
|
+
});
|
|
3128
|
+
outgoing.clear();
|
|
3129
|
+
ring.length = 0;
|
|
3130
|
+
await port.close?.();
|
|
3131
|
+
};
|
|
3132
|
+
const onTransportAbort = () => {
|
|
3133
|
+
close(signal?.reason).catch(() => {});
|
|
2432
3134
|
};
|
|
2433
3135
|
port.addEventListener("message", listener);
|
|
2434
3136
|
port.start?.();
|
|
3137
|
+
if (keepalive) {
|
|
3138
|
+
pinger = setInterval(tick, keepalive.intervalMs);
|
|
3139
|
+
pinger.unref?.();
|
|
3140
|
+
}
|
|
3141
|
+
if (signal?.aborted) close(signal.reason).catch(() => {});
|
|
3142
|
+
else signal?.addEventListener("abort", onTransportAbort, { once: true });
|
|
2435
3143
|
return {
|
|
2436
3144
|
maxMessageSize,
|
|
2437
|
-
async openPort(meta) {
|
|
3145
|
+
async openPort(meta, { signal: portSignal } = {}) {
|
|
2438
3146
|
if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
|
|
2439
|
-
if (
|
|
3147
|
+
if (portSignal?.aborted) throw portSignal.reason;
|
|
3148
|
+
if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
|
|
2440
3149
|
while (open.has(nextId)) nextId += 2;
|
|
2441
3150
|
const id = nextId;
|
|
2442
3151
|
nextId += 2;
|
|
2443
|
-
const handle = attach(id);
|
|
2444
|
-
post({
|
|
3152
|
+
const handle = attach(id, meta, window, true, exactCodec);
|
|
3153
|
+
post(window ? exactCodec ? {
|
|
3154
|
+
type: "open",
|
|
3155
|
+
id,
|
|
3156
|
+
meta,
|
|
3157
|
+
window,
|
|
3158
|
+
exact: true
|
|
3159
|
+
} : {
|
|
3160
|
+
type: "open",
|
|
3161
|
+
id,
|
|
3162
|
+
meta,
|
|
3163
|
+
window
|
|
3164
|
+
} : {
|
|
2445
3165
|
type: "open",
|
|
2446
3166
|
id,
|
|
2447
3167
|
meta
|
|
2448
3168
|
});
|
|
3169
|
+
if (portSignal) {
|
|
3170
|
+
const record = open.get(id)?.handle;
|
|
3171
|
+
const onAbort = () => {
|
|
3172
|
+
if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
|
|
3173
|
+
};
|
|
3174
|
+
portSignal.addEventListener("abort", onAbort, { once: true });
|
|
3175
|
+
getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
|
|
3176
|
+
}
|
|
2449
3177
|
return handle.port;
|
|
2450
3178
|
},
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
}
|
|
2461
|
-
muxClosed = true;
|
|
2462
|
-
port.removeEventListener("message", listener);
|
|
2463
|
-
await port.close?.();
|
|
3179
|
+
close,
|
|
3180
|
+
ports() {
|
|
3181
|
+
return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
|
|
3182
|
+
...info,
|
|
3183
|
+
...credit.enforced ? { window: credit.window } : {},
|
|
3184
|
+
queued: credit.queued,
|
|
3185
|
+
outstanding: credit.outstanding,
|
|
3186
|
+
unconsumed: credit.unconsumed
|
|
3187
|
+
} : info);
|
|
2464
3188
|
}
|
|
2465
3189
|
};
|
|
2466
3190
|
}
|
|
3191
|
+
/** The byte length of a received message's data, or `undefined` if it is not bytes. */
|
|
3192
|
+
function byteLengthOf(data) {
|
|
3193
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
|
|
3194
|
+
}
|
|
3195
|
+
/**
|
|
3196
|
+
* `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
|
|
3197
|
+
* a full table names who filled it. A port without a kind counts as `"?"`.
|
|
3198
|
+
*/
|
|
3199
|
+
function describeKinds(entries) {
|
|
3200
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3201
|
+
for (const { meta } of entries) {
|
|
3202
|
+
const kind = meta?.kind;
|
|
3203
|
+
const name = typeof kind === "string" ? kind : "?";
|
|
3204
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
3205
|
+
}
|
|
3206
|
+
if (!counts.size) return "none";
|
|
3207
|
+
return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
|
|
3208
|
+
}
|
|
2467
3209
|
/**
|
|
2468
3210
|
* Announced with every port this pair opens. Layer 1 never reads `meta` — it is
|
|
2469
3211
|
* here so a peer running something else over the same multiplexer can tell a
|
|
@@ -2485,9 +3227,22 @@
|
|
|
2485
3227
|
maxMessageSize: mux.maxMessageSize,
|
|
2486
3228
|
timeout
|
|
2487
3229
|
};
|
|
2488
|
-
const call = (input) =>
|
|
2489
|
-
|
|
2490
|
-
|
|
3230
|
+
const call = (input) => {
|
|
3231
|
+
let stream;
|
|
3232
|
+
let abandoned = false;
|
|
3233
|
+
return returningAtOnce((async function* () {
|
|
3234
|
+
const port = await mux.openPort(STREAM_META);
|
|
3235
|
+
if (abandoned) {
|
|
3236
|
+
await port.close?.();
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
stream = duplexOverPort(port, streamOptions)(input);
|
|
3240
|
+
yield* stream;
|
|
3241
|
+
})(), () => {
|
|
3242
|
+
abandoned = true;
|
|
3243
|
+
stream?.return(void 0).catch(() => {});
|
|
3244
|
+
});
|
|
3245
|
+
};
|
|
2491
3246
|
return {
|
|
2492
3247
|
call,
|
|
2493
3248
|
async close() {
|
|
@@ -2557,7 +3312,7 @@
|
|
|
2557
3312
|
const candidate = value;
|
|
2558
3313
|
if (typeof candidate.id !== "number") return false;
|
|
2559
3314
|
if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
|
|
2560
|
-
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
|
|
3315
|
+
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
|
|
2561
3316
|
}
|
|
2562
3317
|
/**
|
|
2563
3318
|
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
@@ -2578,6 +3333,27 @@
|
|
|
2578
3333
|
}
|
|
2579
3334
|
};
|
|
2580
3335
|
//#endregion
|
|
3336
|
+
//#region src/link/async-dispose.ts
|
|
3337
|
+
/**
|
|
3338
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
3339
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
3340
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
3341
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
3342
|
+
* declared types, which would require consumers to compile with the
|
|
3343
|
+
* `esnext.disposable` lib.
|
|
3344
|
+
*/
|
|
3345
|
+
function asyncDisposable(target, dispose) {
|
|
3346
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
3347
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
3348
|
+
value: async () => {
|
|
3349
|
+
await dispose();
|
|
3350
|
+
},
|
|
3351
|
+
configurable: true,
|
|
3352
|
+
writable: true
|
|
3353
|
+
});
|
|
3354
|
+
return target;
|
|
3355
|
+
}
|
|
3356
|
+
//#endregion
|
|
2581
3357
|
//#region src/link/http-link.ts
|
|
2582
3358
|
/**
|
|
2583
3359
|
* The version of the HTTP-over-`MessagePort` protocol this package speaks,
|
|
@@ -2607,11 +3383,12 @@
|
|
|
2607
3383
|
*/
|
|
2608
3384
|
const LINK_CLOSE = "webrun-http:link-close";
|
|
2609
3385
|
/** Opens the calling end of a link on `port` (one port per link). */
|
|
2610
|
-
async function connectHttpLink(port, { streamTimeout } = {}) {
|
|
3386
|
+
async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
|
|
2611
3387
|
const { call, close } = await connect({
|
|
2612
3388
|
mux: overPipe(port, {
|
|
2613
3389
|
codec: structuredCodec,
|
|
2614
|
-
side: "initiator"
|
|
3390
|
+
side: "initiator",
|
|
3391
|
+
maxPorts
|
|
2615
3392
|
}),
|
|
2616
3393
|
timeout: streamTimeout
|
|
2617
3394
|
});
|
|
@@ -2627,14 +3404,15 @@
|
|
|
2627
3404
|
* that abandons the requests still running (their handlers see the
|
|
2628
3405
|
* cancellation) and releases the port.
|
|
2629
3406
|
*/
|
|
2630
|
-
async function serveHttpLink(port, handler, { streamTimeout } = {}) {
|
|
3407
|
+
async function serveHttpLink(port, handler, { streamTimeout, maxPorts, requestIdleMs } = {}) {
|
|
2631
3408
|
const teardown = await serve({
|
|
2632
3409
|
mux: overPipe(port, {
|
|
2633
3410
|
codec: structuredCodec,
|
|
2634
|
-
side: "responder"
|
|
3411
|
+
side: "responder",
|
|
3412
|
+
maxPorts
|
|
2635
3413
|
}),
|
|
2636
3414
|
timeout: streamTimeout
|
|
2637
|
-
}, serveFetchOverDuplex(async (request) => await handler(request)));
|
|
3415
|
+
}, serveFetchOverDuplex(async (request) => await handler(request), { requestIdleMs }));
|
|
2638
3416
|
const onClose = (event) => {
|
|
2639
3417
|
if (event.data?.type === LINK_CLOSE) close();
|
|
2640
3418
|
};
|
|
@@ -2659,7 +3437,7 @@
|
|
|
2659
3437
|
inflight--;
|
|
2660
3438
|
if (inflight === 0) for (const resolve of idleWaiters.splice(0)) resolve();
|
|
2661
3439
|
};
|
|
2662
|
-
|
|
3440
|
+
const link = {
|
|
2663
3441
|
get inflight() {
|
|
2664
3442
|
return inflight;
|
|
2665
3443
|
},
|
|
@@ -2696,7 +3474,7 @@
|
|
|
2696
3474
|
finish();
|
|
2697
3475
|
return response;
|
|
2698
3476
|
}
|
|
2699
|
-
return new Response(guardBody(response.body, lost, () => lostReason, finish), {
|
|
3477
|
+
return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
|
|
2700
3478
|
status: response.status,
|
|
2701
3479
|
statusText: response.statusText,
|
|
2702
3480
|
headers: response.headers
|
|
@@ -2712,35 +3490,68 @@
|
|
|
2712
3490
|
} catch {}
|
|
2713
3491
|
}
|
|
2714
3492
|
};
|
|
3493
|
+
return asyncDisposable(link, () => link.close());
|
|
2715
3494
|
}
|
|
2716
3495
|
/**
|
|
2717
|
-
* A response body that reports its end (done, cancelled or
|
|
2718
|
-
* with the link's reason when the link is lost mid-stream.
|
|
3496
|
+
* A response body that reports its end (done, cancelled, failed or aborted) and
|
|
3497
|
+
* fails with the link's reason when the link is lost mid-stream. An abort of the
|
|
3498
|
+
* request's signal ends it at once, read or not: the exchange is over (the layer
|
|
3499
|
+
* below has given its port back), so the request leaves `inflight` then.
|
|
2719
3500
|
*/
|
|
2720
|
-
function guardBody(body, lost, reason, finish) {
|
|
3501
|
+
function guardBody(body, lost, reason, finish, signal) {
|
|
2721
3502
|
const reader = body.getReader();
|
|
3503
|
+
let controller;
|
|
3504
|
+
let ended = false;
|
|
3505
|
+
const end = () => {
|
|
3506
|
+
ended = true;
|
|
3507
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3508
|
+
finish();
|
|
3509
|
+
};
|
|
3510
|
+
const onAbort = () => {
|
|
3511
|
+
if (ended) return;
|
|
3512
|
+
end();
|
|
3513
|
+
reader.cancel().catch(() => {});
|
|
3514
|
+
try {
|
|
3515
|
+
controller.error(abortError(signal));
|
|
3516
|
+
} catch {}
|
|
3517
|
+
};
|
|
2722
3518
|
return new ReadableStream({
|
|
3519
|
+
start(c) {
|
|
3520
|
+
controller = c;
|
|
3521
|
+
if (signal?.aborted) onAbort();
|
|
3522
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
3523
|
+
},
|
|
2723
3524
|
async pull(controller) {
|
|
2724
3525
|
try {
|
|
2725
3526
|
const { done, value } = await Promise.race([reader.read(), lost]);
|
|
3527
|
+
if (ended) return;
|
|
2726
3528
|
if (done) {
|
|
2727
|
-
|
|
3529
|
+
end();
|
|
2728
3530
|
controller.close();
|
|
2729
3531
|
return;
|
|
2730
3532
|
}
|
|
2731
3533
|
controller.enqueue(value);
|
|
2732
3534
|
} catch (error) {
|
|
2733
|
-
|
|
3535
|
+
if (ended) return;
|
|
3536
|
+
end();
|
|
2734
3537
|
reader.cancel().catch(() => {});
|
|
2735
3538
|
controller.error(reason() ?? error);
|
|
2736
3539
|
}
|
|
2737
3540
|
},
|
|
2738
3541
|
async cancel(why) {
|
|
2739
|
-
|
|
3542
|
+
end();
|
|
2740
3543
|
await reader.cancel(why).catch(() => {});
|
|
2741
3544
|
}
|
|
2742
3545
|
});
|
|
2743
3546
|
}
|
|
3547
|
+
/** The signal's reason as an `Error` (`AbortError` when it gave none). */
|
|
3548
|
+
function abortError(signal) {
|
|
3549
|
+
const why = signal.reason;
|
|
3550
|
+
if (why instanceof Error) return why;
|
|
3551
|
+
const err = new Error(why === void 0 ? "Aborted" : String(why));
|
|
3552
|
+
err.name = "AbortError";
|
|
3553
|
+
return err;
|
|
3554
|
+
}
|
|
2744
3555
|
//#endregion
|
|
2745
3556
|
//#region src/link/locks.ts
|
|
2746
3557
|
function lockManager() {
|