@statewalker/webrun-http-browser 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +327 -51
- 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 +203 -44
- package/dist/relay-worker.js +203 -44
- package/dist/sw-worker.js +324 -51
- package/dist/sw.js +323 -50
- package/dist/worker.js +324 -51
- package/package.json +4 -4
- package/src/link/async-dispose.ts +22 -0
- package/src/link/http-link.ts +22 -6
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;
|
|
1282
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));
|
|
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,14 +1639,18 @@
|
|
|
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
1656
|
return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
|
|
@@ -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,16 +1689,101 @@
|
|
|
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
|
}
|
|
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
|
+
};
|
|
1786
|
+
}
|
|
1609
1787
|
async function* withAbort(iter, signal) {
|
|
1610
1788
|
if (!signal) {
|
|
1611
1789
|
yield* iter;
|
|
@@ -1616,13 +1794,6 @@
|
|
|
1616
1794
|
yield chunk;
|
|
1617
1795
|
}
|
|
1618
1796
|
}
|
|
1619
|
-
function abortReason$1(signal) {
|
|
1620
|
-
const reason = signal.reason;
|
|
1621
|
-
if (reason instanceof Error) return reason;
|
|
1622
|
-
const err = new Error(reason === void 0 ? "Aborted" : String(reason));
|
|
1623
|
-
err.name = "AbortError";
|
|
1624
|
-
return err;
|
|
1625
|
-
}
|
|
1626
1797
|
//#endregion
|
|
1627
1798
|
//#region src/core/data-channels.ts
|
|
1628
1799
|
const MESSAGE_TYPE_REQUEST = "REQUEST";
|
|
@@ -1980,6 +2151,69 @@
|
|
|
1980
2151
|
}
|
|
1981
2152
|
}
|
|
1982
2153
|
/**
|
|
2154
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
2155
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
2156
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
2157
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
2158
|
+
* declared types, which would require consumers to compile with the
|
|
2159
|
+
* `esnext.disposable` lib.
|
|
2160
|
+
*/
|
|
2161
|
+
function asyncDisposable$1(target, dispose) {
|
|
2162
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
2163
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
2164
|
+
value: async () => {
|
|
2165
|
+
await dispose();
|
|
2166
|
+
},
|
|
2167
|
+
configurable: true,
|
|
2168
|
+
writable: true
|
|
2169
|
+
});
|
|
2170
|
+
return target;
|
|
2171
|
+
}
|
|
2172
|
+
/**
|
|
2173
|
+
* `gen`, except that `return()` and `throw()` take effect at once.
|
|
2174
|
+
*
|
|
2175
|
+
* On an async generator both are queued behind a `next()` that is still
|
|
2176
|
+
* pending: a consumer that gives up on a stream while it waits for the peer's
|
|
2177
|
+
* next chunk — an aborted fetch whose response head has not come, say — cannot
|
|
2178
|
+
* end it until that chunk arrives, which for a peer that never answers is
|
|
2179
|
+
* never. The stream's `finally` (which closes its port and tells the peer)
|
|
2180
|
+
* then never runs, and on a multiplexer the port is kept for good.
|
|
2181
|
+
*
|
|
2182
|
+
* So `abandon` runs first, synchronously: it must make the pending `next()`
|
|
2183
|
+
* settle (abort the stream), which then resolves done, after which the
|
|
2184
|
+
* generator's own `return()` / `throw()` proceeds as usual. On a generator
|
|
2185
|
+
* suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
|
|
2186
|
+
*/
|
|
2187
|
+
function returningAtOnce(gen, abandon) {
|
|
2188
|
+
let abandoned = false;
|
|
2189
|
+
const giveUp = (reason) => {
|
|
2190
|
+
if (abandoned) return;
|
|
2191
|
+
abandoned = true;
|
|
2192
|
+
abandon(reason);
|
|
2193
|
+
};
|
|
2194
|
+
const wrapped = {
|
|
2195
|
+
next: (...args) => gen.next(...args).catch((error) => {
|
|
2196
|
+
if (abandoned) return {
|
|
2197
|
+
done: true,
|
|
2198
|
+
value: void 0
|
|
2199
|
+
};
|
|
2200
|
+
throw error;
|
|
2201
|
+
}),
|
|
2202
|
+
return(value) {
|
|
2203
|
+
giveUp();
|
|
2204
|
+
return gen.return(value);
|
|
2205
|
+
},
|
|
2206
|
+
throw(error) {
|
|
2207
|
+
giveUp(error);
|
|
2208
|
+
return gen.throw(error);
|
|
2209
|
+
},
|
|
2210
|
+
[Symbol.asyncIterator]() {
|
|
2211
|
+
return wrapped;
|
|
2212
|
+
}
|
|
2213
|
+
};
|
|
2214
|
+
return asyncDisposable$1(wrapped, () => wrapped.return(void 0));
|
|
2215
|
+
}
|
|
2216
|
+
/**
|
|
1983
2217
|
* The `type` of the out-of-band notice a side posts when it abandons a stream.
|
|
1984
2218
|
*
|
|
1985
2219
|
* Layer 1's `close` is not observable to layer 2 — a closed virtual port drops
|
|
@@ -2075,7 +2309,7 @@
|
|
|
2075
2309
|
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
2076
2310
|
const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
|
|
2077
2311
|
pump.catch(() => {});
|
|
2078
|
-
return (async function* () {
|
|
2312
|
+
return returningAtOnce((async function* () {
|
|
2079
2313
|
try {
|
|
2080
2314
|
yield* inbound.stream;
|
|
2081
2315
|
} finally {
|
|
@@ -2089,7 +2323,9 @@
|
|
|
2089
2323
|
await port.close?.();
|
|
2090
2324
|
} catch {}
|
|
2091
2325
|
}
|
|
2092
|
-
})()
|
|
2326
|
+
})(), () => {
|
|
2327
|
+
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
2328
|
+
});
|
|
2093
2329
|
}
|
|
2094
2330
|
/**
|
|
2095
2331
|
* Listens for the peer's abort notice, and can post our own. The notice is a
|
|
@@ -2485,9 +2721,22 @@
|
|
|
2485
2721
|
maxMessageSize: mux.maxMessageSize,
|
|
2486
2722
|
timeout
|
|
2487
2723
|
};
|
|
2488
|
-
const call = (input) =>
|
|
2489
|
-
|
|
2490
|
-
|
|
2724
|
+
const call = (input) => {
|
|
2725
|
+
let stream;
|
|
2726
|
+
let abandoned = false;
|
|
2727
|
+
return returningAtOnce((async function* () {
|
|
2728
|
+
const port = await mux.openPort(STREAM_META);
|
|
2729
|
+
if (abandoned) {
|
|
2730
|
+
await port.close?.();
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
stream = duplexOverPort(port, streamOptions)(input);
|
|
2734
|
+
yield* stream;
|
|
2735
|
+
})(), () => {
|
|
2736
|
+
abandoned = true;
|
|
2737
|
+
stream?.return(void 0).catch(() => {});
|
|
2738
|
+
});
|
|
2739
|
+
};
|
|
2491
2740
|
return {
|
|
2492
2741
|
call,
|
|
2493
2742
|
async close() {
|
|
@@ -2578,6 +2827,27 @@
|
|
|
2578
2827
|
}
|
|
2579
2828
|
};
|
|
2580
2829
|
//#endregion
|
|
2830
|
+
//#region src/link/async-dispose.ts
|
|
2831
|
+
/**
|
|
2832
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
2833
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
2834
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
2835
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
2836
|
+
* declared types, which would require consumers to compile with the
|
|
2837
|
+
* `esnext.disposable` lib.
|
|
2838
|
+
*/
|
|
2839
|
+
function asyncDisposable(target, dispose) {
|
|
2840
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
2841
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
2842
|
+
value: async () => {
|
|
2843
|
+
await dispose();
|
|
2844
|
+
},
|
|
2845
|
+
configurable: true,
|
|
2846
|
+
writable: true
|
|
2847
|
+
});
|
|
2848
|
+
return target;
|
|
2849
|
+
}
|
|
2850
|
+
//#endregion
|
|
2581
2851
|
//#region src/link/http-link.ts
|
|
2582
2852
|
/**
|
|
2583
2853
|
* The version of the HTTP-over-`MessagePort` protocol this package speaks,
|
|
@@ -2607,11 +2877,12 @@
|
|
|
2607
2877
|
*/
|
|
2608
2878
|
const LINK_CLOSE = "webrun-http:link-close";
|
|
2609
2879
|
/** Opens the calling end of a link on `port` (one port per link). */
|
|
2610
|
-
async function connectHttpLink(port, { streamTimeout } = {}) {
|
|
2880
|
+
async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
|
|
2611
2881
|
const { call, close } = await connect({
|
|
2612
2882
|
mux: overPipe(port, {
|
|
2613
2883
|
codec: structuredCodec,
|
|
2614
|
-
side: "initiator"
|
|
2884
|
+
side: "initiator",
|
|
2885
|
+
maxPorts
|
|
2615
2886
|
}),
|
|
2616
2887
|
timeout: streamTimeout
|
|
2617
2888
|
});
|
|
@@ -2627,14 +2898,15 @@
|
|
|
2627
2898
|
* that abandons the requests still running (their handlers see the
|
|
2628
2899
|
* cancellation) and releases the port.
|
|
2629
2900
|
*/
|
|
2630
|
-
async function serveHttpLink(port, handler, { streamTimeout } = {}) {
|
|
2901
|
+
async function serveHttpLink(port, handler, { streamTimeout, maxPorts, requestIdleMs } = {}) {
|
|
2631
2902
|
const teardown = await serve({
|
|
2632
2903
|
mux: overPipe(port, {
|
|
2633
2904
|
codec: structuredCodec,
|
|
2634
|
-
side: "responder"
|
|
2905
|
+
side: "responder",
|
|
2906
|
+
maxPorts
|
|
2635
2907
|
}),
|
|
2636
2908
|
timeout: streamTimeout
|
|
2637
|
-
}, serveFetchOverDuplex(async (request) => await handler(request)));
|
|
2909
|
+
}, serveFetchOverDuplex(async (request) => await handler(request), { requestIdleMs }));
|
|
2638
2910
|
const onClose = (event) => {
|
|
2639
2911
|
if (event.data?.type === LINK_CLOSE) close();
|
|
2640
2912
|
};
|
|
@@ -2659,7 +2931,7 @@
|
|
|
2659
2931
|
inflight--;
|
|
2660
2932
|
if (inflight === 0) for (const resolve of idleWaiters.splice(0)) resolve();
|
|
2661
2933
|
};
|
|
2662
|
-
|
|
2934
|
+
const link = {
|
|
2663
2935
|
get inflight() {
|
|
2664
2936
|
return inflight;
|
|
2665
2937
|
},
|
|
@@ -2712,6 +2984,7 @@
|
|
|
2712
2984
|
} catch {}
|
|
2713
2985
|
}
|
|
2714
2986
|
};
|
|
2987
|
+
return asyncDisposable(link, () => link.close());
|
|
2715
2988
|
}
|
|
2716
2989
|
/**
|
|
2717
2990
|
* A response body that reports its end (done, cancelled or failed) and fails
|