@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.js
CHANGED
|
@@ -22,8 +22,6 @@ function deserializeError(error) {
|
|
|
22
22
|
const payload = typeof error === "string" ? { message: error } : error;
|
|
23
23
|
return Object.assign(new Error(payload.message), payload);
|
|
24
24
|
}
|
|
25
|
-
new TextEncoder();
|
|
26
|
-
new TextDecoder();
|
|
27
25
|
/**
|
|
28
26
|
* The newAsyncGenerator function creates async generators from callback-based initialization
|
|
29
27
|
* functions, providing a bridge between imperative event handling and declarative async
|
|
@@ -176,7 +174,6 @@ async function* newAsyncGenerator(init, skipValues = false) {
|
|
|
176
174
|
drainQueue();
|
|
177
175
|
}
|
|
178
176
|
}
|
|
179
|
-
new TextEncoder();
|
|
180
177
|
/**
|
|
181
178
|
* The iterator ↔ `ReadableStream` boundary.
|
|
182
179
|
*
|
|
@@ -446,8 +443,8 @@ var HttpParseError = class extends Error {
|
|
|
446
443
|
const NEWLINE = 10;
|
|
447
444
|
/** Mirrors the HTTP/1.1 codec's default `maxHeaderBytes`. */
|
|
448
445
|
const MAX_ENVELOPE_BYTES = 65536;
|
|
449
|
-
const encoder$1 = new TextEncoder();
|
|
450
|
-
const decoder = new TextDecoder();
|
|
446
|
+
const encoder$1 = /* @__PURE__ */ new TextEncoder();
|
|
447
|
+
const decoder = /* @__PURE__ */ new TextDecoder();
|
|
451
448
|
/**
|
|
452
449
|
* Encode an HTTP envelope plus optional body as one continuous byte stream:
|
|
453
450
|
*
|
|
@@ -701,15 +698,15 @@ function resolveFraming(headers, version) {
|
|
|
701
698
|
};
|
|
702
699
|
return { kind: "none" };
|
|
703
700
|
}
|
|
704
|
-
const CRLF = new Uint8Array([13, 10]);
|
|
705
|
-
const LAST_CHUNK = new Uint8Array([
|
|
701
|
+
const CRLF = /* @__PURE__ */ new Uint8Array([13, 10]);
|
|
702
|
+
const LAST_CHUNK = /* @__PURE__ */ new Uint8Array([
|
|
706
703
|
48,
|
|
707
704
|
13,
|
|
708
705
|
10,
|
|
709
706
|
13,
|
|
710
707
|
10
|
|
711
708
|
]);
|
|
712
|
-
const encoder = new TextEncoder();
|
|
709
|
+
const encoder = /* @__PURE__ */ new TextEncoder();
|
|
713
710
|
/**
|
|
714
711
|
* Chunk sizes are written in HEXADECIMAL — a 21-byte chunk is `15`. Writing
|
|
715
712
|
* them in decimal is the defect note 15 found in @libp2p/http, and it is the
|
|
@@ -1014,7 +1011,7 @@ function newHttpCodec(options = {}) {
|
|
|
1014
1011
|
decodeResponse: (input, o) => decodeResponse(input, opts, o.method)
|
|
1015
1012
|
};
|
|
1016
1013
|
}
|
|
1017
|
-
const httpCodec = newHttpCodec();
|
|
1014
|
+
const httpCodec = /* @__PURE__ */ newHttpCodec();
|
|
1018
1015
|
/**
|
|
1019
1016
|
* Dispatches on byte 0. The formats are self-identifying — a JSON envelope
|
|
1020
1017
|
* always begins `{`, which is not a token character and so can never begin an
|
|
@@ -1064,10 +1061,29 @@ function newSniffingCodec(options) {
|
|
|
1064
1061
|
* Writes HTTP/1.1; accepts HTTP/1.1 or the legacy JSON envelope. Used whenever
|
|
1065
1062
|
* no `codec` option is supplied.
|
|
1066
1063
|
*/
|
|
1067
|
-
const defaultCodec = newSniffingCodec({
|
|
1064
|
+
const defaultCodec = /* @__PURE__ */ newSniffingCodec({
|
|
1068
1065
|
write: httpCodec,
|
|
1069
1066
|
accept: [httpCodec, jsonEnvelopeCodec]
|
|
1070
1067
|
});
|
|
1068
|
+
/**
|
|
1069
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
1070
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
1071
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
1072
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
1073
|
+
* declared types, which would require consumers to compile with the
|
|
1074
|
+
* `esnext.disposable` lib.
|
|
1075
|
+
*/
|
|
1076
|
+
function asyncDisposable$2(target, dispose) {
|
|
1077
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
1078
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
1079
|
+
value: async () => {
|
|
1080
|
+
await dispose();
|
|
1081
|
+
},
|
|
1082
|
+
configurable: true,
|
|
1083
|
+
writable: true
|
|
1084
|
+
});
|
|
1085
|
+
return target;
|
|
1086
|
+
}
|
|
1071
1087
|
/** Carries the serialized JS error between two webrun peers. */
|
|
1072
1088
|
const PEER_ERROR_HEADER = "x-webrun-error";
|
|
1073
1089
|
/** Hard cap on the peer-error header (M3): see `encodeErrorResponse`. */
|
|
@@ -1145,41 +1161,105 @@ async function throwIfPeerError(result, output) {
|
|
|
1145
1161
|
*/
|
|
1146
1162
|
async function httpFetch(call, env, body, options = {}) {
|
|
1147
1163
|
const codec = options.codec ?? defaultCodec;
|
|
1164
|
+
const { signal } = options;
|
|
1165
|
+
if (signal?.aborted) throw abortReason$1(signal);
|
|
1148
1166
|
const output = call(codec.encodeRequest(env, body));
|
|
1149
|
-
|
|
1150
|
-
const release =
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1167
|
+
let released;
|
|
1168
|
+
const release = () => {
|
|
1169
|
+
released ??= (async () => {
|
|
1170
|
+
try {
|
|
1171
|
+
await output.return?.(void 0);
|
|
1172
|
+
} catch {}
|
|
1173
|
+
})();
|
|
1174
|
+
return released;
|
|
1154
1175
|
};
|
|
1176
|
+
let finished = false;
|
|
1177
|
+
const finish = () => {
|
|
1178
|
+
if (finished || released) return;
|
|
1179
|
+
finished = true;
|
|
1180
|
+
if (signal?.aborted) {
|
|
1181
|
+
release();
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
const onAbort = () => void release();
|
|
1185
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1186
|
+
drain(output).finally(() => signal?.removeEventListener("abort", onAbort));
|
|
1187
|
+
};
|
|
1188
|
+
let decoded;
|
|
1189
|
+
try {
|
|
1190
|
+
decoded = await untilAborted(codec.decodeResponse(output, { method: env.method }), signal);
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
if (signal?.aborted) release();
|
|
1193
|
+
else await release();
|
|
1194
|
+
throw error;
|
|
1195
|
+
}
|
|
1155
1196
|
const result = {
|
|
1156
1197
|
envelope: decoded.envelope,
|
|
1157
|
-
body: releasingOnExit(decoded.body, release),
|
|
1158
|
-
release
|
|
1198
|
+
body: releasingOnExit(decoded.body, finish, release),
|
|
1199
|
+
release,
|
|
1200
|
+
finish
|
|
1159
1201
|
};
|
|
1160
1202
|
await throwIfPeerError(result, output);
|
|
1161
1203
|
return result;
|
|
1162
1204
|
}
|
|
1205
|
+
/** `promise`, rejected with the signal's reason if it aborts first. */
|
|
1206
|
+
function untilAborted(promise, signal) {
|
|
1207
|
+
if (!signal) return promise;
|
|
1208
|
+
return new Promise((resolve, reject) => {
|
|
1209
|
+
const onAbort = () => reject(abortReason$1(signal));
|
|
1210
|
+
if (signal.aborted) return onAbort();
|
|
1211
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1212
|
+
promise.then((value) => {
|
|
1213
|
+
signal.removeEventListener("abort", onAbort);
|
|
1214
|
+
resolve(value);
|
|
1215
|
+
}, (error) => {
|
|
1216
|
+
signal.removeEventListener("abort", onAbort);
|
|
1217
|
+
reject(error);
|
|
1218
|
+
});
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
/** The signal's reason as an `Error` (`AbortError` when it gave none). */
|
|
1222
|
+
function abortReason$1(signal) {
|
|
1223
|
+
const reason = signal.reason;
|
|
1224
|
+
if (reason instanceof Error) return reason;
|
|
1225
|
+
const err = new Error(reason === void 0 ? "Aborted" : String(reason));
|
|
1226
|
+
err.name = "AbortError";
|
|
1227
|
+
return err;
|
|
1228
|
+
}
|
|
1163
1229
|
/**
|
|
1164
|
-
* `body`,
|
|
1230
|
+
* `body`, ending the call however it is left: read to its end, or left early.
|
|
1165
1231
|
*
|
|
1166
1232
|
* The body the codec hands back is a generator of its own over a `ByteReader`,
|
|
1167
|
-
* and
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
1170
|
-
*
|
|
1171
|
-
*
|
|
1172
|
-
*
|
|
1233
|
+
* and its ending never reaches the call's `output` (see `throwIfPeerError` for
|
|
1234
|
+
* why the codec must not do that itself). A caller that stopped reading — an
|
|
1235
|
+
* aborted `fetch`, a cancelled response stream — left the peer's producer
|
|
1236
|
+
* parked forever; and one that read the body to its end left the call itself
|
|
1237
|
+
* open, which on a multiplexed transport is one port per exchange until the
|
|
1238
|
+
* multiplexer refuses to open more (`maxPorts`).
|
|
1239
|
+
*
|
|
1240
|
+
* Left early, the call is abandoned (`.return()`): the peer's producer is
|
|
1241
|
+
* released. Read to its end, the call is not cut short but read to ITS end, in
|
|
1242
|
+
* the background: the peer ends it once its side of the exchange is over,
|
|
1243
|
+
* which may be later than the response — a handler that answered before it
|
|
1244
|
+
* read the request body is still receiving it (`HttpDataHandlerResult.settled`),
|
|
1245
|
+
* and abandoning the call would abort that upload.
|
|
1173
1246
|
*/
|
|
1174
|
-
async function* releasingOnExit(body, release) {
|
|
1247
|
+
async function* releasingOnExit(body, finish, release) {
|
|
1175
1248
|
let completed = false;
|
|
1176
1249
|
try {
|
|
1177
1250
|
yield* body;
|
|
1178
1251
|
completed = true;
|
|
1179
1252
|
} finally {
|
|
1180
|
-
if (
|
|
1253
|
+
if (completed) finish();
|
|
1254
|
+
else await release();
|
|
1181
1255
|
}
|
|
1182
1256
|
}
|
|
1257
|
+
/** Reads `output` to its end, discarding what is left, ignoring a failure. */
|
|
1258
|
+
async function drain(output) {
|
|
1259
|
+
try {
|
|
1260
|
+
for (let r = await output.next(); !r.done; r = await output.next());
|
|
1261
|
+
} catch {}
|
|
1262
|
+
}
|
|
1183
1263
|
/**
|
|
1184
1264
|
* Wrap an HTTP handler as a `Duplex` so it can be registered with any
|
|
1185
1265
|
* `webrun-streams-*` adapter's `serve`.
|
|
@@ -1207,6 +1287,7 @@ function httpServe(handler, options = {}) {
|
|
|
1207
1287
|
try {
|
|
1208
1288
|
yield* replyCodec.encodeResponse(result.envelope, result.body, { method: decoded.envelope.method });
|
|
1209
1289
|
completed = true;
|
|
1290
|
+
await result.settled?.();
|
|
1210
1291
|
} finally {
|
|
1211
1292
|
if (!completed) try {
|
|
1212
1293
|
await result.release?.();
|
|
@@ -1430,14 +1511,18 @@ async function fetchOverDuplex(call, request, options = {}) {
|
|
|
1430
1511
|
const buffered = new Uint8Array(await request.arrayBuffer());
|
|
1431
1512
|
if (buffered.byteLength > 0) body = [buffered];
|
|
1432
1513
|
}
|
|
1433
|
-
const { envelope, body: respBody, release } = await httpFetch(call, env, body,
|
|
1514
|
+
const { envelope, body: respBody, release, finish } = await httpFetch(call, env, body, {
|
|
1515
|
+
...options,
|
|
1516
|
+
signal: request.signal ?? void 0
|
|
1517
|
+
});
|
|
1434
1518
|
const responseInit = {
|
|
1435
1519
|
status: envelope.status,
|
|
1436
1520
|
statusText: envelope.statusText,
|
|
1437
1521
|
headers: forwardableHeaders(envelope.headers)
|
|
1438
1522
|
};
|
|
1439
1523
|
if (request.method === "HEAD" || request.method === "OPTIONS" || NULL_BODY_STATUSES.has(envelope.status)) {
|
|
1440
|
-
|
|
1524
|
+
if (finish) finish();
|
|
1525
|
+
else await release?.();
|
|
1441
1526
|
return new Response(null, responseInit);
|
|
1442
1527
|
}
|
|
1443
1528
|
return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
|
|
@@ -1452,13 +1537,21 @@ function serveFetchOverDuplex(handler, options = {}) {
|
|
|
1452
1537
|
method: env.method,
|
|
1453
1538
|
headers: forwardableHeaders(env.headers)
|
|
1454
1539
|
};
|
|
1540
|
+
let upload;
|
|
1455
1541
|
if (env.method !== "GET" && env.method !== "HEAD") {
|
|
1456
1542
|
if (supportsRequestStreams()) {
|
|
1457
|
-
|
|
1543
|
+
upload = trackingEnd(body);
|
|
1544
|
+
reqInit.body = asyncIterableToReadable(upload.source);
|
|
1458
1545
|
reqInit.duplex = "half";
|
|
1459
1546
|
} else reqInit.body = await collectBytes(body);
|
|
1460
1547
|
}
|
|
1461
|
-
const
|
|
1548
|
+
const request = new Request(env.url, reqInit);
|
|
1549
|
+
const response = await handler(request);
|
|
1550
|
+
const settled = async () => {
|
|
1551
|
+
if (!upload) return;
|
|
1552
|
+
if (!request.bodyUsed && !request.body?.locked) request.body?.cancel().catch(() => {});
|
|
1553
|
+
await upload.settle(options.requestIdleMs ?? 3e4);
|
|
1554
|
+
};
|
|
1462
1555
|
const respEnv = {
|
|
1463
1556
|
status: response.status,
|
|
1464
1557
|
statusText: response.statusText,
|
|
@@ -1468,16 +1561,101 @@ function serveFetchOverDuplex(handler, options = {}) {
|
|
|
1468
1561
|
await response.body?.cancel();
|
|
1469
1562
|
return {
|
|
1470
1563
|
envelope: respEnv,
|
|
1471
|
-
body: void 0
|
|
1564
|
+
body: void 0,
|
|
1565
|
+
settled
|
|
1472
1566
|
};
|
|
1473
1567
|
}
|
|
1474
1568
|
return {
|
|
1475
1569
|
envelope: respEnv,
|
|
1476
1570
|
body: response.body ? readableToAsyncIterable(response.body) : void 0,
|
|
1477
|
-
release: () => response.body?.cancel()
|
|
1571
|
+
release: () => response.body?.cancel(),
|
|
1572
|
+
settled
|
|
1478
1573
|
};
|
|
1479
1574
|
}, options);
|
|
1480
1575
|
}
|
|
1576
|
+
/**
|
|
1577
|
+
* `source`, and `settle(idleMs)`: resolves once it is done — read to its end,
|
|
1578
|
+
* failed, or abandoned (`return()` included before the first `next()`, which a
|
|
1579
|
+
* generator's `finally` would not see) — or once nobody has asked it for a
|
|
1580
|
+
* chunk for `idleMs`, in which case it is abandoned: later reads fail.
|
|
1581
|
+
*
|
|
1582
|
+
* Idle counts only while no `next()` is pending: a reader waiting for a slow
|
|
1583
|
+
* sender is still reading.
|
|
1584
|
+
*/
|
|
1585
|
+
function trackingEnd(source) {
|
|
1586
|
+
let end = () => {};
|
|
1587
|
+
let isEnded = false;
|
|
1588
|
+
const ended = new Promise((resolve) => {
|
|
1589
|
+
end = () => {
|
|
1590
|
+
isEnded = true;
|
|
1591
|
+
resolve();
|
|
1592
|
+
};
|
|
1593
|
+
});
|
|
1594
|
+
let abandoned;
|
|
1595
|
+
let pending = 0;
|
|
1596
|
+
let idleSince = Date.now();
|
|
1597
|
+
const it = source[Symbol.asyncIterator]();
|
|
1598
|
+
const abandon = (reason) => {
|
|
1599
|
+
abandoned ??= reason;
|
|
1600
|
+
end();
|
|
1601
|
+
Promise.resolve(it.return?.(void 0)).catch(() => {});
|
|
1602
|
+
};
|
|
1603
|
+
const tracked = {
|
|
1604
|
+
[Symbol.asyncIterator]() {
|
|
1605
|
+
return tracked;
|
|
1606
|
+
},
|
|
1607
|
+
async next() {
|
|
1608
|
+
if (abandoned) throw abandoned;
|
|
1609
|
+
pending++;
|
|
1610
|
+
try {
|
|
1611
|
+
const r = await it.next();
|
|
1612
|
+
if (r.done) end();
|
|
1613
|
+
return r;
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
end();
|
|
1616
|
+
throw error;
|
|
1617
|
+
} finally {
|
|
1618
|
+
if (--pending === 0) idleSince = Date.now();
|
|
1619
|
+
}
|
|
1620
|
+
},
|
|
1621
|
+
async return(value) {
|
|
1622
|
+
end();
|
|
1623
|
+
try {
|
|
1624
|
+
await it.return?.(value);
|
|
1625
|
+
} catch {}
|
|
1626
|
+
return {
|
|
1627
|
+
done: true,
|
|
1628
|
+
value: void 0
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1632
|
+
asyncDisposable$2(tracked, () => tracked.return?.() ?? Promise.resolve());
|
|
1633
|
+
const settle = (idleMs) => {
|
|
1634
|
+
if (isEnded || !Number.isFinite(idleMs)) return ended;
|
|
1635
|
+
return new Promise((resolve) => {
|
|
1636
|
+
let timer;
|
|
1637
|
+
const check = () => {
|
|
1638
|
+
if (isEnded) return resolve();
|
|
1639
|
+
const idle = pending === 0 ? Date.now() - idleSince : 0;
|
|
1640
|
+
if (idle >= idleMs) {
|
|
1641
|
+
abandon(/* @__PURE__ */ new Error(`webrun-http-streams: the request body was left unread for ${idleMs} ms after the response; abandoned`));
|
|
1642
|
+
return resolve();
|
|
1643
|
+
}
|
|
1644
|
+
timer = setTimeout(check, Math.max(1, Math.min(idleMs - idle, 1e3)));
|
|
1645
|
+
timer.unref?.();
|
|
1646
|
+
};
|
|
1647
|
+
ended.then(() => {
|
|
1648
|
+
clearTimeout(timer);
|
|
1649
|
+
resolve();
|
|
1650
|
+
});
|
|
1651
|
+
check();
|
|
1652
|
+
});
|
|
1653
|
+
};
|
|
1654
|
+
return {
|
|
1655
|
+
source: tracked,
|
|
1656
|
+
settle
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1481
1659
|
async function* withAbort(iter, signal) {
|
|
1482
1660
|
if (!signal) {
|
|
1483
1661
|
yield* iter;
|
|
@@ -1488,13 +1666,6 @@ async function* withAbort(iter, signal) {
|
|
|
1488
1666
|
yield chunk;
|
|
1489
1667
|
}
|
|
1490
1668
|
}
|
|
1491
|
-
function abortReason$1(signal) {
|
|
1492
|
-
const reason = signal.reason;
|
|
1493
|
-
if (reason instanceof Error) return reason;
|
|
1494
|
-
const err = new Error(reason === void 0 ? "Aborted" : String(reason));
|
|
1495
|
-
err.name = "AbortError";
|
|
1496
|
-
return err;
|
|
1497
|
-
}
|
|
1498
1669
|
//#endregion
|
|
1499
1670
|
//#region ../webrun-rpc/dist/index.js
|
|
1500
1671
|
const closeSignals = /* @__PURE__ */ new WeakMap();
|
|
@@ -1649,6 +1820,69 @@ async function* throughAbort(input, signal) {
|
|
|
1649
1820
|
}
|
|
1650
1821
|
}
|
|
1651
1822
|
/**
|
|
1823
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
1824
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
1825
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
1826
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
1827
|
+
* declared types, which would require consumers to compile with the
|
|
1828
|
+
* `esnext.disposable` lib.
|
|
1829
|
+
*/
|
|
1830
|
+
function asyncDisposable$1(target, dispose) {
|
|
1831
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
1832
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
1833
|
+
value: async () => {
|
|
1834
|
+
await dispose();
|
|
1835
|
+
},
|
|
1836
|
+
configurable: true,
|
|
1837
|
+
writable: true
|
|
1838
|
+
});
|
|
1839
|
+
return target;
|
|
1840
|
+
}
|
|
1841
|
+
/**
|
|
1842
|
+
* `gen`, except that `return()` and `throw()` take effect at once.
|
|
1843
|
+
*
|
|
1844
|
+
* On an async generator both are queued behind a `next()` that is still
|
|
1845
|
+
* pending: a consumer that gives up on a stream while it waits for the peer's
|
|
1846
|
+
* next chunk — an aborted fetch whose response head has not come, say — cannot
|
|
1847
|
+
* end it until that chunk arrives, which for a peer that never answers is
|
|
1848
|
+
* never. The stream's `finally` (which closes its port and tells the peer)
|
|
1849
|
+
* then never runs, and on a multiplexer the port is kept for good.
|
|
1850
|
+
*
|
|
1851
|
+
* So `abandon` runs first, synchronously: it must make the pending `next()`
|
|
1852
|
+
* settle (abort the stream), which then resolves done, after which the
|
|
1853
|
+
* generator's own `return()` / `throw()` proceeds as usual. On a generator
|
|
1854
|
+
* suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
|
|
1855
|
+
*/
|
|
1856
|
+
function returningAtOnce(gen, abandon) {
|
|
1857
|
+
let abandoned = false;
|
|
1858
|
+
const giveUp = (reason) => {
|
|
1859
|
+
if (abandoned) return;
|
|
1860
|
+
abandoned = true;
|
|
1861
|
+
abandon(reason);
|
|
1862
|
+
};
|
|
1863
|
+
const wrapped = {
|
|
1864
|
+
next: (...args) => gen.next(...args).catch((error) => {
|
|
1865
|
+
if (abandoned) return {
|
|
1866
|
+
done: true,
|
|
1867
|
+
value: void 0
|
|
1868
|
+
};
|
|
1869
|
+
throw error;
|
|
1870
|
+
}),
|
|
1871
|
+
return(value) {
|
|
1872
|
+
giveUp();
|
|
1873
|
+
return gen.return(value);
|
|
1874
|
+
},
|
|
1875
|
+
throw(error) {
|
|
1876
|
+
giveUp(error);
|
|
1877
|
+
return gen.throw(error);
|
|
1878
|
+
},
|
|
1879
|
+
[Symbol.asyncIterator]() {
|
|
1880
|
+
return wrapped;
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
return asyncDisposable$1(wrapped, () => wrapped.return(void 0));
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1652
1886
|
* The `type` of the out-of-band notice a side posts when it abandons a stream.
|
|
1653
1887
|
*
|
|
1654
1888
|
* Layer 1's `close` is not observable to layer 2 — a closed virtual port drops
|
|
@@ -1744,7 +1978,7 @@ function runCallerSide(port, input, options) {
|
|
|
1744
1978
|
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
1745
1979
|
const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
|
|
1746
1980
|
pump.catch(() => {});
|
|
1747
|
-
return (async function* () {
|
|
1981
|
+
return returningAtOnce((async function* () {
|
|
1748
1982
|
try {
|
|
1749
1983
|
yield* inbound.stream;
|
|
1750
1984
|
} finally {
|
|
@@ -1758,7 +1992,9 @@ function runCallerSide(port, input, options) {
|
|
|
1758
1992
|
await port.close?.();
|
|
1759
1993
|
} catch {}
|
|
1760
1994
|
}
|
|
1761
|
-
})()
|
|
1995
|
+
})(), () => {
|
|
1996
|
+
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
1997
|
+
});
|
|
1762
1998
|
}
|
|
1763
1999
|
/**
|
|
1764
2000
|
* Listens for the peer's abort notice, and can post our own. The notice is a
|
|
@@ -2154,9 +2390,22 @@ const connect = async ({ mux: factory, timeout }) => {
|
|
|
2154
2390
|
maxMessageSize: mux.maxMessageSize,
|
|
2155
2391
|
timeout
|
|
2156
2392
|
};
|
|
2157
|
-
const call = (input) =>
|
|
2158
|
-
|
|
2159
|
-
|
|
2393
|
+
const call = (input) => {
|
|
2394
|
+
let stream;
|
|
2395
|
+
let abandoned = false;
|
|
2396
|
+
return returningAtOnce((async function* () {
|
|
2397
|
+
const port = await mux.openPort(STREAM_META);
|
|
2398
|
+
if (abandoned) {
|
|
2399
|
+
await port.close?.();
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
stream = duplexOverPort(port, streamOptions)(input);
|
|
2403
|
+
yield* stream;
|
|
2404
|
+
})(), () => {
|
|
2405
|
+
abandoned = true;
|
|
2406
|
+
stream?.return(void 0).catch(() => {});
|
|
2407
|
+
});
|
|
2408
|
+
};
|
|
2160
2409
|
return {
|
|
2161
2410
|
call,
|
|
2162
2411
|
async close() {
|
|
@@ -2247,6 +2496,27 @@ const structuredCodec = {
|
|
|
2247
2496
|
}
|
|
2248
2497
|
};
|
|
2249
2498
|
//#endregion
|
|
2499
|
+
//#region src/link/async-dispose.ts
|
|
2500
|
+
/**
|
|
2501
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
2502
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
2503
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
2504
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
2505
|
+
* declared types, which would require consumers to compile with the
|
|
2506
|
+
* `esnext.disposable` lib.
|
|
2507
|
+
*/
|
|
2508
|
+
function asyncDisposable(target, dispose) {
|
|
2509
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
2510
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
2511
|
+
value: async () => {
|
|
2512
|
+
await dispose();
|
|
2513
|
+
},
|
|
2514
|
+
configurable: true,
|
|
2515
|
+
writable: true
|
|
2516
|
+
});
|
|
2517
|
+
return target;
|
|
2518
|
+
}
|
|
2519
|
+
//#endregion
|
|
2250
2520
|
//#region src/link/http-link.ts
|
|
2251
2521
|
/**
|
|
2252
2522
|
* The version of the HTTP-over-`MessagePort` protocol this package speaks,
|
|
@@ -2276,11 +2546,12 @@ var LinkLostError = class extends Error {
|
|
|
2276
2546
|
*/
|
|
2277
2547
|
const LINK_CLOSE = "webrun-http:link-close";
|
|
2278
2548
|
/** Opens the calling end of a link on `port` (one port per link). */
|
|
2279
|
-
async function connectHttpLink(port, { streamTimeout } = {}) {
|
|
2549
|
+
async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
|
|
2280
2550
|
const { call, close } = await connect({
|
|
2281
2551
|
mux: overPipe(port, {
|
|
2282
2552
|
codec: structuredCodec,
|
|
2283
|
-
side: "initiator"
|
|
2553
|
+
side: "initiator",
|
|
2554
|
+
maxPorts
|
|
2284
2555
|
}),
|
|
2285
2556
|
timeout: streamTimeout
|
|
2286
2557
|
});
|
|
@@ -2296,14 +2567,15 @@ async function connectHttpLink(port, { streamTimeout } = {}) {
|
|
|
2296
2567
|
* that abandons the requests still running (their handlers see the
|
|
2297
2568
|
* cancellation) and releases the port.
|
|
2298
2569
|
*/
|
|
2299
|
-
async function serveHttpLink(port, handler, { streamTimeout } = {}) {
|
|
2570
|
+
async function serveHttpLink(port, handler, { streamTimeout, maxPorts, requestIdleMs } = {}) {
|
|
2300
2571
|
const teardown = await serve({
|
|
2301
2572
|
mux: overPipe(port, {
|
|
2302
2573
|
codec: structuredCodec,
|
|
2303
|
-
side: "responder"
|
|
2574
|
+
side: "responder",
|
|
2575
|
+
maxPorts
|
|
2304
2576
|
}),
|
|
2305
2577
|
timeout: streamTimeout
|
|
2306
|
-
}, serveFetchOverDuplex(async (request) => await handler(request)));
|
|
2578
|
+
}, serveFetchOverDuplex(async (request) => await handler(request), { requestIdleMs }));
|
|
2307
2579
|
const onClose = (event) => {
|
|
2308
2580
|
if (event.data?.type === LINK_CLOSE) close();
|
|
2309
2581
|
};
|
|
@@ -2328,7 +2600,7 @@ function trackedLink(send, release) {
|
|
|
2328
2600
|
inflight--;
|
|
2329
2601
|
if (inflight === 0) for (const resolve of idleWaiters.splice(0)) resolve();
|
|
2330
2602
|
};
|
|
2331
|
-
|
|
2603
|
+
const link = {
|
|
2332
2604
|
get inflight() {
|
|
2333
2605
|
return inflight;
|
|
2334
2606
|
},
|
|
@@ -2381,6 +2653,7 @@ function trackedLink(send, release) {
|
|
|
2381
2653
|
} catch {}
|
|
2382
2654
|
}
|
|
2383
2655
|
};
|
|
2656
|
+
return asyncDisposable(link, () => link.close());
|
|
2384
2657
|
}
|
|
2385
2658
|
/**
|
|
2386
2659
|
* A response body that reports its end (done, cancelled or failed) and fails
|