@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/worker.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
|
* Inverse of {@link sendIterator}: turns a sequence of `{done, value, error}`
|
|
182
179
|
* chunks (delivered to the supplied callback by `installer`) into an async
|
|
@@ -387,8 +384,8 @@ var HttpParseError = class extends Error {
|
|
|
387
384
|
const NEWLINE = 10;
|
|
388
385
|
/** Mirrors the HTTP/1.1 codec's default `maxHeaderBytes`. */
|
|
389
386
|
const MAX_ENVELOPE_BYTES = 65536;
|
|
390
|
-
const encoder$1 = new TextEncoder();
|
|
391
|
-
const decoder = new TextDecoder();
|
|
387
|
+
const encoder$1 = /* @__PURE__ */ new TextEncoder();
|
|
388
|
+
const decoder = /* @__PURE__ */ new TextDecoder();
|
|
392
389
|
/**
|
|
393
390
|
* Encode an HTTP envelope plus optional body as one continuous byte stream:
|
|
394
391
|
*
|
|
@@ -642,15 +639,15 @@ function resolveFraming(headers, version) {
|
|
|
642
639
|
};
|
|
643
640
|
return { kind: "none" };
|
|
644
641
|
}
|
|
645
|
-
const CRLF = new Uint8Array([13, 10]);
|
|
646
|
-
const LAST_CHUNK = new Uint8Array([
|
|
642
|
+
const CRLF = /* @__PURE__ */ new Uint8Array([13, 10]);
|
|
643
|
+
const LAST_CHUNK = /* @__PURE__ */ new Uint8Array([
|
|
647
644
|
48,
|
|
648
645
|
13,
|
|
649
646
|
10,
|
|
650
647
|
13,
|
|
651
648
|
10
|
|
652
649
|
]);
|
|
653
|
-
const encoder = new TextEncoder();
|
|
650
|
+
const encoder = /* @__PURE__ */ new TextEncoder();
|
|
654
651
|
/**
|
|
655
652
|
* Chunk sizes are written in HEXADECIMAL — a 21-byte chunk is `15`. Writing
|
|
656
653
|
* them in decimal is the defect note 15 found in @libp2p/http, and it is the
|
|
@@ -955,7 +952,7 @@ function newHttpCodec(options = {}) {
|
|
|
955
952
|
decodeResponse: (input, o) => decodeResponse(input, opts, o.method)
|
|
956
953
|
};
|
|
957
954
|
}
|
|
958
|
-
const httpCodec = newHttpCodec();
|
|
955
|
+
const httpCodec = /* @__PURE__ */ newHttpCodec();
|
|
959
956
|
/**
|
|
960
957
|
* Dispatches on byte 0. The formats are self-identifying — a JSON envelope
|
|
961
958
|
* always begins `{`, which is not a token character and so can never begin an
|
|
@@ -1005,10 +1002,29 @@ function newSniffingCodec(options) {
|
|
|
1005
1002
|
* Writes HTTP/1.1; accepts HTTP/1.1 or the legacy JSON envelope. Used whenever
|
|
1006
1003
|
* no `codec` option is supplied.
|
|
1007
1004
|
*/
|
|
1008
|
-
const defaultCodec = newSniffingCodec({
|
|
1005
|
+
const defaultCodec = /* @__PURE__ */ newSniffingCodec({
|
|
1009
1006
|
write: httpCodec,
|
|
1010
1007
|
accept: [httpCodec, jsonEnvelopeCodec]
|
|
1011
1008
|
});
|
|
1009
|
+
/**
|
|
1010
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
1011
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
1012
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
1013
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
1014
|
+
* declared types, which would require consumers to compile with the
|
|
1015
|
+
* `esnext.disposable` lib.
|
|
1016
|
+
*/
|
|
1017
|
+
function asyncDisposable$2(target, dispose) {
|
|
1018
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
1019
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
1020
|
+
value: async () => {
|
|
1021
|
+
await dispose();
|
|
1022
|
+
},
|
|
1023
|
+
configurable: true,
|
|
1024
|
+
writable: true
|
|
1025
|
+
});
|
|
1026
|
+
return target;
|
|
1027
|
+
}
|
|
1012
1028
|
/** Carries the serialized JS error between two webrun peers. */
|
|
1013
1029
|
const PEER_ERROR_HEADER = "x-webrun-error";
|
|
1014
1030
|
/** Hard cap on the peer-error header (M3): see `encodeErrorResponse`. */
|
|
@@ -1086,41 +1102,105 @@ async function throwIfPeerError(result, output) {
|
|
|
1086
1102
|
*/
|
|
1087
1103
|
async function httpFetch(call, env, body, options = {}) {
|
|
1088
1104
|
const codec = options.codec ?? defaultCodec;
|
|
1105
|
+
const { signal } = options;
|
|
1106
|
+
if (signal?.aborted) throw abortReason$1(signal);
|
|
1089
1107
|
const output = call(codec.encodeRequest(env, body));
|
|
1090
|
-
|
|
1091
|
-
const release =
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1108
|
+
let released;
|
|
1109
|
+
const release = () => {
|
|
1110
|
+
released ??= (async () => {
|
|
1111
|
+
try {
|
|
1112
|
+
await output.return?.(void 0);
|
|
1113
|
+
} catch {}
|
|
1114
|
+
})();
|
|
1115
|
+
return released;
|
|
1116
|
+
};
|
|
1117
|
+
let finished = false;
|
|
1118
|
+
const finish = () => {
|
|
1119
|
+
if (finished || released) return;
|
|
1120
|
+
finished = true;
|
|
1121
|
+
if (signal?.aborted) {
|
|
1122
|
+
release();
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
const onAbort = () => void release();
|
|
1126
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1127
|
+
drain(output).finally(() => signal?.removeEventListener("abort", onAbort));
|
|
1095
1128
|
};
|
|
1129
|
+
let decoded;
|
|
1130
|
+
try {
|
|
1131
|
+
decoded = await untilAborted(codec.decodeResponse(output, { method: env.method }), signal);
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
if (signal?.aborted) release();
|
|
1134
|
+
else await release();
|
|
1135
|
+
throw error;
|
|
1136
|
+
}
|
|
1096
1137
|
const result = {
|
|
1097
1138
|
envelope: decoded.envelope,
|
|
1098
|
-
body: releasingOnExit(decoded.body, release),
|
|
1099
|
-
release
|
|
1139
|
+
body: releasingOnExit(decoded.body, finish, release),
|
|
1140
|
+
release,
|
|
1141
|
+
finish
|
|
1100
1142
|
};
|
|
1101
1143
|
await throwIfPeerError(result, output);
|
|
1102
1144
|
return result;
|
|
1103
1145
|
}
|
|
1146
|
+
/** `promise`, rejected with the signal's reason if it aborts first. */
|
|
1147
|
+
function untilAborted(promise, signal) {
|
|
1148
|
+
if (!signal) return promise;
|
|
1149
|
+
return new Promise((resolve, reject) => {
|
|
1150
|
+
const onAbort = () => reject(abortReason$1(signal));
|
|
1151
|
+
if (signal.aborted) return onAbort();
|
|
1152
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1153
|
+
promise.then((value) => {
|
|
1154
|
+
signal.removeEventListener("abort", onAbort);
|
|
1155
|
+
resolve(value);
|
|
1156
|
+
}, (error) => {
|
|
1157
|
+
signal.removeEventListener("abort", onAbort);
|
|
1158
|
+
reject(error);
|
|
1159
|
+
});
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
/** The signal's reason as an `Error` (`AbortError` when it gave none). */
|
|
1163
|
+
function abortReason$1(signal) {
|
|
1164
|
+
const reason = signal.reason;
|
|
1165
|
+
if (reason instanceof Error) return reason;
|
|
1166
|
+
const err = new Error(reason === void 0 ? "Aborted" : String(reason));
|
|
1167
|
+
err.name = "AbortError";
|
|
1168
|
+
return err;
|
|
1169
|
+
}
|
|
1104
1170
|
/**
|
|
1105
|
-
* `body`,
|
|
1171
|
+
* `body`, ending the call however it is left: read to its end, or left early.
|
|
1106
1172
|
*
|
|
1107
1173
|
* The body the codec hands back is a generator of its own over a `ByteReader`,
|
|
1108
|
-
* and
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
1111
|
-
*
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1114
|
-
|
|
1115
|
-
|
|
1174
|
+
* and its ending never reaches the call's `output` (see `throwIfPeerError` for
|
|
1175
|
+
* why the codec must not do that itself). A caller that stopped reading — an
|
|
1176
|
+
* aborted `fetch`, a cancelled response stream — left the peer's producer
|
|
1177
|
+
* parked forever; and one that read the body to its end left the call itself
|
|
1178
|
+
* open, which on a multiplexed transport is one port per exchange until the
|
|
1179
|
+
* multiplexer refuses to open more (`maxPorts`).
|
|
1180
|
+
*
|
|
1181
|
+
* Left early, the call is abandoned (`.return()`): the peer's producer is
|
|
1182
|
+
* released. Read to its end, the call is not cut short but read to ITS end, in
|
|
1183
|
+
* the background: the peer ends it once its side of the exchange is over,
|
|
1184
|
+
* which may be later than the response — a handler that answered before it
|
|
1185
|
+
* read the request body is still receiving it (`HttpDataHandlerResult.settled`),
|
|
1186
|
+
* and abandoning the call would abort that upload.
|
|
1187
|
+
*/
|
|
1188
|
+
async function* releasingOnExit(body, finish, release) {
|
|
1116
1189
|
let completed = false;
|
|
1117
1190
|
try {
|
|
1118
1191
|
yield* body;
|
|
1119
1192
|
completed = true;
|
|
1120
1193
|
} finally {
|
|
1121
|
-
if (
|
|
1194
|
+
if (completed) finish();
|
|
1195
|
+
else await release();
|
|
1122
1196
|
}
|
|
1123
1197
|
}
|
|
1198
|
+
/** Reads `output` to its end, discarding what is left, ignoring a failure. */
|
|
1199
|
+
async function drain(output) {
|
|
1200
|
+
try {
|
|
1201
|
+
for (let r = await output.next(); !r.done; r = await output.next());
|
|
1202
|
+
} catch {}
|
|
1203
|
+
}
|
|
1124
1204
|
/**
|
|
1125
1205
|
* Wrap an HTTP handler as a `Duplex` so it can be registered with any
|
|
1126
1206
|
* `webrun-streams-*` adapter's `serve`.
|
|
@@ -1148,6 +1228,7 @@ function httpServe(handler, options = {}) {
|
|
|
1148
1228
|
try {
|
|
1149
1229
|
yield* replyCodec.encodeResponse(result.envelope, result.body, { method: decoded.envelope.method });
|
|
1150
1230
|
completed = true;
|
|
1231
|
+
await result.settled?.();
|
|
1151
1232
|
} finally {
|
|
1152
1233
|
if (!completed) try {
|
|
1153
1234
|
await result.release?.();
|
|
@@ -1316,17 +1397,21 @@ async function fetchOverDuplex(call, request, options = {}) {
|
|
|
1316
1397
|
const buffered = new Uint8Array(await request.arrayBuffer());
|
|
1317
1398
|
if (buffered.byteLength > 0) body = [buffered];
|
|
1318
1399
|
}
|
|
1319
|
-
const { envelope, body: respBody, release } = await httpFetch(call, env, body,
|
|
1400
|
+
const { envelope, body: respBody, release, finish } = await httpFetch(call, env, body, {
|
|
1401
|
+
...options,
|
|
1402
|
+
signal: request.signal ?? void 0
|
|
1403
|
+
});
|
|
1320
1404
|
const responseInit = {
|
|
1321
1405
|
status: envelope.status,
|
|
1322
1406
|
statusText: envelope.statusText,
|
|
1323
1407
|
headers: forwardableHeaders(envelope.headers)
|
|
1324
1408
|
};
|
|
1325
1409
|
if (request.method === "HEAD" || request.method === "OPTIONS" || NULL_BODY_STATUSES.has(envelope.status)) {
|
|
1326
|
-
|
|
1410
|
+
if (finish) finish();
|
|
1411
|
+
else await release?.();
|
|
1327
1412
|
return new Response(null, responseInit);
|
|
1328
1413
|
}
|
|
1329
|
-
return new Response(
|
|
1414
|
+
return new Response(responseBody(respBody, request.signal, release), responseInit);
|
|
1330
1415
|
}
|
|
1331
1416
|
/**
|
|
1332
1417
|
* Wrap a `(Request) => Promise<Response>` handler as a `Duplex` so it can be
|
|
@@ -1338,13 +1423,21 @@ function serveFetchOverDuplex(handler, options = {}) {
|
|
|
1338
1423
|
method: env.method,
|
|
1339
1424
|
headers: forwardableHeaders(env.headers)
|
|
1340
1425
|
};
|
|
1426
|
+
let upload;
|
|
1341
1427
|
if (env.method !== "GET" && env.method !== "HEAD") {
|
|
1342
1428
|
if (supportsRequestStreams()) {
|
|
1343
|
-
|
|
1429
|
+
upload = trackingEnd(body);
|
|
1430
|
+
reqInit.body = asyncIterableToReadable(upload.source);
|
|
1344
1431
|
reqInit.duplex = "half";
|
|
1345
1432
|
} else reqInit.body = await collectBytes(body);
|
|
1346
1433
|
}
|
|
1347
|
-
const
|
|
1434
|
+
const request = new Request(env.url, reqInit);
|
|
1435
|
+
const response = await handler(request);
|
|
1436
|
+
const settled = async () => {
|
|
1437
|
+
if (!upload) return;
|
|
1438
|
+
if (!request.bodyUsed && !request.body?.locked) request.body?.cancel().catch(() => {});
|
|
1439
|
+
await upload.settle(options.requestIdleMs ?? 3e4);
|
|
1440
|
+
};
|
|
1348
1441
|
const respEnv = {
|
|
1349
1442
|
status: response.status,
|
|
1350
1443
|
statusText: response.statusText,
|
|
@@ -1354,32 +1447,173 @@ function serveFetchOverDuplex(handler, options = {}) {
|
|
|
1354
1447
|
await response.body?.cancel();
|
|
1355
1448
|
return {
|
|
1356
1449
|
envelope: respEnv,
|
|
1357
|
-
body: void 0
|
|
1450
|
+
body: void 0,
|
|
1451
|
+
settled
|
|
1358
1452
|
};
|
|
1359
1453
|
}
|
|
1360
1454
|
return {
|
|
1361
1455
|
envelope: respEnv,
|
|
1362
1456
|
body: response.body ? readableToAsyncIterable(response.body) : void 0,
|
|
1363
|
-
release: () => response.body?.cancel()
|
|
1457
|
+
release: () => response.body?.cancel(),
|
|
1458
|
+
settled
|
|
1364
1459
|
};
|
|
1365
1460
|
}, options);
|
|
1366
1461
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1462
|
+
/**
|
|
1463
|
+
* `source`, and `settle(idleMs)`: resolves once it is done — read to its end,
|
|
1464
|
+
* failed, or abandoned (`return()` included before the first `next()`, which a
|
|
1465
|
+
* generator's `finally` would not see) — or once nobody has asked it for a
|
|
1466
|
+
* chunk for `idleMs`, in which case it is abandoned: later reads fail.
|
|
1467
|
+
*
|
|
1468
|
+
* Idle counts only while no `next()` is pending: a reader waiting for a slow
|
|
1469
|
+
* sender is still reading.
|
|
1470
|
+
*/
|
|
1471
|
+
function trackingEnd(source) {
|
|
1472
|
+
let end = () => {};
|
|
1473
|
+
let isEnded = false;
|
|
1474
|
+
const ended = new Promise((resolve) => {
|
|
1475
|
+
end = () => {
|
|
1476
|
+
isEnded = true;
|
|
1477
|
+
resolve();
|
|
1478
|
+
};
|
|
1479
|
+
});
|
|
1480
|
+
let abandoned;
|
|
1481
|
+
let pending = 0;
|
|
1482
|
+
let idleSince = Date.now();
|
|
1483
|
+
const it = source[Symbol.asyncIterator]();
|
|
1484
|
+
const abandon = (reason) => {
|
|
1485
|
+
abandoned ??= reason;
|
|
1486
|
+
end();
|
|
1487
|
+
Promise.resolve(it.return?.(void 0)).catch(() => {});
|
|
1488
|
+
};
|
|
1489
|
+
const tracked = {
|
|
1490
|
+
[Symbol.asyncIterator]() {
|
|
1491
|
+
return tracked;
|
|
1492
|
+
},
|
|
1493
|
+
async next() {
|
|
1494
|
+
if (abandoned) throw abandoned;
|
|
1495
|
+
pending++;
|
|
1496
|
+
try {
|
|
1497
|
+
const r = await it.next();
|
|
1498
|
+
if (r.done) end();
|
|
1499
|
+
return r;
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
end();
|
|
1502
|
+
throw error;
|
|
1503
|
+
} finally {
|
|
1504
|
+
if (--pending === 0) idleSince = Date.now();
|
|
1505
|
+
}
|
|
1506
|
+
},
|
|
1507
|
+
async return(value) {
|
|
1508
|
+
end();
|
|
1509
|
+
try {
|
|
1510
|
+
await it.return?.(value);
|
|
1511
|
+
} catch {}
|
|
1512
|
+
return {
|
|
1513
|
+
done: true,
|
|
1514
|
+
value: void 0
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1518
|
+
asyncDisposable$2(tracked, () => tracked.return?.() ?? Promise.resolve());
|
|
1519
|
+
const settle = (idleMs) => {
|
|
1520
|
+
if (isEnded || !Number.isFinite(idleMs)) return ended;
|
|
1521
|
+
return new Promise((resolve) => {
|
|
1522
|
+
let timer;
|
|
1523
|
+
const check = () => {
|
|
1524
|
+
if (isEnded) return resolve();
|
|
1525
|
+
const idle = pending === 0 ? Date.now() - idleSince : 0;
|
|
1526
|
+
if (idle >= idleMs) {
|
|
1527
|
+
abandon(/* @__PURE__ */ new Error(`webrun-http-streams: the request body was left unread for ${idleMs} ms after the response; abandoned`));
|
|
1528
|
+
return resolve();
|
|
1529
|
+
}
|
|
1530
|
+
timer = setTimeout(check, Math.max(1, Math.min(idleMs - idle, 1e3)));
|
|
1531
|
+
timer.unref?.();
|
|
1532
|
+
};
|
|
1533
|
+
ended.then(() => {
|
|
1534
|
+
clearTimeout(timer);
|
|
1535
|
+
resolve();
|
|
1536
|
+
});
|
|
1537
|
+
check();
|
|
1538
|
+
});
|
|
1539
|
+
};
|
|
1540
|
+
return {
|
|
1541
|
+
source: tracked,
|
|
1542
|
+
settle
|
|
1543
|
+
};
|
|
1376
1544
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1545
|
+
/**
|
|
1546
|
+
* How long a cancelled body waits for the read in flight before it abandons the call anyway.
|
|
1547
|
+
* The read's answer tells a body that has ended (the call is finished: an upload the handler
|
|
1548
|
+
* reads after answering goes on) from one that has not; a peer that sends nothing more never
|
|
1549
|
+
* answers it. A body whose end comes later than this after the cancel is abandoned too, so a
|
|
1550
|
+
* handler still streaming its response and still reading the upload then loses the upload.
|
|
1551
|
+
*/
|
|
1552
|
+
const CANCEL_GRACE_MS = 1e3;
|
|
1553
|
+
/**
|
|
1554
|
+
* The response body as a stream that the request's signal can end at any time.
|
|
1555
|
+
*
|
|
1556
|
+
* An abort errors the stream with the abort's reason at once — a read waiting
|
|
1557
|
+
* for a peer that sends nothing more rejects then, not when a next chunk
|
|
1558
|
+
* arrives, which may be never — and abandons the call (`release`), so on a
|
|
1559
|
+
* multiplexed transport its port is given back whether or not anyone still
|
|
1560
|
+
* reads the body. The listener is removed once the body ends, however it ends:
|
|
1561
|
+
* read to its end, cancelled, failed or aborted. A body nobody reads or cancels
|
|
1562
|
+
* keeps it until the signal aborts (and keeps the call open until then anyway).
|
|
1563
|
+
* A cancel abandons the call too — unless the read in flight comes back with the
|
|
1564
|
+
* body's end, which finishes it instead (see `cancel`).
|
|
1565
|
+
*/
|
|
1566
|
+
function responseBody(body, signal, release) {
|
|
1567
|
+
const it = body[Symbol.asyncIterator]();
|
|
1568
|
+
let controller;
|
|
1569
|
+
let ended = false;
|
|
1570
|
+
let reading = false;
|
|
1571
|
+
const end = () => {
|
|
1572
|
+
ended = true;
|
|
1573
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1574
|
+
};
|
|
1575
|
+
const onAbort = () => {
|
|
1576
|
+
if (ended) return;
|
|
1577
|
+
end();
|
|
1578
|
+
try {
|
|
1579
|
+
controller.error(abortReason$1(signal));
|
|
1580
|
+
} catch {}
|
|
1581
|
+
release?.();
|
|
1582
|
+
};
|
|
1583
|
+
return new ReadableStream({
|
|
1584
|
+
start(c) {
|
|
1585
|
+
controller = c;
|
|
1586
|
+
if (signal?.aborted) onAbort();
|
|
1587
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
1588
|
+
},
|
|
1589
|
+
async pull(c) {
|
|
1590
|
+
try {
|
|
1591
|
+
reading = true;
|
|
1592
|
+
const { done, value } = await it.next().finally(() => {
|
|
1593
|
+
reading = false;
|
|
1594
|
+
});
|
|
1595
|
+
if (ended) return;
|
|
1596
|
+
if (done) {
|
|
1597
|
+
end();
|
|
1598
|
+
c.close();
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
c.enqueue(value);
|
|
1602
|
+
} catch (err) {
|
|
1603
|
+
if (ended) return;
|
|
1604
|
+
end();
|
|
1605
|
+
c.error(err);
|
|
1606
|
+
}
|
|
1607
|
+
},
|
|
1608
|
+
cancel() {
|
|
1609
|
+
end();
|
|
1610
|
+
const returned = Promise.resolve(it.return?.()).catch(() => {});
|
|
1611
|
+
if (!reading) return;
|
|
1612
|
+
const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
|
|
1613
|
+
grace.unref?.();
|
|
1614
|
+
returned.finally(() => clearTimeout(grace));
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1383
1617
|
}
|
|
1384
1618
|
//#endregion
|
|
1385
1619
|
//#region ../webrun-rpc/dist/index.js
|
|
@@ -1504,8 +1738,11 @@ function listenPort(port, handler, { channelName = "", log = () => {} } = {}) {
|
|
|
1504
1738
|
error
|
|
1505
1739
|
});
|
|
1506
1740
|
};
|
|
1507
|
-
|
|
1508
|
-
|
|
1741
|
+
const listener = (event) => {
|
|
1742
|
+
onMessage(event);
|
|
1743
|
+
};
|
|
1744
|
+
port.addEventListener("message", listener);
|
|
1745
|
+
return () => port.removeEventListener("message", listener);
|
|
1509
1746
|
}
|
|
1510
1747
|
/**
|
|
1511
1748
|
* Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
|
|
@@ -1535,6 +1772,69 @@ async function* throughAbort(input, signal) {
|
|
|
1535
1772
|
}
|
|
1536
1773
|
}
|
|
1537
1774
|
/**
|
|
1775
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
1776
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
1777
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
1778
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
1779
|
+
* declared types, which would require consumers to compile with the
|
|
1780
|
+
* `esnext.disposable` lib.
|
|
1781
|
+
*/
|
|
1782
|
+
function asyncDisposable$1(target, dispose) {
|
|
1783
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
1784
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
1785
|
+
value: async () => {
|
|
1786
|
+
await dispose();
|
|
1787
|
+
},
|
|
1788
|
+
configurable: true,
|
|
1789
|
+
writable: true
|
|
1790
|
+
});
|
|
1791
|
+
return target;
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* `gen`, except that `return()` and `throw()` take effect at once.
|
|
1795
|
+
*
|
|
1796
|
+
* On an async generator both are queued behind a `next()` that is still
|
|
1797
|
+
* pending: a consumer that gives up on a stream while it waits for the peer's
|
|
1798
|
+
* next chunk — an aborted fetch whose response head has not come, say — cannot
|
|
1799
|
+
* end it until that chunk arrives, which for a peer that never answers is
|
|
1800
|
+
* never. The stream's `finally` (which closes its port and tells the peer)
|
|
1801
|
+
* then never runs, and on a multiplexer the port is kept for good.
|
|
1802
|
+
*
|
|
1803
|
+
* So `abandon` runs first, synchronously: it must make the pending `next()`
|
|
1804
|
+
* settle (abort the stream), which then resolves done, after which the
|
|
1805
|
+
* generator's own `return()` / `throw()` proceeds as usual. On a generator
|
|
1806
|
+
* suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
|
|
1807
|
+
*/
|
|
1808
|
+
function returningAtOnce(gen, abandon) {
|
|
1809
|
+
let abandoned = false;
|
|
1810
|
+
const giveUp = (reason) => {
|
|
1811
|
+
if (abandoned) return;
|
|
1812
|
+
abandoned = true;
|
|
1813
|
+
abandon(reason);
|
|
1814
|
+
};
|
|
1815
|
+
const wrapped = {
|
|
1816
|
+
next: (...args) => gen.next(...args).catch((error) => {
|
|
1817
|
+
if (abandoned) return {
|
|
1818
|
+
done: true,
|
|
1819
|
+
value: void 0
|
|
1820
|
+
};
|
|
1821
|
+
throw error;
|
|
1822
|
+
}),
|
|
1823
|
+
return(value) {
|
|
1824
|
+
giveUp();
|
|
1825
|
+
return gen.return(value);
|
|
1826
|
+
},
|
|
1827
|
+
throw(error) {
|
|
1828
|
+
giveUp(error);
|
|
1829
|
+
return gen.throw(error);
|
|
1830
|
+
},
|
|
1831
|
+
[Symbol.asyncIterator]() {
|
|
1832
|
+
return wrapped;
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
return asyncDisposable$1(wrapped, () => wrapped.return(void 0));
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1538
1838
|
* The `type` of the out-of-band notice a side posts when it abandons a stream.
|
|
1539
1839
|
*
|
|
1540
1840
|
* Layer 1's `close` is not observable to layer 2 — a closed virtual port drops
|
|
@@ -1630,7 +1930,7 @@ function runCallerSide(port, input, options) {
|
|
|
1630
1930
|
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
1631
1931
|
const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
|
|
1632
1932
|
pump.catch(() => {});
|
|
1633
|
-
return (async function* () {
|
|
1933
|
+
return returningAtOnce((async function* () {
|
|
1634
1934
|
try {
|
|
1635
1935
|
yield* inbound.stream;
|
|
1636
1936
|
} finally {
|
|
@@ -1644,7 +1944,9 @@ function runCallerSide(port, input, options) {
|
|
|
1644
1944
|
await port.close?.();
|
|
1645
1945
|
} catch {}
|
|
1646
1946
|
}
|
|
1647
|
-
})()
|
|
1947
|
+
})(), () => {
|
|
1948
|
+
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
1949
|
+
});
|
|
1648
1950
|
}
|
|
1649
1951
|
/**
|
|
1650
1952
|
* Listens for the peer's abort notice, and can post our own. The notice is a
|
|
@@ -1845,6 +2147,33 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
|
|
|
1845
2147
|
}
|
|
1846
2148
|
}
|
|
1847
2149
|
/**
|
|
2150
|
+
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
2151
|
+
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
2152
|
+
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
2153
|
+
* are clamped to 4 ms in browsers.
|
|
2154
|
+
*/
|
|
2155
|
+
const setImmediateFn = globalThis.setImmediate;
|
|
2156
|
+
/** @internal */
|
|
2157
|
+
const nextTask = setImmediateFn ? (fn) => {
|
|
2158
|
+
setImmediateFn(fn);
|
|
2159
|
+
} : (() => {
|
|
2160
|
+
let channel;
|
|
2161
|
+
const queue = [];
|
|
2162
|
+
return (fn) => {
|
|
2163
|
+
if (!channel) {
|
|
2164
|
+
channel = new MessageChannel();
|
|
2165
|
+
channel.port1.onmessage = () => queue.shift()?.();
|
|
2166
|
+
}
|
|
2167
|
+
queue.push(fn);
|
|
2168
|
+
channel.port2.postMessage(0);
|
|
2169
|
+
};
|
|
2170
|
+
})();
|
|
2171
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
2172
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
2173
|
+
function setPortSendState(port, state) {
|
|
2174
|
+
states.set(port, state);
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
1848
2177
|
* One virtual port.
|
|
1849
2178
|
*
|
|
1850
2179
|
* `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
|
|
@@ -1859,6 +2188,14 @@ function newVirtualPort(send, requestClose) {
|
|
|
1859
2188
|
if (closing.signal.aborted) return;
|
|
1860
2189
|
closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
|
|
1861
2190
|
};
|
|
2191
|
+
const close = (reason) => {
|
|
2192
|
+
if (closed) return;
|
|
2193
|
+
closed = true;
|
|
2194
|
+
const notify = requestClose;
|
|
2195
|
+
listeners.clear();
|
|
2196
|
+
notify(reason);
|
|
2197
|
+
abort(reason);
|
|
2198
|
+
};
|
|
1862
2199
|
const port = {
|
|
1863
2200
|
addEventListener(_type, listener) {
|
|
1864
2201
|
listeners.add(listener);
|
|
@@ -1870,24 +2207,28 @@ function newVirtualPort(send, requestClose) {
|
|
|
1870
2207
|
if (closed) return;
|
|
1871
2208
|
send(message, transfer);
|
|
1872
2209
|
},
|
|
1873
|
-
close
|
|
1874
|
-
if (closed) return;
|
|
1875
|
-
closed = true;
|
|
1876
|
-
const notify = requestClose;
|
|
1877
|
-
listeners.clear();
|
|
1878
|
-
notify();
|
|
1879
|
-
abort();
|
|
1880
|
-
}
|
|
2210
|
+
close
|
|
1881
2211
|
};
|
|
1882
2212
|
setPortCloseSignal(port, closing.signal);
|
|
2213
|
+
const dispose = Symbol.dispose;
|
|
2214
|
+
if (dispose) Object.defineProperty(port, dispose, {
|
|
2215
|
+
value: () => close(),
|
|
2216
|
+
configurable: true
|
|
2217
|
+
});
|
|
1883
2218
|
return {
|
|
1884
2219
|
port,
|
|
1885
2220
|
deliver(payload) {
|
|
1886
2221
|
if (closed) return;
|
|
1887
2222
|
const event = new MessageEvent("message", { data: payload });
|
|
2223
|
+
let pending;
|
|
1888
2224
|
for (const listener of [...listeners]) try {
|
|
1889
|
-
listener(event);
|
|
2225
|
+
const result = listener(event);
|
|
2226
|
+
if (result && typeof result.then === "function") {
|
|
2227
|
+
pending ??= [];
|
|
2228
|
+
pending.push(result);
|
|
2229
|
+
}
|
|
1890
2230
|
} catch {}
|
|
2231
|
+
return pending ? Promise.allSettled(pending) : void 0;
|
|
1891
2232
|
},
|
|
1892
2233
|
markClosed(reason) {
|
|
1893
2234
|
closed = true;
|
|
@@ -1896,53 +2237,309 @@ function newVirtualPort(send, requestClose) {
|
|
|
1896
2237
|
},
|
|
1897
2238
|
isClosed() {
|
|
1898
2239
|
return closed;
|
|
2240
|
+
},
|
|
2241
|
+
hasListeners() {
|
|
2242
|
+
return listeners.size > 0;
|
|
1899
2243
|
}
|
|
1900
2244
|
};
|
|
1901
2245
|
}
|
|
1902
2246
|
/**
|
|
1903
|
-
* The
|
|
1904
|
-
*
|
|
2247
|
+
* The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
|
|
2248
|
+
* whose codec passes unknown envelopes through reads them as a close of that
|
|
2249
|
+
* id, which must therefore not be a real port (0 is the initiator's first).
|
|
1905
2250
|
*/
|
|
1906
|
-
const
|
|
2251
|
+
const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
2252
|
+
/** The longest delay a timer holds; past it `setInterval` fires at once. */
|
|
2253
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
1907
2254
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
1908
2255
|
const DEFAULT_MAX_PORTS = 1024;
|
|
1909
2256
|
/**
|
|
2257
|
+
* What a message costs a port's window when the codec cannot say
|
|
2258
|
+
* (`PortCodec.encode`): its bytes and string lengths, found in its values up
|
|
2259
|
+
* to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
|
|
2260
|
+
* from the same structured value, so they agree.
|
|
2261
|
+
*/
|
|
2262
|
+
function estimateMessageSize(payload) {
|
|
2263
|
+
let budget = 256;
|
|
2264
|
+
const walk = (v, depth) => {
|
|
2265
|
+
if (budget-- <= 0) return 0;
|
|
2266
|
+
if (typeof v === "string") return v.length;
|
|
2267
|
+
if (v instanceof ArrayBuffer) return v.byteLength;
|
|
2268
|
+
if (ArrayBuffer.isView(v)) return v.byteLength;
|
|
2269
|
+
if (v === null || typeof v !== "object" || depth >= 4) return 8;
|
|
2270
|
+
let n = 16;
|
|
2271
|
+
if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
|
|
2272
|
+
else for (const k in v) n += k.length + walk(v[k], depth + 1);
|
|
2273
|
+
return n;
|
|
2274
|
+
};
|
|
2275
|
+
return Math.max(64, walk(payload, 0));
|
|
2276
|
+
}
|
|
2277
|
+
/**
|
|
1910
2278
|
* The default `PortMux`: emulates multiplexing over a single port.
|
|
1911
2279
|
*
|
|
1912
2280
|
* A transport that already multiplexes natively supplies its own `PortMux`
|
|
1913
2281
|
* instead — this implementation is for transports that offer one pipe.
|
|
1914
2282
|
*/
|
|
1915
2283
|
function multiplexPort(port, options) {
|
|
1916
|
-
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
|
|
2284
|
+
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
|
|
2285
|
+
const misses = keepalive?.misses ?? 3;
|
|
2286
|
+
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`);
|
|
2287
|
+
if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
|
|
1917
2288
|
const open = /* @__PURE__ */ new Map();
|
|
2289
|
+
const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
|
|
2290
|
+
const entry = open.get(id);
|
|
2291
|
+
if (entry?.handle !== record) return;
|
|
2292
|
+
const { handle: _, credit: __, partial: ___, ...info } = entry;
|
|
2293
|
+
if (!record.hasListeners()) {
|
|
2294
|
+
release("leaked");
|
|
2295
|
+
record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
|
|
2296
|
+
}
|
|
2297
|
+
try {
|
|
2298
|
+
onLeak?.(info);
|
|
2299
|
+
} catch {}
|
|
2300
|
+
}) : void 0;
|
|
2301
|
+
const touch = (id) => {
|
|
2302
|
+
const entry = open.get(id);
|
|
2303
|
+
if (entry) entry.lastActivity = Date.now();
|
|
2304
|
+
};
|
|
1918
2305
|
let nextId = side === "initiator" ? 0 : 1;
|
|
1919
2306
|
let muxClosed = false;
|
|
2307
|
+
/** This end counts encoded bytes and reassembles frames. */
|
|
2308
|
+
const exactCodec = codec.encode != null && codec.decode != null;
|
|
1920
2309
|
const post = (envelope, transfer) => {
|
|
1921
2310
|
if (muxClosed) return;
|
|
2311
|
+
codecPost(envelope, transfer);
|
|
2312
|
+
};
|
|
2313
|
+
/** Like `post`, also while closing: the mux's own close notices. */
|
|
2314
|
+
const codecPost = (envelope, transfer) => {
|
|
1922
2315
|
try {
|
|
1923
2316
|
codec.post(port, envelope, transfer);
|
|
1924
2317
|
} catch {}
|
|
1925
2318
|
};
|
|
1926
|
-
const
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2319
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
2320
|
+
const ring = [];
|
|
2321
|
+
let pumping = false;
|
|
2322
|
+
const pump = async () => {
|
|
2323
|
+
pumping = true;
|
|
2324
|
+
try {
|
|
2325
|
+
while (ring.length && !muxClosed) {
|
|
2326
|
+
if (port.whenDrained) await port.whenDrained();
|
|
2327
|
+
if (muxClosed) break;
|
|
2328
|
+
const id = ring.shift();
|
|
2329
|
+
const queue = outgoing.get(id);
|
|
2330
|
+
const item = queue?.shift();
|
|
2331
|
+
if (queue?.length) ring.push(id);
|
|
2332
|
+
else outgoing.delete(id);
|
|
2333
|
+
item?.();
|
|
2334
|
+
}
|
|
2335
|
+
} catch (err) {
|
|
2336
|
+
close(err).catch(() => {});
|
|
2337
|
+
} finally {
|
|
2338
|
+
pumping = false;
|
|
2339
|
+
}
|
|
2340
|
+
};
|
|
2341
|
+
const sendOnPort = (id, item) => {
|
|
2342
|
+
if (frameSize == null) {
|
|
2343
|
+
item();
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
let queue = outgoing.get(id);
|
|
2347
|
+
if (!queue) {
|
|
2348
|
+
queue = [];
|
|
2349
|
+
outgoing.set(id, queue);
|
|
2350
|
+
ring.push(id);
|
|
2351
|
+
}
|
|
2352
|
+
queue.push(item);
|
|
2353
|
+
if (!pumping) pump();
|
|
2354
|
+
};
|
|
2355
|
+
const dropOutgoing = (id) => {
|
|
2356
|
+
if (!outgoing.delete(id)) return;
|
|
2357
|
+
const at = ring.indexOf(id);
|
|
2358
|
+
if (at >= 0) ring.splice(at, 1);
|
|
2359
|
+
};
|
|
2360
|
+
const postBytes = (bytes) => {
|
|
2361
|
+
if (muxClosed) return;
|
|
2362
|
+
try {
|
|
2363
|
+
port.postMessage(bytes);
|
|
2364
|
+
} catch {}
|
|
2365
|
+
};
|
|
2366
|
+
const releaseWaiters = (c) => {
|
|
2367
|
+
for (const w of c?.waiters.splice(0) ?? []) w.resolve();
|
|
2368
|
+
};
|
|
2369
|
+
const flush = (c) => {
|
|
2370
|
+
while (c.queue.length) {
|
|
2371
|
+
const next = c.queue[0];
|
|
2372
|
+
if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
2373
|
+
c.queue.shift();
|
|
2374
|
+
c.queued -= next.size;
|
|
2375
|
+
c.outstanding += next.size;
|
|
2376
|
+
next.send();
|
|
2377
|
+
}
|
|
2378
|
+
for (const w of [...c.waiters]) if (c.queued <= w.below) {
|
|
2379
|
+
c.waiters.splice(c.waiters.indexOf(w), 1);
|
|
2380
|
+
w.resolve();
|
|
2381
|
+
}
|
|
2382
|
+
};
|
|
2383
|
+
const grant = (id, c, bytes) => {
|
|
2384
|
+
c.unconsumed -= bytes;
|
|
2385
|
+
c.toGrant += bytes;
|
|
2386
|
+
const send = () => {
|
|
2387
|
+
c.grantScheduled = false;
|
|
2388
|
+
if (!c.toGrant || !open.has(id)) return;
|
|
1933
2389
|
post({
|
|
2390
|
+
type: "credit",
|
|
2391
|
+
id,
|
|
2392
|
+
bytes: c.toGrant
|
|
2393
|
+
});
|
|
2394
|
+
c.toGrant = 0;
|
|
2395
|
+
};
|
|
2396
|
+
if (c.toGrant >= c.window / 4) {
|
|
2397
|
+
send();
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
if (c.grantScheduled) return;
|
|
2401
|
+
c.grantScheduled = true;
|
|
2402
|
+
setTimeout(send, 0);
|
|
2403
|
+
};
|
|
2404
|
+
const attach = (id, meta, portWindow, opener, exact) => {
|
|
2405
|
+
const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
|
|
2406
|
+
window: portWindow,
|
|
2407
|
+
enforced: false,
|
|
2408
|
+
opener,
|
|
2409
|
+
exact,
|
|
2410
|
+
outstanding: 0,
|
|
2411
|
+
queue: [],
|
|
2412
|
+
queued: 0,
|
|
2413
|
+
unconsumed: 0,
|
|
2414
|
+
waiters: [],
|
|
2415
|
+
toGrant: 0,
|
|
2416
|
+
grantScheduled: false
|
|
2417
|
+
} : void 0;
|
|
2418
|
+
const self = newVirtualPort((payload, transfer) => {
|
|
2419
|
+
touch(id);
|
|
2420
|
+
const envelope = {
|
|
2421
|
+
type: "message",
|
|
2422
|
+
id,
|
|
2423
|
+
payload
|
|
2424
|
+
};
|
|
2425
|
+
if (!c) {
|
|
2426
|
+
post(envelope, transfer);
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
const bytes = c.exact ? codec.encode?.(envelope) : void 0;
|
|
2430
|
+
const size = bytes?.byteLength ?? estimateMessageSize(payload);
|
|
2431
|
+
const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
|
|
2432
|
+
const held = !bytes && waits ? {
|
|
2433
|
+
...envelope,
|
|
2434
|
+
payload: structuredClone(payload, { transfer })
|
|
2435
|
+
} : void 0;
|
|
2436
|
+
const send = () => {
|
|
2437
|
+
if (!bytes) {
|
|
2438
|
+
sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
if (frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
2442
|
+
for (let at = 0; at < bytes.byteLength; at += frameSize) {
|
|
2443
|
+
const data = bytes.subarray(at, at + frameSize);
|
|
2444
|
+
const frame = at + frameSize >= bytes.byteLength ? {
|
|
2445
|
+
type: "frame",
|
|
2446
|
+
id,
|
|
2447
|
+
data,
|
|
2448
|
+
last: true
|
|
2449
|
+
} : {
|
|
2450
|
+
type: "frame",
|
|
2451
|
+
id,
|
|
2452
|
+
data
|
|
2453
|
+
};
|
|
2454
|
+
sendOnPort(id, () => post(frame));
|
|
2455
|
+
}
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
sendOnPort(id, () => postBytes(bytes));
|
|
2459
|
+
};
|
|
2460
|
+
c.queue.push({
|
|
2461
|
+
send,
|
|
2462
|
+
size
|
|
2463
|
+
});
|
|
2464
|
+
c.queued += size;
|
|
2465
|
+
flush(c);
|
|
2466
|
+
}, (reason) => {
|
|
2467
|
+
release(reason);
|
|
2468
|
+
record.markClosed(reason);
|
|
2469
|
+
});
|
|
2470
|
+
function release(reason) {
|
|
2471
|
+
open.delete(id);
|
|
2472
|
+
leaks?.unregister(record);
|
|
2473
|
+
if (c?.queue.length) {
|
|
2474
|
+
for (const item of c.queue.splice(0)) item.send();
|
|
2475
|
+
c.queued = 0;
|
|
2476
|
+
}
|
|
2477
|
+
sendOnPort(id, () => post({
|
|
1934
2478
|
type: "close",
|
|
1935
2479
|
id,
|
|
1936
2480
|
reason
|
|
2481
|
+
}));
|
|
2482
|
+
releaseWaiters(c);
|
|
2483
|
+
}
|
|
2484
|
+
const { markClosed, isClosed, hasListeners } = self;
|
|
2485
|
+
if (c) {
|
|
2486
|
+
const deliver = self.deliver;
|
|
2487
|
+
self.deliver = (payload, size = estimateMessageSize(payload)) => {
|
|
2488
|
+
c.unconsumed += size;
|
|
2489
|
+
const done = deliver(payload);
|
|
2490
|
+
if (done) done.then(() => grant(id, c, size));
|
|
2491
|
+
else grant(id, c, size);
|
|
2492
|
+
return done;
|
|
2493
|
+
};
|
|
2494
|
+
setPortSendState(self.port, {
|
|
2495
|
+
bufferedAmount: () => c.queued,
|
|
2496
|
+
ready: (below = 0) => new Promise((resolve) => {
|
|
2497
|
+
if (c.queued <= below || isClosed()) nextTask(resolve);
|
|
2498
|
+
else c.waiters.push({
|
|
2499
|
+
below,
|
|
2500
|
+
resolve: () => nextTask(resolve)
|
|
2501
|
+
});
|
|
2502
|
+
})
|
|
1937
2503
|
});
|
|
1938
|
-
|
|
1939
|
-
|
|
2504
|
+
}
|
|
2505
|
+
const record = {
|
|
2506
|
+
deliver: self.deliver,
|
|
2507
|
+
markClosed,
|
|
2508
|
+
isClosed,
|
|
2509
|
+
hasListeners,
|
|
2510
|
+
close: (reason) => {
|
|
2511
|
+
if (isClosed()) return;
|
|
2512
|
+
release(reason);
|
|
2513
|
+
markClosed(reason);
|
|
2514
|
+
}
|
|
2515
|
+
};
|
|
2516
|
+
leaks?.register(self.port, {
|
|
2517
|
+
id,
|
|
2518
|
+
record,
|
|
2519
|
+
release
|
|
2520
|
+
}, record);
|
|
2521
|
+
const now = Date.now();
|
|
2522
|
+
open.set(id, {
|
|
2523
|
+
id,
|
|
2524
|
+
meta,
|
|
2525
|
+
openedAt: now,
|
|
2526
|
+
lastActivity: now,
|
|
2527
|
+
handle: record,
|
|
2528
|
+
credit: c
|
|
1940
2529
|
});
|
|
1941
|
-
open.set(id, self);
|
|
1942
2530
|
return self;
|
|
1943
2531
|
};
|
|
1944
|
-
const handleEnvelope = (envelope) => {
|
|
2532
|
+
const handleEnvelope = (envelope, event) => {
|
|
1945
2533
|
if (muxClosed) return;
|
|
2534
|
+
heardSincePing = true;
|
|
2535
|
+
if (envelope.type === "ping" || envelope.type === "pong") {
|
|
2536
|
+
peerKeepsAlive = true;
|
|
2537
|
+
if (envelope.type === "ping") post({
|
|
2538
|
+
type: "pong",
|
|
2539
|
+
id: KEEPALIVE_ID
|
|
2540
|
+
});
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
1946
2543
|
if (envelope.type === "open") {
|
|
1947
2544
|
if (open.has(envelope.id)) return;
|
|
1948
2545
|
if (open.size >= maxPorts) {
|
|
@@ -1953,7 +2550,18 @@ function multiplexPort(port, options) {
|
|
|
1953
2550
|
});
|
|
1954
2551
|
return;
|
|
1955
2552
|
}
|
|
1956
|
-
const handle = attach(envelope.id);
|
|
2553
|
+
const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
|
|
2554
|
+
const credit = open.get(envelope.id)?.credit;
|
|
2555
|
+
if (credit) post(credit.exact ? {
|
|
2556
|
+
type: "credit",
|
|
2557
|
+
id: envelope.id,
|
|
2558
|
+
bytes: 0,
|
|
2559
|
+
exact: true
|
|
2560
|
+
} : {
|
|
2561
|
+
type: "credit",
|
|
2562
|
+
id: envelope.id,
|
|
2563
|
+
bytes: 0
|
|
2564
|
+
});
|
|
1957
2565
|
let accepted = false;
|
|
1958
2566
|
if (onPort) try {
|
|
1959
2567
|
accepted = onPort(handle.port, envelope.meta) !== false;
|
|
@@ -1968,57 +2576,191 @@ function multiplexPort(port, options) {
|
|
|
1968
2576
|
id: envelope.id,
|
|
1969
2577
|
reason: "rejected"
|
|
1970
2578
|
});
|
|
2579
|
+
return;
|
|
1971
2580
|
}
|
|
1972
2581
|
return;
|
|
1973
2582
|
}
|
|
1974
|
-
const
|
|
2583
|
+
const entry = open.get(envelope.id);
|
|
2584
|
+
if (envelope.type === "credit") {
|
|
2585
|
+
const c = entry?.credit;
|
|
2586
|
+
if (!c || !(envelope.bytes >= 0)) return;
|
|
2587
|
+
if (!c.enforced && c.opener) {
|
|
2588
|
+
if (c.exact && envelope.exact !== true) {
|
|
2589
|
+
c.exact = false;
|
|
2590
|
+
c.outstanding = 0;
|
|
2591
|
+
}
|
|
2592
|
+
post({
|
|
2593
|
+
type: "credit",
|
|
2594
|
+
id: envelope.id,
|
|
2595
|
+
bytes: 0
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
c.enforced = true;
|
|
2599
|
+
c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
|
|
2600
|
+
flush(c);
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
1975
2603
|
if (envelope.type === "message") {
|
|
1976
|
-
|
|
1977
|
-
|
|
2604
|
+
const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
|
|
2605
|
+
if (!entry) return;
|
|
2606
|
+
touch(envelope.id);
|
|
2607
|
+
entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
|
|
1978
2608
|
return;
|
|
1979
2609
|
}
|
|
1980
|
-
if (
|
|
2610
|
+
if (envelope.type === "frame") {
|
|
2611
|
+
if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
|
|
2612
|
+
touch(envelope.id);
|
|
2613
|
+
const partial = entry.partial ?? {
|
|
2614
|
+
chunks: [],
|
|
2615
|
+
bytes: 0
|
|
2616
|
+
};
|
|
2617
|
+
entry.partial = partial;
|
|
2618
|
+
partial.chunks.push(envelope.data);
|
|
2619
|
+
partial.bytes += envelope.data.byteLength;
|
|
2620
|
+
if (!envelope.last) return;
|
|
2621
|
+
entry.partial = void 0;
|
|
2622
|
+
const whole = new Uint8Array(partial.bytes);
|
|
2623
|
+
let at = 0;
|
|
2624
|
+
for (const chunk of partial.chunks) {
|
|
2625
|
+
whole.set(chunk, at);
|
|
2626
|
+
at += chunk.byteLength;
|
|
2627
|
+
}
|
|
2628
|
+
const inner = codec.decode(whole);
|
|
2629
|
+
if (inner?.type !== "message" || inner.id !== envelope.id) return;
|
|
2630
|
+
entry.handle.deliver(inner.payload, whole.byteLength);
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
dropOutgoing(envelope.id);
|
|
2634
|
+
if (!entry) return;
|
|
1981
2635
|
open.delete(envelope.id);
|
|
1982
|
-
|
|
2636
|
+
releaseWaiters(entry.credit);
|
|
2637
|
+
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
|
|
1983
2638
|
};
|
|
1984
2639
|
const listener = (event) => {
|
|
1985
2640
|
const envelope = codec.read(event);
|
|
1986
|
-
if (envelope) handleEnvelope(envelope);
|
|
2641
|
+
if (envelope) handleEnvelope(envelope, event);
|
|
2642
|
+
};
|
|
2643
|
+
let peerKeepsAlive = false;
|
|
2644
|
+
let heardSincePing = true;
|
|
2645
|
+
let unanswered = 0;
|
|
2646
|
+
let pinger;
|
|
2647
|
+
const tick = () => {
|
|
2648
|
+
if (!keepalive) return;
|
|
2649
|
+
const backlogged = (port.bufferedAmount ?? 0) > 0;
|
|
2650
|
+
if (heardSincePing) unanswered = 0;
|
|
2651
|
+
else if (peerKeepsAlive && !backlogged) unanswered++;
|
|
2652
|
+
if (unanswered >= misses) {
|
|
2653
|
+
close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
heardSincePing = false;
|
|
2657
|
+
post({
|
|
2658
|
+
type: "ping",
|
|
2659
|
+
id: KEEPALIVE_ID
|
|
2660
|
+
});
|
|
2661
|
+
};
|
|
2662
|
+
const close = async (reason) => {
|
|
2663
|
+
if (muxClosed) return;
|
|
2664
|
+
clearInterval(pinger);
|
|
2665
|
+
muxClosed = true;
|
|
2666
|
+
signal?.removeEventListener("abort", onTransportAbort);
|
|
2667
|
+
port.removeEventListener("message", listener);
|
|
2668
|
+
const closedNow = /* @__PURE__ */ new Set();
|
|
2669
|
+
for (const [id, { handle, credit }] of [...open]) {
|
|
2670
|
+
closedNow.add(id);
|
|
2671
|
+
codecPost({
|
|
2672
|
+
type: "close",
|
|
2673
|
+
id
|
|
2674
|
+
});
|
|
2675
|
+
open.delete(id);
|
|
2676
|
+
releaseWaiters(credit);
|
|
2677
|
+
handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
|
|
2678
|
+
}
|
|
2679
|
+
for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
|
|
2680
|
+
type: "close",
|
|
2681
|
+
id
|
|
2682
|
+
});
|
|
2683
|
+
outgoing.clear();
|
|
2684
|
+
ring.length = 0;
|
|
2685
|
+
await port.close?.();
|
|
2686
|
+
};
|
|
2687
|
+
const onTransportAbort = () => {
|
|
2688
|
+
close(signal?.reason).catch(() => {});
|
|
1987
2689
|
};
|
|
1988
2690
|
port.addEventListener("message", listener);
|
|
1989
2691
|
port.start?.();
|
|
2692
|
+
if (keepalive) {
|
|
2693
|
+
pinger = setInterval(tick, keepalive.intervalMs);
|
|
2694
|
+
pinger.unref?.();
|
|
2695
|
+
}
|
|
2696
|
+
if (signal?.aborted) close(signal.reason).catch(() => {});
|
|
2697
|
+
else signal?.addEventListener("abort", onTransportAbort, { once: true });
|
|
1990
2698
|
return {
|
|
1991
2699
|
maxMessageSize,
|
|
1992
|
-
async openPort(meta) {
|
|
2700
|
+
async openPort(meta, { signal: portSignal } = {}) {
|
|
1993
2701
|
if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
|
|
1994
|
-
if (
|
|
2702
|
+
if (portSignal?.aborted) throw portSignal.reason;
|
|
2703
|
+
if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
|
|
1995
2704
|
while (open.has(nextId)) nextId += 2;
|
|
1996
2705
|
const id = nextId;
|
|
1997
2706
|
nextId += 2;
|
|
1998
|
-
const handle = attach(id);
|
|
1999
|
-
post({
|
|
2707
|
+
const handle = attach(id, meta, window, true, exactCodec);
|
|
2708
|
+
post(window ? exactCodec ? {
|
|
2709
|
+
type: "open",
|
|
2710
|
+
id,
|
|
2711
|
+
meta,
|
|
2712
|
+
window,
|
|
2713
|
+
exact: true
|
|
2714
|
+
} : {
|
|
2715
|
+
type: "open",
|
|
2716
|
+
id,
|
|
2717
|
+
meta,
|
|
2718
|
+
window
|
|
2719
|
+
} : {
|
|
2000
2720
|
type: "open",
|
|
2001
2721
|
id,
|
|
2002
2722
|
meta
|
|
2003
2723
|
});
|
|
2724
|
+
if (portSignal) {
|
|
2725
|
+
const record = open.get(id)?.handle;
|
|
2726
|
+
const onAbort = () => {
|
|
2727
|
+
if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
|
|
2728
|
+
};
|
|
2729
|
+
portSignal.addEventListener("abort", onAbort, { once: true });
|
|
2730
|
+
getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
|
|
2731
|
+
}
|
|
2004
2732
|
return handle.port;
|
|
2005
2733
|
},
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
}
|
|
2016
|
-
muxClosed = true;
|
|
2017
|
-
port.removeEventListener("message", listener);
|
|
2018
|
-
await port.close?.();
|
|
2734
|
+
close,
|
|
2735
|
+
ports() {
|
|
2736
|
+
return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
|
|
2737
|
+
...info,
|
|
2738
|
+
...credit.enforced ? { window: credit.window } : {},
|
|
2739
|
+
queued: credit.queued,
|
|
2740
|
+
outstanding: credit.outstanding,
|
|
2741
|
+
unconsumed: credit.unconsumed
|
|
2742
|
+
} : info);
|
|
2019
2743
|
}
|
|
2020
2744
|
};
|
|
2021
2745
|
}
|
|
2746
|
+
/** The byte length of a received message's data, or `undefined` if it is not bytes. */
|
|
2747
|
+
function byteLengthOf(data) {
|
|
2748
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
|
|
2749
|
+
}
|
|
2750
|
+
/**
|
|
2751
|
+
* `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
|
|
2752
|
+
* a full table names who filled it. A port without a kind counts as `"?"`.
|
|
2753
|
+
*/
|
|
2754
|
+
function describeKinds(entries) {
|
|
2755
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2756
|
+
for (const { meta } of entries) {
|
|
2757
|
+
const kind = meta?.kind;
|
|
2758
|
+
const name = typeof kind === "string" ? kind : "?";
|
|
2759
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
2760
|
+
}
|
|
2761
|
+
if (!counts.size) return "none";
|
|
2762
|
+
return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
|
|
2763
|
+
}
|
|
2022
2764
|
/**
|
|
2023
2765
|
* Announced with every port this pair opens. Layer 1 never reads `meta` — it is
|
|
2024
2766
|
* here so a peer running something else over the same multiplexer can tell a
|
|
@@ -2040,9 +2782,22 @@ const connect = async ({ mux: factory, timeout }) => {
|
|
|
2040
2782
|
maxMessageSize: mux.maxMessageSize,
|
|
2041
2783
|
timeout
|
|
2042
2784
|
};
|
|
2043
|
-
const call = (input) =>
|
|
2044
|
-
|
|
2045
|
-
|
|
2785
|
+
const call = (input) => {
|
|
2786
|
+
let stream;
|
|
2787
|
+
let abandoned = false;
|
|
2788
|
+
return returningAtOnce((async function* () {
|
|
2789
|
+
const port = await mux.openPort(STREAM_META);
|
|
2790
|
+
if (abandoned) {
|
|
2791
|
+
await port.close?.();
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2794
|
+
stream = duplexOverPort(port, streamOptions)(input);
|
|
2795
|
+
yield* stream;
|
|
2796
|
+
})(), () => {
|
|
2797
|
+
abandoned = true;
|
|
2798
|
+
stream?.return(void 0).catch(() => {});
|
|
2799
|
+
});
|
|
2800
|
+
};
|
|
2046
2801
|
return {
|
|
2047
2802
|
call,
|
|
2048
2803
|
async close() {
|
|
@@ -2112,7 +2867,7 @@ function isEnvelope(value) {
|
|
|
2112
2867
|
const candidate = value;
|
|
2113
2868
|
if (typeof candidate.id !== "number") return false;
|
|
2114
2869
|
if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
|
|
2115
|
-
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
|
|
2870
|
+
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
|
|
2116
2871
|
}
|
|
2117
2872
|
/**
|
|
2118
2873
|
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
@@ -2133,6 +2888,27 @@ const structuredCodec = {
|
|
|
2133
2888
|
}
|
|
2134
2889
|
};
|
|
2135
2890
|
//#endregion
|
|
2891
|
+
//#region src/link/async-dispose.ts
|
|
2892
|
+
/**
|
|
2893
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
2894
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
2895
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
2896
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
2897
|
+
* declared types, which would require consumers to compile with the
|
|
2898
|
+
* `esnext.disposable` lib.
|
|
2899
|
+
*/
|
|
2900
|
+
function asyncDisposable(target, dispose) {
|
|
2901
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
2902
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
2903
|
+
value: async () => {
|
|
2904
|
+
await dispose();
|
|
2905
|
+
},
|
|
2906
|
+
configurable: true,
|
|
2907
|
+
writable: true
|
|
2908
|
+
});
|
|
2909
|
+
return target;
|
|
2910
|
+
}
|
|
2911
|
+
//#endregion
|
|
2136
2912
|
//#region src/link/http-link.ts
|
|
2137
2913
|
/**
|
|
2138
2914
|
* The version of the HTTP-over-`MessagePort` protocol this package speaks,
|
|
@@ -2162,11 +2938,12 @@ var LinkLostError = class extends Error {
|
|
|
2162
2938
|
*/
|
|
2163
2939
|
const LINK_CLOSE = "webrun-http:link-close";
|
|
2164
2940
|
/** Opens the calling end of a link on `port` (one port per link). */
|
|
2165
|
-
async function connectHttpLink(port, { streamTimeout } = {}) {
|
|
2941
|
+
async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
|
|
2166
2942
|
const { call, close } = await connect({
|
|
2167
2943
|
mux: overPipe(port, {
|
|
2168
2944
|
codec: structuredCodec,
|
|
2169
|
-
side: "initiator"
|
|
2945
|
+
side: "initiator",
|
|
2946
|
+
maxPorts
|
|
2170
2947
|
}),
|
|
2171
2948
|
timeout: streamTimeout
|
|
2172
2949
|
});
|
|
@@ -2182,14 +2959,15 @@ async function connectHttpLink(port, { streamTimeout } = {}) {
|
|
|
2182
2959
|
* that abandons the requests still running (their handlers see the
|
|
2183
2960
|
* cancellation) and releases the port.
|
|
2184
2961
|
*/
|
|
2185
|
-
async function serveHttpLink(port, handler, { streamTimeout } = {}) {
|
|
2962
|
+
async function serveHttpLink(port, handler, { streamTimeout, maxPorts, requestIdleMs } = {}) {
|
|
2186
2963
|
const teardown = await serve({
|
|
2187
2964
|
mux: overPipe(port, {
|
|
2188
2965
|
codec: structuredCodec,
|
|
2189
|
-
side: "responder"
|
|
2966
|
+
side: "responder",
|
|
2967
|
+
maxPorts
|
|
2190
2968
|
}),
|
|
2191
2969
|
timeout: streamTimeout
|
|
2192
|
-
}, serveFetchOverDuplex(async (request) => await handler(request)));
|
|
2970
|
+
}, serveFetchOverDuplex(async (request) => await handler(request), { requestIdleMs }));
|
|
2193
2971
|
const onClose = (event) => {
|
|
2194
2972
|
if (event.data?.type === LINK_CLOSE) close();
|
|
2195
2973
|
};
|
|
@@ -2214,7 +2992,7 @@ function trackedLink(send, release) {
|
|
|
2214
2992
|
inflight--;
|
|
2215
2993
|
if (inflight === 0) for (const resolve of idleWaiters.splice(0)) resolve();
|
|
2216
2994
|
};
|
|
2217
|
-
|
|
2995
|
+
const link = {
|
|
2218
2996
|
get inflight() {
|
|
2219
2997
|
return inflight;
|
|
2220
2998
|
},
|
|
@@ -2251,7 +3029,7 @@ function trackedLink(send, release) {
|
|
|
2251
3029
|
finish();
|
|
2252
3030
|
return response;
|
|
2253
3031
|
}
|
|
2254
|
-
return new Response(guardBody(response.body, lost, () => lostReason, finish), {
|
|
3032
|
+
return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
|
|
2255
3033
|
status: response.status,
|
|
2256
3034
|
statusText: response.statusText,
|
|
2257
3035
|
headers: response.headers
|
|
@@ -2267,35 +3045,68 @@ function trackedLink(send, release) {
|
|
|
2267
3045
|
} catch {}
|
|
2268
3046
|
}
|
|
2269
3047
|
};
|
|
3048
|
+
return asyncDisposable(link, () => link.close());
|
|
2270
3049
|
}
|
|
2271
3050
|
/**
|
|
2272
|
-
* A response body that reports its end (done, cancelled or
|
|
2273
|
-
* with the link's reason when the link is lost mid-stream.
|
|
3051
|
+
* A response body that reports its end (done, cancelled, failed or aborted) and
|
|
3052
|
+
* fails with the link's reason when the link is lost mid-stream. An abort of the
|
|
3053
|
+
* request's signal ends it at once, read or not: the exchange is over (the layer
|
|
3054
|
+
* below has given its port back), so the request leaves `inflight` then.
|
|
2274
3055
|
*/
|
|
2275
|
-
function guardBody(body, lost, reason, finish) {
|
|
3056
|
+
function guardBody(body, lost, reason, finish, signal) {
|
|
2276
3057
|
const reader = body.getReader();
|
|
3058
|
+
let controller;
|
|
3059
|
+
let ended = false;
|
|
3060
|
+
const end = () => {
|
|
3061
|
+
ended = true;
|
|
3062
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3063
|
+
finish();
|
|
3064
|
+
};
|
|
3065
|
+
const onAbort = () => {
|
|
3066
|
+
if (ended) return;
|
|
3067
|
+
end();
|
|
3068
|
+
reader.cancel().catch(() => {});
|
|
3069
|
+
try {
|
|
3070
|
+
controller.error(abortError(signal));
|
|
3071
|
+
} catch {}
|
|
3072
|
+
};
|
|
2277
3073
|
return new ReadableStream({
|
|
3074
|
+
start(c) {
|
|
3075
|
+
controller = c;
|
|
3076
|
+
if (signal?.aborted) onAbort();
|
|
3077
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
3078
|
+
},
|
|
2278
3079
|
async pull(controller) {
|
|
2279
3080
|
try {
|
|
2280
3081
|
const { done, value } = await Promise.race([reader.read(), lost]);
|
|
3082
|
+
if (ended) return;
|
|
2281
3083
|
if (done) {
|
|
2282
|
-
|
|
3084
|
+
end();
|
|
2283
3085
|
controller.close();
|
|
2284
3086
|
return;
|
|
2285
3087
|
}
|
|
2286
3088
|
controller.enqueue(value);
|
|
2287
3089
|
} catch (error) {
|
|
2288
|
-
|
|
3090
|
+
if (ended) return;
|
|
3091
|
+
end();
|
|
2289
3092
|
reader.cancel().catch(() => {});
|
|
2290
3093
|
controller.error(reason() ?? error);
|
|
2291
3094
|
}
|
|
2292
3095
|
},
|
|
2293
3096
|
async cancel(why) {
|
|
2294
|
-
|
|
3097
|
+
end();
|
|
2295
3098
|
await reader.cancel(why).catch(() => {});
|
|
2296
3099
|
}
|
|
2297
3100
|
});
|
|
2298
3101
|
}
|
|
3102
|
+
/** The signal's reason as an `Error` (`AbortError` when it gave none). */
|
|
3103
|
+
function abortError(signal) {
|
|
3104
|
+
const why = signal.reason;
|
|
3105
|
+
if (why instanceof Error) return why;
|
|
3106
|
+
const err = new Error(why === void 0 ? "Aborted" : String(why));
|
|
3107
|
+
err.name = "AbortError";
|
|
3108
|
+
return err;
|
|
3109
|
+
}
|
|
2299
3110
|
//#endregion
|
|
2300
3111
|
//#region src/link/locks.ts
|
|
2301
3112
|
function lockManager() {
|
|
@@ -3442,12 +4253,18 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
3442
4253
|
const ownerLock = newLockName("webrun-http-owner");
|
|
3443
4254
|
const held = holdLock(ownerLock);
|
|
3444
4255
|
const controls = /* @__PURE__ */ new Set();
|
|
4256
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3445
4257
|
const tellPages = (message) => {
|
|
3446
4258
|
for (const control of controls) control.postMessage(message);
|
|
3447
4259
|
};
|
|
3448
4260
|
const endpoint = new HandlerEndpoint(handler, {
|
|
3449
4261
|
...options,
|
|
3450
|
-
requestRelink: () =>
|
|
4262
|
+
requestRelink: () => {
|
|
4263
|
+
for (const control of controls) control.postMessage({
|
|
4264
|
+
type: "relink",
|
|
4265
|
+
seen: seen.get(control) ?? 0
|
|
4266
|
+
});
|
|
4267
|
+
},
|
|
3451
4268
|
keepAlive: () => tellPages({ type: "keepalive" })
|
|
3452
4269
|
});
|
|
3453
4270
|
const onMessage = (event) => {
|
|
@@ -3459,6 +4276,7 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
3459
4276
|
control.onmessage = (message) => {
|
|
3460
4277
|
const msg = message.data;
|
|
3461
4278
|
if (msg?.type === "link") {
|
|
4279
|
+
if (msg.seq !== void 0) seen.set(control, msg.seq);
|
|
3462
4280
|
const [down, ctl, up] = message.ports;
|
|
3463
4281
|
if (down) endpoint.link(msg.key, {
|
|
3464
4282
|
down,
|
|
@@ -3499,6 +4317,7 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
3499
4317
|
await endpoint.close();
|
|
3500
4318
|
for (const control of controls) control.close();
|
|
3501
4319
|
controls.clear();
|
|
4320
|
+
seen.clear();
|
|
3502
4321
|
(await held)();
|
|
3503
4322
|
}
|
|
3504
4323
|
};
|