@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.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;
|
|
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));
|
|
1154
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,17 +1511,21 @@ 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
|
-
return new Response(
|
|
1528
|
+
return new Response(responseBody(respBody, request.signal, release), responseInit);
|
|
1444
1529
|
}
|
|
1445
1530
|
/**
|
|
1446
1531
|
* Wrap a `(Request) => Promise<Response>` handler as a `Duplex` so it can be
|
|
@@ -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,32 +1561,173 @@ 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
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
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
|
+
};
|
|
1490
1658
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1659
|
+
/**
|
|
1660
|
+
* How long a cancelled body waits for the read in flight before it abandons the call anyway.
|
|
1661
|
+
* The read's answer tells a body that has ended (the call is finished: an upload the handler
|
|
1662
|
+
* reads after answering goes on) from one that has not; a peer that sends nothing more never
|
|
1663
|
+
* answers it. A body whose end comes later than this after the cancel is abandoned too, so a
|
|
1664
|
+
* handler still streaming its response and still reading the upload then loses the upload.
|
|
1665
|
+
*/
|
|
1666
|
+
const CANCEL_GRACE_MS = 1e3;
|
|
1667
|
+
/**
|
|
1668
|
+
* The response body as a stream that the request's signal can end at any time.
|
|
1669
|
+
*
|
|
1670
|
+
* An abort errors the stream with the abort's reason at once — a read waiting
|
|
1671
|
+
* for a peer that sends nothing more rejects then, not when a next chunk
|
|
1672
|
+
* arrives, which may be never — and abandons the call (`release`), so on a
|
|
1673
|
+
* multiplexed transport its port is given back whether or not anyone still
|
|
1674
|
+
* reads the body. The listener is removed once the body ends, however it ends:
|
|
1675
|
+
* read to its end, cancelled, failed or aborted. A body nobody reads or cancels
|
|
1676
|
+
* keeps it until the signal aborts (and keeps the call open until then anyway).
|
|
1677
|
+
* A cancel abandons the call too — unless the read in flight comes back with the
|
|
1678
|
+
* body's end, which finishes it instead (see `cancel`).
|
|
1679
|
+
*/
|
|
1680
|
+
function responseBody(body, signal, release) {
|
|
1681
|
+
const it = body[Symbol.asyncIterator]();
|
|
1682
|
+
let controller;
|
|
1683
|
+
let ended = false;
|
|
1684
|
+
let reading = false;
|
|
1685
|
+
const end = () => {
|
|
1686
|
+
ended = true;
|
|
1687
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1688
|
+
};
|
|
1689
|
+
const onAbort = () => {
|
|
1690
|
+
if (ended) return;
|
|
1691
|
+
end();
|
|
1692
|
+
try {
|
|
1693
|
+
controller.error(abortReason$1(signal));
|
|
1694
|
+
} catch {}
|
|
1695
|
+
release?.();
|
|
1696
|
+
};
|
|
1697
|
+
return new ReadableStream({
|
|
1698
|
+
start(c) {
|
|
1699
|
+
controller = c;
|
|
1700
|
+
if (signal?.aborted) onAbort();
|
|
1701
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
1702
|
+
},
|
|
1703
|
+
async pull(c) {
|
|
1704
|
+
try {
|
|
1705
|
+
reading = true;
|
|
1706
|
+
const { done, value } = await it.next().finally(() => {
|
|
1707
|
+
reading = false;
|
|
1708
|
+
});
|
|
1709
|
+
if (ended) return;
|
|
1710
|
+
if (done) {
|
|
1711
|
+
end();
|
|
1712
|
+
c.close();
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
c.enqueue(value);
|
|
1716
|
+
} catch (err) {
|
|
1717
|
+
if (ended) return;
|
|
1718
|
+
end();
|
|
1719
|
+
c.error(err);
|
|
1720
|
+
}
|
|
1721
|
+
},
|
|
1722
|
+
cancel() {
|
|
1723
|
+
end();
|
|
1724
|
+
const returned = Promise.resolve(it.return?.()).catch(() => {});
|
|
1725
|
+
if (!reading) return;
|
|
1726
|
+
const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
|
|
1727
|
+
grace.unref?.();
|
|
1728
|
+
returned.finally(() => clearTimeout(grace));
|
|
1729
|
+
}
|
|
1730
|
+
});
|
|
1497
1731
|
}
|
|
1498
1732
|
//#endregion
|
|
1499
1733
|
//#region ../webrun-rpc/dist/index.js
|
|
@@ -1618,8 +1852,11 @@ function listenPort(port, handler, { channelName = "", log = () => {} } = {}) {
|
|
|
1618
1852
|
error
|
|
1619
1853
|
});
|
|
1620
1854
|
};
|
|
1621
|
-
|
|
1622
|
-
|
|
1855
|
+
const listener = (event) => {
|
|
1856
|
+
onMessage(event);
|
|
1857
|
+
};
|
|
1858
|
+
port.addEventListener("message", listener);
|
|
1859
|
+
return () => port.removeEventListener("message", listener);
|
|
1623
1860
|
}
|
|
1624
1861
|
/**
|
|
1625
1862
|
* Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
|
|
@@ -1649,6 +1886,69 @@ async function* throughAbort(input, signal) {
|
|
|
1649
1886
|
}
|
|
1650
1887
|
}
|
|
1651
1888
|
/**
|
|
1889
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
1890
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
1891
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
1892
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
1893
|
+
* declared types, which would require consumers to compile with the
|
|
1894
|
+
* `esnext.disposable` lib.
|
|
1895
|
+
*/
|
|
1896
|
+
function asyncDisposable$1(target, dispose) {
|
|
1897
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
1898
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
1899
|
+
value: async () => {
|
|
1900
|
+
await dispose();
|
|
1901
|
+
},
|
|
1902
|
+
configurable: true,
|
|
1903
|
+
writable: true
|
|
1904
|
+
});
|
|
1905
|
+
return target;
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* `gen`, except that `return()` and `throw()` take effect at once.
|
|
1909
|
+
*
|
|
1910
|
+
* On an async generator both are queued behind a `next()` that is still
|
|
1911
|
+
* pending: a consumer that gives up on a stream while it waits for the peer's
|
|
1912
|
+
* next chunk — an aborted fetch whose response head has not come, say — cannot
|
|
1913
|
+
* end it until that chunk arrives, which for a peer that never answers is
|
|
1914
|
+
* never. The stream's `finally` (which closes its port and tells the peer)
|
|
1915
|
+
* then never runs, and on a multiplexer the port is kept for good.
|
|
1916
|
+
*
|
|
1917
|
+
* So `abandon` runs first, synchronously: it must make the pending `next()`
|
|
1918
|
+
* settle (abort the stream), which then resolves done, after which the
|
|
1919
|
+
* generator's own `return()` / `throw()` proceeds as usual. On a generator
|
|
1920
|
+
* suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
|
|
1921
|
+
*/
|
|
1922
|
+
function returningAtOnce(gen, abandon) {
|
|
1923
|
+
let abandoned = false;
|
|
1924
|
+
const giveUp = (reason) => {
|
|
1925
|
+
if (abandoned) return;
|
|
1926
|
+
abandoned = true;
|
|
1927
|
+
abandon(reason);
|
|
1928
|
+
};
|
|
1929
|
+
const wrapped = {
|
|
1930
|
+
next: (...args) => gen.next(...args).catch((error) => {
|
|
1931
|
+
if (abandoned) return {
|
|
1932
|
+
done: true,
|
|
1933
|
+
value: void 0
|
|
1934
|
+
};
|
|
1935
|
+
throw error;
|
|
1936
|
+
}),
|
|
1937
|
+
return(value) {
|
|
1938
|
+
giveUp();
|
|
1939
|
+
return gen.return(value);
|
|
1940
|
+
},
|
|
1941
|
+
throw(error) {
|
|
1942
|
+
giveUp(error);
|
|
1943
|
+
return gen.throw(error);
|
|
1944
|
+
},
|
|
1945
|
+
[Symbol.asyncIterator]() {
|
|
1946
|
+
return wrapped;
|
|
1947
|
+
}
|
|
1948
|
+
};
|
|
1949
|
+
return asyncDisposable$1(wrapped, () => wrapped.return(void 0));
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1652
1952
|
* The `type` of the out-of-band notice a side posts when it abandons a stream.
|
|
1653
1953
|
*
|
|
1654
1954
|
* Layer 1's `close` is not observable to layer 2 — a closed virtual port drops
|
|
@@ -1744,7 +2044,7 @@ function runCallerSide(port, input, options) {
|
|
|
1744
2044
|
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
1745
2045
|
const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
|
|
1746
2046
|
pump.catch(() => {});
|
|
1747
|
-
return (async function* () {
|
|
2047
|
+
return returningAtOnce((async function* () {
|
|
1748
2048
|
try {
|
|
1749
2049
|
yield* inbound.stream;
|
|
1750
2050
|
} finally {
|
|
@@ -1758,7 +2058,9 @@ function runCallerSide(port, input, options) {
|
|
|
1758
2058
|
await port.close?.();
|
|
1759
2059
|
} catch {}
|
|
1760
2060
|
}
|
|
1761
|
-
})()
|
|
2061
|
+
})(), () => {
|
|
2062
|
+
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
2063
|
+
});
|
|
1762
2064
|
}
|
|
1763
2065
|
/**
|
|
1764
2066
|
* Listens for the peer's abort notice, and can post our own. The notice is a
|
|
@@ -1959,6 +2261,33 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
|
|
|
1959
2261
|
}
|
|
1960
2262
|
}
|
|
1961
2263
|
/**
|
|
2264
|
+
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
2265
|
+
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
2266
|
+
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
2267
|
+
* are clamped to 4 ms in browsers.
|
|
2268
|
+
*/
|
|
2269
|
+
const setImmediateFn = globalThis.setImmediate;
|
|
2270
|
+
/** @internal */
|
|
2271
|
+
const nextTask = setImmediateFn ? (fn) => {
|
|
2272
|
+
setImmediateFn(fn);
|
|
2273
|
+
} : (() => {
|
|
2274
|
+
let channel;
|
|
2275
|
+
const queue = [];
|
|
2276
|
+
return (fn) => {
|
|
2277
|
+
if (!channel) {
|
|
2278
|
+
channel = new MessageChannel();
|
|
2279
|
+
channel.port1.onmessage = () => queue.shift()?.();
|
|
2280
|
+
}
|
|
2281
|
+
queue.push(fn);
|
|
2282
|
+
channel.port2.postMessage(0);
|
|
2283
|
+
};
|
|
2284
|
+
})();
|
|
2285
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
2286
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
2287
|
+
function setPortSendState(port, state) {
|
|
2288
|
+
states.set(port, state);
|
|
2289
|
+
}
|
|
2290
|
+
/**
|
|
1962
2291
|
* One virtual port.
|
|
1963
2292
|
*
|
|
1964
2293
|
* `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
|
|
@@ -1973,6 +2302,14 @@ function newVirtualPort(send, requestClose) {
|
|
|
1973
2302
|
if (closing.signal.aborted) return;
|
|
1974
2303
|
closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
|
|
1975
2304
|
};
|
|
2305
|
+
const close = (reason) => {
|
|
2306
|
+
if (closed) return;
|
|
2307
|
+
closed = true;
|
|
2308
|
+
const notify = requestClose;
|
|
2309
|
+
listeners.clear();
|
|
2310
|
+
notify(reason);
|
|
2311
|
+
abort(reason);
|
|
2312
|
+
};
|
|
1976
2313
|
const port = {
|
|
1977
2314
|
addEventListener(_type, listener) {
|
|
1978
2315
|
listeners.add(listener);
|
|
@@ -1984,24 +2321,28 @@ function newVirtualPort(send, requestClose) {
|
|
|
1984
2321
|
if (closed) return;
|
|
1985
2322
|
send(message, transfer);
|
|
1986
2323
|
},
|
|
1987
|
-
close
|
|
1988
|
-
if (closed) return;
|
|
1989
|
-
closed = true;
|
|
1990
|
-
const notify = requestClose;
|
|
1991
|
-
listeners.clear();
|
|
1992
|
-
notify();
|
|
1993
|
-
abort();
|
|
1994
|
-
}
|
|
2324
|
+
close
|
|
1995
2325
|
};
|
|
1996
2326
|
setPortCloseSignal(port, closing.signal);
|
|
2327
|
+
const dispose = Symbol.dispose;
|
|
2328
|
+
if (dispose) Object.defineProperty(port, dispose, {
|
|
2329
|
+
value: () => close(),
|
|
2330
|
+
configurable: true
|
|
2331
|
+
});
|
|
1997
2332
|
return {
|
|
1998
2333
|
port,
|
|
1999
2334
|
deliver(payload) {
|
|
2000
2335
|
if (closed) return;
|
|
2001
2336
|
const event = new MessageEvent("message", { data: payload });
|
|
2337
|
+
let pending;
|
|
2002
2338
|
for (const listener of [...listeners]) try {
|
|
2003
|
-
listener(event);
|
|
2339
|
+
const result = listener(event);
|
|
2340
|
+
if (result && typeof result.then === "function") {
|
|
2341
|
+
pending ??= [];
|
|
2342
|
+
pending.push(result);
|
|
2343
|
+
}
|
|
2004
2344
|
} catch {}
|
|
2345
|
+
return pending ? Promise.allSettled(pending) : void 0;
|
|
2005
2346
|
},
|
|
2006
2347
|
markClosed(reason) {
|
|
2007
2348
|
closed = true;
|
|
@@ -2010,53 +2351,309 @@ function newVirtualPort(send, requestClose) {
|
|
|
2010
2351
|
},
|
|
2011
2352
|
isClosed() {
|
|
2012
2353
|
return closed;
|
|
2354
|
+
},
|
|
2355
|
+
hasListeners() {
|
|
2356
|
+
return listeners.size > 0;
|
|
2013
2357
|
}
|
|
2014
2358
|
};
|
|
2015
2359
|
}
|
|
2016
2360
|
/**
|
|
2017
|
-
* The
|
|
2018
|
-
*
|
|
2361
|
+
* The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
|
|
2362
|
+
* whose codec passes unknown envelopes through reads them as a close of that
|
|
2363
|
+
* id, which must therefore not be a real port (0 is the initiator's first).
|
|
2019
2364
|
*/
|
|
2020
|
-
const
|
|
2365
|
+
const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
2366
|
+
/** The longest delay a timer holds; past it `setInterval` fires at once. */
|
|
2367
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
2021
2368
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
2022
2369
|
const DEFAULT_MAX_PORTS = 1024;
|
|
2023
2370
|
/**
|
|
2371
|
+
* What a message costs a port's window when the codec cannot say
|
|
2372
|
+
* (`PortCodec.encode`): its bytes and string lengths, found in its values up
|
|
2373
|
+
* to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
|
|
2374
|
+
* from the same structured value, so they agree.
|
|
2375
|
+
*/
|
|
2376
|
+
function estimateMessageSize(payload) {
|
|
2377
|
+
let budget = 256;
|
|
2378
|
+
const walk = (v, depth) => {
|
|
2379
|
+
if (budget-- <= 0) return 0;
|
|
2380
|
+
if (typeof v === "string") return v.length;
|
|
2381
|
+
if (v instanceof ArrayBuffer) return v.byteLength;
|
|
2382
|
+
if (ArrayBuffer.isView(v)) return v.byteLength;
|
|
2383
|
+
if (v === null || typeof v !== "object" || depth >= 4) return 8;
|
|
2384
|
+
let n = 16;
|
|
2385
|
+
if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
|
|
2386
|
+
else for (const k in v) n += k.length + walk(v[k], depth + 1);
|
|
2387
|
+
return n;
|
|
2388
|
+
};
|
|
2389
|
+
return Math.max(64, walk(payload, 0));
|
|
2390
|
+
}
|
|
2391
|
+
/**
|
|
2024
2392
|
* The default `PortMux`: emulates multiplexing over a single port.
|
|
2025
2393
|
*
|
|
2026
2394
|
* A transport that already multiplexes natively supplies its own `PortMux`
|
|
2027
2395
|
* instead — this implementation is for transports that offer one pipe.
|
|
2028
2396
|
*/
|
|
2029
2397
|
function multiplexPort(port, options) {
|
|
2030
|
-
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
|
|
2398
|
+
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
|
|
2399
|
+
const misses = keepalive?.misses ?? 3;
|
|
2400
|
+
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`);
|
|
2401
|
+
if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
|
|
2031
2402
|
const open = /* @__PURE__ */ new Map();
|
|
2403
|
+
const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
|
|
2404
|
+
const entry = open.get(id);
|
|
2405
|
+
if (entry?.handle !== record) return;
|
|
2406
|
+
const { handle: _, credit: __, partial: ___, ...info } = entry;
|
|
2407
|
+
if (!record.hasListeners()) {
|
|
2408
|
+
release("leaked");
|
|
2409
|
+
record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
|
|
2410
|
+
}
|
|
2411
|
+
try {
|
|
2412
|
+
onLeak?.(info);
|
|
2413
|
+
} catch {}
|
|
2414
|
+
}) : void 0;
|
|
2415
|
+
const touch = (id) => {
|
|
2416
|
+
const entry = open.get(id);
|
|
2417
|
+
if (entry) entry.lastActivity = Date.now();
|
|
2418
|
+
};
|
|
2032
2419
|
let nextId = side === "initiator" ? 0 : 1;
|
|
2033
2420
|
let muxClosed = false;
|
|
2421
|
+
/** This end counts encoded bytes and reassembles frames. */
|
|
2422
|
+
const exactCodec = codec.encode != null && codec.decode != null;
|
|
2034
2423
|
const post = (envelope, transfer) => {
|
|
2035
2424
|
if (muxClosed) return;
|
|
2425
|
+
codecPost(envelope, transfer);
|
|
2426
|
+
};
|
|
2427
|
+
/** Like `post`, also while closing: the mux's own close notices. */
|
|
2428
|
+
const codecPost = (envelope, transfer) => {
|
|
2036
2429
|
try {
|
|
2037
2430
|
codec.post(port, envelope, transfer);
|
|
2038
2431
|
} catch {}
|
|
2039
2432
|
};
|
|
2040
|
-
const
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2433
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
2434
|
+
const ring = [];
|
|
2435
|
+
let pumping = false;
|
|
2436
|
+
const pump = async () => {
|
|
2437
|
+
pumping = true;
|
|
2438
|
+
try {
|
|
2439
|
+
while (ring.length && !muxClosed) {
|
|
2440
|
+
if (port.whenDrained) await port.whenDrained();
|
|
2441
|
+
if (muxClosed) break;
|
|
2442
|
+
const id = ring.shift();
|
|
2443
|
+
const queue = outgoing.get(id);
|
|
2444
|
+
const item = queue?.shift();
|
|
2445
|
+
if (queue?.length) ring.push(id);
|
|
2446
|
+
else outgoing.delete(id);
|
|
2447
|
+
item?.();
|
|
2448
|
+
}
|
|
2449
|
+
} catch (err) {
|
|
2450
|
+
close(err).catch(() => {});
|
|
2451
|
+
} finally {
|
|
2452
|
+
pumping = false;
|
|
2453
|
+
}
|
|
2454
|
+
};
|
|
2455
|
+
const sendOnPort = (id, item) => {
|
|
2456
|
+
if (frameSize == null) {
|
|
2457
|
+
item();
|
|
2458
|
+
return;
|
|
2459
|
+
}
|
|
2460
|
+
let queue = outgoing.get(id);
|
|
2461
|
+
if (!queue) {
|
|
2462
|
+
queue = [];
|
|
2463
|
+
outgoing.set(id, queue);
|
|
2464
|
+
ring.push(id);
|
|
2465
|
+
}
|
|
2466
|
+
queue.push(item);
|
|
2467
|
+
if (!pumping) pump();
|
|
2468
|
+
};
|
|
2469
|
+
const dropOutgoing = (id) => {
|
|
2470
|
+
if (!outgoing.delete(id)) return;
|
|
2471
|
+
const at = ring.indexOf(id);
|
|
2472
|
+
if (at >= 0) ring.splice(at, 1);
|
|
2473
|
+
};
|
|
2474
|
+
const postBytes = (bytes) => {
|
|
2475
|
+
if (muxClosed) return;
|
|
2476
|
+
try {
|
|
2477
|
+
port.postMessage(bytes);
|
|
2478
|
+
} catch {}
|
|
2479
|
+
};
|
|
2480
|
+
const releaseWaiters = (c) => {
|
|
2481
|
+
for (const w of c?.waiters.splice(0) ?? []) w.resolve();
|
|
2482
|
+
};
|
|
2483
|
+
const flush = (c) => {
|
|
2484
|
+
while (c.queue.length) {
|
|
2485
|
+
const next = c.queue[0];
|
|
2486
|
+
if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
2487
|
+
c.queue.shift();
|
|
2488
|
+
c.queued -= next.size;
|
|
2489
|
+
c.outstanding += next.size;
|
|
2490
|
+
next.send();
|
|
2491
|
+
}
|
|
2492
|
+
for (const w of [...c.waiters]) if (c.queued <= w.below) {
|
|
2493
|
+
c.waiters.splice(c.waiters.indexOf(w), 1);
|
|
2494
|
+
w.resolve();
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
const grant = (id, c, bytes) => {
|
|
2498
|
+
c.unconsumed -= bytes;
|
|
2499
|
+
c.toGrant += bytes;
|
|
2500
|
+
const send = () => {
|
|
2501
|
+
c.grantScheduled = false;
|
|
2502
|
+
if (!c.toGrant || !open.has(id)) return;
|
|
2047
2503
|
post({
|
|
2504
|
+
type: "credit",
|
|
2505
|
+
id,
|
|
2506
|
+
bytes: c.toGrant
|
|
2507
|
+
});
|
|
2508
|
+
c.toGrant = 0;
|
|
2509
|
+
};
|
|
2510
|
+
if (c.toGrant >= c.window / 4) {
|
|
2511
|
+
send();
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
if (c.grantScheduled) return;
|
|
2515
|
+
c.grantScheduled = true;
|
|
2516
|
+
setTimeout(send, 0);
|
|
2517
|
+
};
|
|
2518
|
+
const attach = (id, meta, portWindow, opener, exact) => {
|
|
2519
|
+
const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
|
|
2520
|
+
window: portWindow,
|
|
2521
|
+
enforced: false,
|
|
2522
|
+
opener,
|
|
2523
|
+
exact,
|
|
2524
|
+
outstanding: 0,
|
|
2525
|
+
queue: [],
|
|
2526
|
+
queued: 0,
|
|
2527
|
+
unconsumed: 0,
|
|
2528
|
+
waiters: [],
|
|
2529
|
+
toGrant: 0,
|
|
2530
|
+
grantScheduled: false
|
|
2531
|
+
} : void 0;
|
|
2532
|
+
const self = newVirtualPort((payload, transfer) => {
|
|
2533
|
+
touch(id);
|
|
2534
|
+
const envelope = {
|
|
2535
|
+
type: "message",
|
|
2536
|
+
id,
|
|
2537
|
+
payload
|
|
2538
|
+
};
|
|
2539
|
+
if (!c) {
|
|
2540
|
+
post(envelope, transfer);
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
const bytes = c.exact ? codec.encode?.(envelope) : void 0;
|
|
2544
|
+
const size = bytes?.byteLength ?? estimateMessageSize(payload);
|
|
2545
|
+
const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
|
|
2546
|
+
const held = !bytes && waits ? {
|
|
2547
|
+
...envelope,
|
|
2548
|
+
payload: structuredClone(payload, { transfer })
|
|
2549
|
+
} : void 0;
|
|
2550
|
+
const send = () => {
|
|
2551
|
+
if (!bytes) {
|
|
2552
|
+
sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
if (frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
2556
|
+
for (let at = 0; at < bytes.byteLength; at += frameSize) {
|
|
2557
|
+
const data = bytes.subarray(at, at + frameSize);
|
|
2558
|
+
const frame = at + frameSize >= bytes.byteLength ? {
|
|
2559
|
+
type: "frame",
|
|
2560
|
+
id,
|
|
2561
|
+
data,
|
|
2562
|
+
last: true
|
|
2563
|
+
} : {
|
|
2564
|
+
type: "frame",
|
|
2565
|
+
id,
|
|
2566
|
+
data
|
|
2567
|
+
};
|
|
2568
|
+
sendOnPort(id, () => post(frame));
|
|
2569
|
+
}
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
sendOnPort(id, () => postBytes(bytes));
|
|
2573
|
+
};
|
|
2574
|
+
c.queue.push({
|
|
2575
|
+
send,
|
|
2576
|
+
size
|
|
2577
|
+
});
|
|
2578
|
+
c.queued += size;
|
|
2579
|
+
flush(c);
|
|
2580
|
+
}, (reason) => {
|
|
2581
|
+
release(reason);
|
|
2582
|
+
record.markClosed(reason);
|
|
2583
|
+
});
|
|
2584
|
+
function release(reason) {
|
|
2585
|
+
open.delete(id);
|
|
2586
|
+
leaks?.unregister(record);
|
|
2587
|
+
if (c?.queue.length) {
|
|
2588
|
+
for (const item of c.queue.splice(0)) item.send();
|
|
2589
|
+
c.queued = 0;
|
|
2590
|
+
}
|
|
2591
|
+
sendOnPort(id, () => post({
|
|
2048
2592
|
type: "close",
|
|
2049
2593
|
id,
|
|
2050
2594
|
reason
|
|
2595
|
+
}));
|
|
2596
|
+
releaseWaiters(c);
|
|
2597
|
+
}
|
|
2598
|
+
const { markClosed, isClosed, hasListeners } = self;
|
|
2599
|
+
if (c) {
|
|
2600
|
+
const deliver = self.deliver;
|
|
2601
|
+
self.deliver = (payload, size = estimateMessageSize(payload)) => {
|
|
2602
|
+
c.unconsumed += size;
|
|
2603
|
+
const done = deliver(payload);
|
|
2604
|
+
if (done) done.then(() => grant(id, c, size));
|
|
2605
|
+
else grant(id, c, size);
|
|
2606
|
+
return done;
|
|
2607
|
+
};
|
|
2608
|
+
setPortSendState(self.port, {
|
|
2609
|
+
bufferedAmount: () => c.queued,
|
|
2610
|
+
ready: (below = 0) => new Promise((resolve) => {
|
|
2611
|
+
if (c.queued <= below || isClosed()) nextTask(resolve);
|
|
2612
|
+
else c.waiters.push({
|
|
2613
|
+
below,
|
|
2614
|
+
resolve: () => nextTask(resolve)
|
|
2615
|
+
});
|
|
2616
|
+
})
|
|
2051
2617
|
});
|
|
2052
|
-
|
|
2053
|
-
|
|
2618
|
+
}
|
|
2619
|
+
const record = {
|
|
2620
|
+
deliver: self.deliver,
|
|
2621
|
+
markClosed,
|
|
2622
|
+
isClosed,
|
|
2623
|
+
hasListeners,
|
|
2624
|
+
close: (reason) => {
|
|
2625
|
+
if (isClosed()) return;
|
|
2626
|
+
release(reason);
|
|
2627
|
+
markClosed(reason);
|
|
2628
|
+
}
|
|
2629
|
+
};
|
|
2630
|
+
leaks?.register(self.port, {
|
|
2631
|
+
id,
|
|
2632
|
+
record,
|
|
2633
|
+
release
|
|
2634
|
+
}, record);
|
|
2635
|
+
const now = Date.now();
|
|
2636
|
+
open.set(id, {
|
|
2637
|
+
id,
|
|
2638
|
+
meta,
|
|
2639
|
+
openedAt: now,
|
|
2640
|
+
lastActivity: now,
|
|
2641
|
+
handle: record,
|
|
2642
|
+
credit: c
|
|
2054
2643
|
});
|
|
2055
|
-
open.set(id, self);
|
|
2056
2644
|
return self;
|
|
2057
2645
|
};
|
|
2058
|
-
const handleEnvelope = (envelope) => {
|
|
2646
|
+
const handleEnvelope = (envelope, event) => {
|
|
2059
2647
|
if (muxClosed) return;
|
|
2648
|
+
heardSincePing = true;
|
|
2649
|
+
if (envelope.type === "ping" || envelope.type === "pong") {
|
|
2650
|
+
peerKeepsAlive = true;
|
|
2651
|
+
if (envelope.type === "ping") post({
|
|
2652
|
+
type: "pong",
|
|
2653
|
+
id: KEEPALIVE_ID
|
|
2654
|
+
});
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2060
2657
|
if (envelope.type === "open") {
|
|
2061
2658
|
if (open.has(envelope.id)) return;
|
|
2062
2659
|
if (open.size >= maxPorts) {
|
|
@@ -2067,7 +2664,18 @@ function multiplexPort(port, options) {
|
|
|
2067
2664
|
});
|
|
2068
2665
|
return;
|
|
2069
2666
|
}
|
|
2070
|
-
const handle = attach(envelope.id);
|
|
2667
|
+
const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
|
|
2668
|
+
const credit = open.get(envelope.id)?.credit;
|
|
2669
|
+
if (credit) post(credit.exact ? {
|
|
2670
|
+
type: "credit",
|
|
2671
|
+
id: envelope.id,
|
|
2672
|
+
bytes: 0,
|
|
2673
|
+
exact: true
|
|
2674
|
+
} : {
|
|
2675
|
+
type: "credit",
|
|
2676
|
+
id: envelope.id,
|
|
2677
|
+
bytes: 0
|
|
2678
|
+
});
|
|
2071
2679
|
let accepted = false;
|
|
2072
2680
|
if (onPort) try {
|
|
2073
2681
|
accepted = onPort(handle.port, envelope.meta) !== false;
|
|
@@ -2082,57 +2690,191 @@ function multiplexPort(port, options) {
|
|
|
2082
2690
|
id: envelope.id,
|
|
2083
2691
|
reason: "rejected"
|
|
2084
2692
|
});
|
|
2693
|
+
return;
|
|
2085
2694
|
}
|
|
2086
2695
|
return;
|
|
2087
2696
|
}
|
|
2088
|
-
const
|
|
2697
|
+
const entry = open.get(envelope.id);
|
|
2698
|
+
if (envelope.type === "credit") {
|
|
2699
|
+
const c = entry?.credit;
|
|
2700
|
+
if (!c || !(envelope.bytes >= 0)) return;
|
|
2701
|
+
if (!c.enforced && c.opener) {
|
|
2702
|
+
if (c.exact && envelope.exact !== true) {
|
|
2703
|
+
c.exact = false;
|
|
2704
|
+
c.outstanding = 0;
|
|
2705
|
+
}
|
|
2706
|
+
post({
|
|
2707
|
+
type: "credit",
|
|
2708
|
+
id: envelope.id,
|
|
2709
|
+
bytes: 0
|
|
2710
|
+
});
|
|
2711
|
+
}
|
|
2712
|
+
c.enforced = true;
|
|
2713
|
+
c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
|
|
2714
|
+
flush(c);
|
|
2715
|
+
return;
|
|
2716
|
+
}
|
|
2089
2717
|
if (envelope.type === "message") {
|
|
2090
|
-
|
|
2091
|
-
|
|
2718
|
+
const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
|
|
2719
|
+
if (!entry) return;
|
|
2720
|
+
touch(envelope.id);
|
|
2721
|
+
entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
|
|
2092
2722
|
return;
|
|
2093
2723
|
}
|
|
2094
|
-
if (
|
|
2724
|
+
if (envelope.type === "frame") {
|
|
2725
|
+
if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
|
|
2726
|
+
touch(envelope.id);
|
|
2727
|
+
const partial = entry.partial ?? {
|
|
2728
|
+
chunks: [],
|
|
2729
|
+
bytes: 0
|
|
2730
|
+
};
|
|
2731
|
+
entry.partial = partial;
|
|
2732
|
+
partial.chunks.push(envelope.data);
|
|
2733
|
+
partial.bytes += envelope.data.byteLength;
|
|
2734
|
+
if (!envelope.last) return;
|
|
2735
|
+
entry.partial = void 0;
|
|
2736
|
+
const whole = new Uint8Array(partial.bytes);
|
|
2737
|
+
let at = 0;
|
|
2738
|
+
for (const chunk of partial.chunks) {
|
|
2739
|
+
whole.set(chunk, at);
|
|
2740
|
+
at += chunk.byteLength;
|
|
2741
|
+
}
|
|
2742
|
+
const inner = codec.decode(whole);
|
|
2743
|
+
if (inner?.type !== "message" || inner.id !== envelope.id) return;
|
|
2744
|
+
entry.handle.deliver(inner.payload, whole.byteLength);
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
dropOutgoing(envelope.id);
|
|
2748
|
+
if (!entry) return;
|
|
2095
2749
|
open.delete(envelope.id);
|
|
2096
|
-
|
|
2750
|
+
releaseWaiters(entry.credit);
|
|
2751
|
+
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
|
|
2097
2752
|
};
|
|
2098
2753
|
const listener = (event) => {
|
|
2099
2754
|
const envelope = codec.read(event);
|
|
2100
|
-
if (envelope) handleEnvelope(envelope);
|
|
2755
|
+
if (envelope) handleEnvelope(envelope, event);
|
|
2756
|
+
};
|
|
2757
|
+
let peerKeepsAlive = false;
|
|
2758
|
+
let heardSincePing = true;
|
|
2759
|
+
let unanswered = 0;
|
|
2760
|
+
let pinger;
|
|
2761
|
+
const tick = () => {
|
|
2762
|
+
if (!keepalive) return;
|
|
2763
|
+
const backlogged = (port.bufferedAmount ?? 0) > 0;
|
|
2764
|
+
if (heardSincePing) unanswered = 0;
|
|
2765
|
+
else if (peerKeepsAlive && !backlogged) unanswered++;
|
|
2766
|
+
if (unanswered >= misses) {
|
|
2767
|
+
close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
heardSincePing = false;
|
|
2771
|
+
post({
|
|
2772
|
+
type: "ping",
|
|
2773
|
+
id: KEEPALIVE_ID
|
|
2774
|
+
});
|
|
2775
|
+
};
|
|
2776
|
+
const close = async (reason) => {
|
|
2777
|
+
if (muxClosed) return;
|
|
2778
|
+
clearInterval(pinger);
|
|
2779
|
+
muxClosed = true;
|
|
2780
|
+
signal?.removeEventListener("abort", onTransportAbort);
|
|
2781
|
+
port.removeEventListener("message", listener);
|
|
2782
|
+
const closedNow = /* @__PURE__ */ new Set();
|
|
2783
|
+
for (const [id, { handle, credit }] of [...open]) {
|
|
2784
|
+
closedNow.add(id);
|
|
2785
|
+
codecPost({
|
|
2786
|
+
type: "close",
|
|
2787
|
+
id
|
|
2788
|
+
});
|
|
2789
|
+
open.delete(id);
|
|
2790
|
+
releaseWaiters(credit);
|
|
2791
|
+
handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
|
|
2792
|
+
}
|
|
2793
|
+
for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
|
|
2794
|
+
type: "close",
|
|
2795
|
+
id
|
|
2796
|
+
});
|
|
2797
|
+
outgoing.clear();
|
|
2798
|
+
ring.length = 0;
|
|
2799
|
+
await port.close?.();
|
|
2800
|
+
};
|
|
2801
|
+
const onTransportAbort = () => {
|
|
2802
|
+
close(signal?.reason).catch(() => {});
|
|
2101
2803
|
};
|
|
2102
2804
|
port.addEventListener("message", listener);
|
|
2103
2805
|
port.start?.();
|
|
2806
|
+
if (keepalive) {
|
|
2807
|
+
pinger = setInterval(tick, keepalive.intervalMs);
|
|
2808
|
+
pinger.unref?.();
|
|
2809
|
+
}
|
|
2810
|
+
if (signal?.aborted) close(signal.reason).catch(() => {});
|
|
2811
|
+
else signal?.addEventListener("abort", onTransportAbort, { once: true });
|
|
2104
2812
|
return {
|
|
2105
2813
|
maxMessageSize,
|
|
2106
|
-
async openPort(meta) {
|
|
2814
|
+
async openPort(meta, { signal: portSignal } = {}) {
|
|
2107
2815
|
if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
|
|
2108
|
-
if (
|
|
2816
|
+
if (portSignal?.aborted) throw portSignal.reason;
|
|
2817
|
+
if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
|
|
2109
2818
|
while (open.has(nextId)) nextId += 2;
|
|
2110
2819
|
const id = nextId;
|
|
2111
2820
|
nextId += 2;
|
|
2112
|
-
const handle = attach(id);
|
|
2113
|
-
post({
|
|
2821
|
+
const handle = attach(id, meta, window, true, exactCodec);
|
|
2822
|
+
post(window ? exactCodec ? {
|
|
2823
|
+
type: "open",
|
|
2824
|
+
id,
|
|
2825
|
+
meta,
|
|
2826
|
+
window,
|
|
2827
|
+
exact: true
|
|
2828
|
+
} : {
|
|
2829
|
+
type: "open",
|
|
2830
|
+
id,
|
|
2831
|
+
meta,
|
|
2832
|
+
window
|
|
2833
|
+
} : {
|
|
2114
2834
|
type: "open",
|
|
2115
2835
|
id,
|
|
2116
2836
|
meta
|
|
2117
2837
|
});
|
|
2838
|
+
if (portSignal) {
|
|
2839
|
+
const record = open.get(id)?.handle;
|
|
2840
|
+
const onAbort = () => {
|
|
2841
|
+
if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
|
|
2842
|
+
};
|
|
2843
|
+
portSignal.addEventListener("abort", onAbort, { once: true });
|
|
2844
|
+
getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
|
|
2845
|
+
}
|
|
2118
2846
|
return handle.port;
|
|
2119
2847
|
},
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
}
|
|
2130
|
-
muxClosed = true;
|
|
2131
|
-
port.removeEventListener("message", listener);
|
|
2132
|
-
await port.close?.();
|
|
2848
|
+
close,
|
|
2849
|
+
ports() {
|
|
2850
|
+
return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
|
|
2851
|
+
...info,
|
|
2852
|
+
...credit.enforced ? { window: credit.window } : {},
|
|
2853
|
+
queued: credit.queued,
|
|
2854
|
+
outstanding: credit.outstanding,
|
|
2855
|
+
unconsumed: credit.unconsumed
|
|
2856
|
+
} : info);
|
|
2133
2857
|
}
|
|
2134
2858
|
};
|
|
2135
2859
|
}
|
|
2860
|
+
/** The byte length of a received message's data, or `undefined` if it is not bytes. */
|
|
2861
|
+
function byteLengthOf(data) {
|
|
2862
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
|
|
2863
|
+
}
|
|
2864
|
+
/**
|
|
2865
|
+
* `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
|
|
2866
|
+
* a full table names who filled it. A port without a kind counts as `"?"`.
|
|
2867
|
+
*/
|
|
2868
|
+
function describeKinds(entries) {
|
|
2869
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2870
|
+
for (const { meta } of entries) {
|
|
2871
|
+
const kind = meta?.kind;
|
|
2872
|
+
const name = typeof kind === "string" ? kind : "?";
|
|
2873
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
2874
|
+
}
|
|
2875
|
+
if (!counts.size) return "none";
|
|
2876
|
+
return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
|
|
2877
|
+
}
|
|
2136
2878
|
/**
|
|
2137
2879
|
* Announced with every port this pair opens. Layer 1 never reads `meta` — it is
|
|
2138
2880
|
* here so a peer running something else over the same multiplexer can tell a
|
|
@@ -2154,9 +2896,22 @@ const connect = async ({ mux: factory, timeout }) => {
|
|
|
2154
2896
|
maxMessageSize: mux.maxMessageSize,
|
|
2155
2897
|
timeout
|
|
2156
2898
|
};
|
|
2157
|
-
const call = (input) =>
|
|
2158
|
-
|
|
2159
|
-
|
|
2899
|
+
const call = (input) => {
|
|
2900
|
+
let stream;
|
|
2901
|
+
let abandoned = false;
|
|
2902
|
+
return returningAtOnce((async function* () {
|
|
2903
|
+
const port = await mux.openPort(STREAM_META);
|
|
2904
|
+
if (abandoned) {
|
|
2905
|
+
await port.close?.();
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
stream = duplexOverPort(port, streamOptions)(input);
|
|
2909
|
+
yield* stream;
|
|
2910
|
+
})(), () => {
|
|
2911
|
+
abandoned = true;
|
|
2912
|
+
stream?.return(void 0).catch(() => {});
|
|
2913
|
+
});
|
|
2914
|
+
};
|
|
2160
2915
|
return {
|
|
2161
2916
|
call,
|
|
2162
2917
|
async close() {
|
|
@@ -2226,7 +2981,7 @@ function isEnvelope(value) {
|
|
|
2226
2981
|
const candidate = value;
|
|
2227
2982
|
if (typeof candidate.id !== "number") return false;
|
|
2228
2983
|
if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
|
|
2229
|
-
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
|
|
2984
|
+
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
|
|
2230
2985
|
}
|
|
2231
2986
|
/**
|
|
2232
2987
|
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
@@ -2247,6 +3002,27 @@ const structuredCodec = {
|
|
|
2247
3002
|
}
|
|
2248
3003
|
};
|
|
2249
3004
|
//#endregion
|
|
3005
|
+
//#region src/link/async-dispose.ts
|
|
3006
|
+
/**
|
|
3007
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
3008
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
3009
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
3010
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
3011
|
+
* declared types, which would require consumers to compile with the
|
|
3012
|
+
* `esnext.disposable` lib.
|
|
3013
|
+
*/
|
|
3014
|
+
function asyncDisposable(target, dispose) {
|
|
3015
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
3016
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
3017
|
+
value: async () => {
|
|
3018
|
+
await dispose();
|
|
3019
|
+
},
|
|
3020
|
+
configurable: true,
|
|
3021
|
+
writable: true
|
|
3022
|
+
});
|
|
3023
|
+
return target;
|
|
3024
|
+
}
|
|
3025
|
+
//#endregion
|
|
2250
3026
|
//#region src/link/http-link.ts
|
|
2251
3027
|
/**
|
|
2252
3028
|
* The version of the HTTP-over-`MessagePort` protocol this package speaks,
|
|
@@ -2276,11 +3052,12 @@ var LinkLostError = class extends Error {
|
|
|
2276
3052
|
*/
|
|
2277
3053
|
const LINK_CLOSE = "webrun-http:link-close";
|
|
2278
3054
|
/** Opens the calling end of a link on `port` (one port per link). */
|
|
2279
|
-
async function connectHttpLink(port, { streamTimeout } = {}) {
|
|
3055
|
+
async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
|
|
2280
3056
|
const { call, close } = await connect({
|
|
2281
3057
|
mux: overPipe(port, {
|
|
2282
3058
|
codec: structuredCodec,
|
|
2283
|
-
side: "initiator"
|
|
3059
|
+
side: "initiator",
|
|
3060
|
+
maxPorts
|
|
2284
3061
|
}),
|
|
2285
3062
|
timeout: streamTimeout
|
|
2286
3063
|
});
|
|
@@ -2296,14 +3073,15 @@ async function connectHttpLink(port, { streamTimeout } = {}) {
|
|
|
2296
3073
|
* that abandons the requests still running (their handlers see the
|
|
2297
3074
|
* cancellation) and releases the port.
|
|
2298
3075
|
*/
|
|
2299
|
-
async function serveHttpLink(port, handler, { streamTimeout } = {}) {
|
|
3076
|
+
async function serveHttpLink(port, handler, { streamTimeout, maxPorts, requestIdleMs } = {}) {
|
|
2300
3077
|
const teardown = await serve({
|
|
2301
3078
|
mux: overPipe(port, {
|
|
2302
3079
|
codec: structuredCodec,
|
|
2303
|
-
side: "responder"
|
|
3080
|
+
side: "responder",
|
|
3081
|
+
maxPorts
|
|
2304
3082
|
}),
|
|
2305
3083
|
timeout: streamTimeout
|
|
2306
|
-
}, serveFetchOverDuplex(async (request) => await handler(request)));
|
|
3084
|
+
}, serveFetchOverDuplex(async (request) => await handler(request), { requestIdleMs }));
|
|
2307
3085
|
const onClose = (event) => {
|
|
2308
3086
|
if (event.data?.type === LINK_CLOSE) close();
|
|
2309
3087
|
};
|
|
@@ -2328,7 +3106,7 @@ function trackedLink(send, release) {
|
|
|
2328
3106
|
inflight--;
|
|
2329
3107
|
if (inflight === 0) for (const resolve of idleWaiters.splice(0)) resolve();
|
|
2330
3108
|
};
|
|
2331
|
-
|
|
3109
|
+
const link = {
|
|
2332
3110
|
get inflight() {
|
|
2333
3111
|
return inflight;
|
|
2334
3112
|
},
|
|
@@ -2365,7 +3143,7 @@ function trackedLink(send, release) {
|
|
|
2365
3143
|
finish();
|
|
2366
3144
|
return response;
|
|
2367
3145
|
}
|
|
2368
|
-
return new Response(guardBody(response.body, lost, () => lostReason, finish), {
|
|
3146
|
+
return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
|
|
2369
3147
|
status: response.status,
|
|
2370
3148
|
statusText: response.statusText,
|
|
2371
3149
|
headers: response.headers
|
|
@@ -2381,35 +3159,68 @@ function trackedLink(send, release) {
|
|
|
2381
3159
|
} catch {}
|
|
2382
3160
|
}
|
|
2383
3161
|
};
|
|
3162
|
+
return asyncDisposable(link, () => link.close());
|
|
2384
3163
|
}
|
|
2385
3164
|
/**
|
|
2386
|
-
* A response body that reports its end (done, cancelled or
|
|
2387
|
-
* with the link's reason when the link is lost mid-stream.
|
|
3165
|
+
* A response body that reports its end (done, cancelled, failed or aborted) and
|
|
3166
|
+
* fails with the link's reason when the link is lost mid-stream. An abort of the
|
|
3167
|
+
* request's signal ends it at once, read or not: the exchange is over (the layer
|
|
3168
|
+
* below has given its port back), so the request leaves `inflight` then.
|
|
2388
3169
|
*/
|
|
2389
|
-
function guardBody(body, lost, reason, finish) {
|
|
3170
|
+
function guardBody(body, lost, reason, finish, signal) {
|
|
2390
3171
|
const reader = body.getReader();
|
|
3172
|
+
let controller;
|
|
3173
|
+
let ended = false;
|
|
3174
|
+
const end = () => {
|
|
3175
|
+
ended = true;
|
|
3176
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3177
|
+
finish();
|
|
3178
|
+
};
|
|
3179
|
+
const onAbort = () => {
|
|
3180
|
+
if (ended) return;
|
|
3181
|
+
end();
|
|
3182
|
+
reader.cancel().catch(() => {});
|
|
3183
|
+
try {
|
|
3184
|
+
controller.error(abortError(signal));
|
|
3185
|
+
} catch {}
|
|
3186
|
+
};
|
|
2391
3187
|
return new ReadableStream({
|
|
3188
|
+
start(c) {
|
|
3189
|
+
controller = c;
|
|
3190
|
+
if (signal?.aborted) onAbort();
|
|
3191
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
3192
|
+
},
|
|
2392
3193
|
async pull(controller) {
|
|
2393
3194
|
try {
|
|
2394
3195
|
const { done, value } = await Promise.race([reader.read(), lost]);
|
|
3196
|
+
if (ended) return;
|
|
2395
3197
|
if (done) {
|
|
2396
|
-
|
|
3198
|
+
end();
|
|
2397
3199
|
controller.close();
|
|
2398
3200
|
return;
|
|
2399
3201
|
}
|
|
2400
3202
|
controller.enqueue(value);
|
|
2401
3203
|
} catch (error) {
|
|
2402
|
-
|
|
3204
|
+
if (ended) return;
|
|
3205
|
+
end();
|
|
2403
3206
|
reader.cancel().catch(() => {});
|
|
2404
3207
|
controller.error(reason() ?? error);
|
|
2405
3208
|
}
|
|
2406
3209
|
},
|
|
2407
3210
|
async cancel(why) {
|
|
2408
|
-
|
|
3211
|
+
end();
|
|
2409
3212
|
await reader.cancel(why).catch(() => {});
|
|
2410
3213
|
}
|
|
2411
3214
|
});
|
|
2412
3215
|
}
|
|
3216
|
+
/** The signal's reason as an `Error` (`AbortError` when it gave none). */
|
|
3217
|
+
function abortError(signal) {
|
|
3218
|
+
const why = signal.reason;
|
|
3219
|
+
if (why instanceof Error) return why;
|
|
3220
|
+
const err = new Error(why === void 0 ? "Aborted" : String(why));
|
|
3221
|
+
err.name = "AbortError";
|
|
3222
|
+
return err;
|
|
3223
|
+
}
|
|
2413
3224
|
//#endregion
|
|
2414
3225
|
//#region src/link/locks.ts
|
|
2415
3226
|
function lockManager() {
|
|
@@ -5225,6 +6036,8 @@ var WorkerLinkImpl = class {
|
|
|
5225
6036
|
_ownerLock;
|
|
5226
6037
|
_attaching;
|
|
5227
6038
|
_detached = false;
|
|
6039
|
+
/** The `seq` of the last link sent to the worker. */
|
|
6040
|
+
_sent = 0;
|
|
5228
6041
|
constructor(_adapter, _control, init) {
|
|
5229
6042
|
this._adapter = _adapter;
|
|
5230
6043
|
this._control = _control;
|
|
@@ -5236,6 +6049,7 @@ var WorkerLinkImpl = class {
|
|
|
5236
6049
|
_control.onmessage = (event) => {
|
|
5237
6050
|
const message = event.data;
|
|
5238
6051
|
if (message?.type === "relink") {
|
|
6052
|
+
if (message.seen !== void 0 && message.seen < this._sent) return;
|
|
5239
6053
|
this._adapter._poke(WAKE_MESSAGE);
|
|
5240
6054
|
this.attach().catch(() => {});
|
|
5241
6055
|
} else if (message?.type === "keepalive") this._adapter._poke(KEEPALIVE_MESSAGE);
|
|
@@ -5282,7 +6096,8 @@ var WorkerLinkImpl = class {
|
|
|
5282
6096
|
this._control.postMessage({
|
|
5283
6097
|
type: "link",
|
|
5284
6098
|
key: this.key,
|
|
5285
|
-
routerLock: reply.routerLock
|
|
6099
|
+
routerLock: reply.routerLock,
|
|
6100
|
+
seq: ++this._sent
|
|
5286
6101
|
}, [
|
|
5287
6102
|
down.port2,
|
|
5288
6103
|
ctl.port2,
|
|
@@ -5383,12 +6198,18 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
5383
6198
|
const ownerLock = newLockName("webrun-http-owner");
|
|
5384
6199
|
const held = holdLock(ownerLock);
|
|
5385
6200
|
const controls = /* @__PURE__ */ new Set();
|
|
6201
|
+
const seen = /* @__PURE__ */ new Map();
|
|
5386
6202
|
const tellPages = (message) => {
|
|
5387
6203
|
for (const control of controls) control.postMessage(message);
|
|
5388
6204
|
};
|
|
5389
6205
|
const endpoint = new HandlerEndpoint(handler, {
|
|
5390
6206
|
...options,
|
|
5391
|
-
requestRelink: () =>
|
|
6207
|
+
requestRelink: () => {
|
|
6208
|
+
for (const control of controls) control.postMessage({
|
|
6209
|
+
type: "relink",
|
|
6210
|
+
seen: seen.get(control) ?? 0
|
|
6211
|
+
});
|
|
6212
|
+
},
|
|
5392
6213
|
keepAlive: () => tellPages({ type: "keepalive" })
|
|
5393
6214
|
});
|
|
5394
6215
|
const onMessage = (event) => {
|
|
@@ -5400,6 +6221,7 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
5400
6221
|
control.onmessage = (message) => {
|
|
5401
6222
|
const msg = message.data;
|
|
5402
6223
|
if (msg?.type === "link") {
|
|
6224
|
+
if (msg.seq !== void 0) seen.set(control, msg.seq);
|
|
5403
6225
|
const [down, ctl, up] = message.ports;
|
|
5404
6226
|
if (down) endpoint.link(msg.key, {
|
|
5405
6227
|
down,
|
|
@@ -5440,6 +6262,7 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
5440
6262
|
await endpoint.close();
|
|
5441
6263
|
for (const control of controls) control.close();
|
|
5442
6264
|
controls.clear();
|
|
6265
|
+
seen.clear();
|
|
5443
6266
|
(await held)();
|
|
5444
6267
|
}
|
|
5445
6268
|
};
|