@statewalker/webrun-http-browser 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +327 -51
- package/dist/link/async-dispose.d.ts +10 -0
- package/dist/link/async-dispose.d.ts.map +1 -0
- package/dist/link/http-link.d.ts +16 -2
- package/dist/link/http-link.d.ts.map +1 -1
- package/dist/relay-sw.js +203 -44
- package/dist/relay-worker.js +203 -44
- package/dist/sw-worker.js +324 -51
- package/dist/sw.js +323 -50
- package/dist/worker.js +324 -51
- package/package.json +4 -4
- package/src/link/async-dispose.ts +22 -0
- package/src/link/http-link.ts +22 -6
package/dist/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;
|
|
1095
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));
|
|
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,14 +1397,18 @@ 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
1414
|
return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
|
|
@@ -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,16 +1447,101 @@ 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
|
}
|
|
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
|
+
};
|
|
1544
|
+
}
|
|
1367
1545
|
async function* withAbort(iter, signal) {
|
|
1368
1546
|
if (!signal) {
|
|
1369
1547
|
yield* iter;
|
|
@@ -1374,13 +1552,6 @@ async function* withAbort(iter, signal) {
|
|
|
1374
1552
|
yield chunk;
|
|
1375
1553
|
}
|
|
1376
1554
|
}
|
|
1377
|
-
function abortReason$1(signal) {
|
|
1378
|
-
const reason = signal.reason;
|
|
1379
|
-
if (reason instanceof Error) return reason;
|
|
1380
|
-
const err = new Error(reason === void 0 ? "Aborted" : String(reason));
|
|
1381
|
-
err.name = "AbortError";
|
|
1382
|
-
return err;
|
|
1383
|
-
}
|
|
1384
1555
|
//#endregion
|
|
1385
1556
|
//#region ../webrun-rpc/dist/index.js
|
|
1386
1557
|
const closeSignals = /* @__PURE__ */ new WeakMap();
|
|
@@ -1535,6 +1706,69 @@ async function* throughAbort(input, signal) {
|
|
|
1535
1706
|
}
|
|
1536
1707
|
}
|
|
1537
1708
|
/**
|
|
1709
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
1710
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
1711
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
1712
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
1713
|
+
* declared types, which would require consumers to compile with the
|
|
1714
|
+
* `esnext.disposable` lib.
|
|
1715
|
+
*/
|
|
1716
|
+
function asyncDisposable$1(target, dispose) {
|
|
1717
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
1718
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
1719
|
+
value: async () => {
|
|
1720
|
+
await dispose();
|
|
1721
|
+
},
|
|
1722
|
+
configurable: true,
|
|
1723
|
+
writable: true
|
|
1724
|
+
});
|
|
1725
|
+
return target;
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* `gen`, except that `return()` and `throw()` take effect at once.
|
|
1729
|
+
*
|
|
1730
|
+
* On an async generator both are queued behind a `next()` that is still
|
|
1731
|
+
* pending: a consumer that gives up on a stream while it waits for the peer's
|
|
1732
|
+
* next chunk — an aborted fetch whose response head has not come, say — cannot
|
|
1733
|
+
* end it until that chunk arrives, which for a peer that never answers is
|
|
1734
|
+
* never. The stream's `finally` (which closes its port and tells the peer)
|
|
1735
|
+
* then never runs, and on a multiplexer the port is kept for good.
|
|
1736
|
+
*
|
|
1737
|
+
* So `abandon` runs first, synchronously: it must make the pending `next()`
|
|
1738
|
+
* settle (abort the stream), which then resolves done, after which the
|
|
1739
|
+
* generator's own `return()` / `throw()` proceeds as usual. On a generator
|
|
1740
|
+
* suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
|
|
1741
|
+
*/
|
|
1742
|
+
function returningAtOnce(gen, abandon) {
|
|
1743
|
+
let abandoned = false;
|
|
1744
|
+
const giveUp = (reason) => {
|
|
1745
|
+
if (abandoned) return;
|
|
1746
|
+
abandoned = true;
|
|
1747
|
+
abandon(reason);
|
|
1748
|
+
};
|
|
1749
|
+
const wrapped = {
|
|
1750
|
+
next: (...args) => gen.next(...args).catch((error) => {
|
|
1751
|
+
if (abandoned) return {
|
|
1752
|
+
done: true,
|
|
1753
|
+
value: void 0
|
|
1754
|
+
};
|
|
1755
|
+
throw error;
|
|
1756
|
+
}),
|
|
1757
|
+
return(value) {
|
|
1758
|
+
giveUp();
|
|
1759
|
+
return gen.return(value);
|
|
1760
|
+
},
|
|
1761
|
+
throw(error) {
|
|
1762
|
+
giveUp(error);
|
|
1763
|
+
return gen.throw(error);
|
|
1764
|
+
},
|
|
1765
|
+
[Symbol.asyncIterator]() {
|
|
1766
|
+
return wrapped;
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1769
|
+
return asyncDisposable$1(wrapped, () => wrapped.return(void 0));
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1538
1772
|
* The `type` of the out-of-band notice a side posts when it abandons a stream.
|
|
1539
1773
|
*
|
|
1540
1774
|
* Layer 1's `close` is not observable to layer 2 — a closed virtual port drops
|
|
@@ -1630,7 +1864,7 @@ function runCallerSide(port, input, options) {
|
|
|
1630
1864
|
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
1631
1865
|
const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
|
|
1632
1866
|
pump.catch(() => {});
|
|
1633
|
-
return (async function* () {
|
|
1867
|
+
return returningAtOnce((async function* () {
|
|
1634
1868
|
try {
|
|
1635
1869
|
yield* inbound.stream;
|
|
1636
1870
|
} finally {
|
|
@@ -1644,7 +1878,9 @@ function runCallerSide(port, input, options) {
|
|
|
1644
1878
|
await port.close?.();
|
|
1645
1879
|
} catch {}
|
|
1646
1880
|
}
|
|
1647
|
-
})()
|
|
1881
|
+
})(), () => {
|
|
1882
|
+
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
1883
|
+
});
|
|
1648
1884
|
}
|
|
1649
1885
|
/**
|
|
1650
1886
|
* Listens for the peer's abort notice, and can post our own. The notice is a
|
|
@@ -2040,9 +2276,22 @@ const connect = async ({ mux: factory, timeout }) => {
|
|
|
2040
2276
|
maxMessageSize: mux.maxMessageSize,
|
|
2041
2277
|
timeout
|
|
2042
2278
|
};
|
|
2043
|
-
const call = (input) =>
|
|
2044
|
-
|
|
2045
|
-
|
|
2279
|
+
const call = (input) => {
|
|
2280
|
+
let stream;
|
|
2281
|
+
let abandoned = false;
|
|
2282
|
+
return returningAtOnce((async function* () {
|
|
2283
|
+
const port = await mux.openPort(STREAM_META);
|
|
2284
|
+
if (abandoned) {
|
|
2285
|
+
await port.close?.();
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
stream = duplexOverPort(port, streamOptions)(input);
|
|
2289
|
+
yield* stream;
|
|
2290
|
+
})(), () => {
|
|
2291
|
+
abandoned = true;
|
|
2292
|
+
stream?.return(void 0).catch(() => {});
|
|
2293
|
+
});
|
|
2294
|
+
};
|
|
2046
2295
|
return {
|
|
2047
2296
|
call,
|
|
2048
2297
|
async close() {
|
|
@@ -2133,6 +2382,27 @@ const structuredCodec = {
|
|
|
2133
2382
|
}
|
|
2134
2383
|
};
|
|
2135
2384
|
//#endregion
|
|
2385
|
+
//#region src/link/async-dispose.ts
|
|
2386
|
+
/**
|
|
2387
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
2388
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
2389
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
2390
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
2391
|
+
* declared types, which would require consumers to compile with the
|
|
2392
|
+
* `esnext.disposable` lib.
|
|
2393
|
+
*/
|
|
2394
|
+
function asyncDisposable(target, dispose) {
|
|
2395
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
2396
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
2397
|
+
value: async () => {
|
|
2398
|
+
await dispose();
|
|
2399
|
+
},
|
|
2400
|
+
configurable: true,
|
|
2401
|
+
writable: true
|
|
2402
|
+
});
|
|
2403
|
+
return target;
|
|
2404
|
+
}
|
|
2405
|
+
//#endregion
|
|
2136
2406
|
//#region src/link/http-link.ts
|
|
2137
2407
|
/**
|
|
2138
2408
|
* The version of the HTTP-over-`MessagePort` protocol this package speaks,
|
|
@@ -2162,11 +2432,12 @@ var LinkLostError = class extends Error {
|
|
|
2162
2432
|
*/
|
|
2163
2433
|
const LINK_CLOSE = "webrun-http:link-close";
|
|
2164
2434
|
/** Opens the calling end of a link on `port` (one port per link). */
|
|
2165
|
-
async function connectHttpLink(port, { streamTimeout } = {}) {
|
|
2435
|
+
async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
|
|
2166
2436
|
const { call, close } = await connect({
|
|
2167
2437
|
mux: overPipe(port, {
|
|
2168
2438
|
codec: structuredCodec,
|
|
2169
|
-
side: "initiator"
|
|
2439
|
+
side: "initiator",
|
|
2440
|
+
maxPorts
|
|
2170
2441
|
}),
|
|
2171
2442
|
timeout: streamTimeout
|
|
2172
2443
|
});
|
|
@@ -2182,14 +2453,15 @@ async function connectHttpLink(port, { streamTimeout } = {}) {
|
|
|
2182
2453
|
* that abandons the requests still running (their handlers see the
|
|
2183
2454
|
* cancellation) and releases the port.
|
|
2184
2455
|
*/
|
|
2185
|
-
async function serveHttpLink(port, handler, { streamTimeout } = {}) {
|
|
2456
|
+
async function serveHttpLink(port, handler, { streamTimeout, maxPorts, requestIdleMs } = {}) {
|
|
2186
2457
|
const teardown = await serve({
|
|
2187
2458
|
mux: overPipe(port, {
|
|
2188
2459
|
codec: structuredCodec,
|
|
2189
|
-
side: "responder"
|
|
2460
|
+
side: "responder",
|
|
2461
|
+
maxPorts
|
|
2190
2462
|
}),
|
|
2191
2463
|
timeout: streamTimeout
|
|
2192
|
-
}, serveFetchOverDuplex(async (request) => await handler(request)));
|
|
2464
|
+
}, serveFetchOverDuplex(async (request) => await handler(request), { requestIdleMs }));
|
|
2193
2465
|
const onClose = (event) => {
|
|
2194
2466
|
if (event.data?.type === LINK_CLOSE) close();
|
|
2195
2467
|
};
|
|
@@ -2214,7 +2486,7 @@ function trackedLink(send, release) {
|
|
|
2214
2486
|
inflight--;
|
|
2215
2487
|
if (inflight === 0) for (const resolve of idleWaiters.splice(0)) resolve();
|
|
2216
2488
|
};
|
|
2217
|
-
|
|
2489
|
+
const link = {
|
|
2218
2490
|
get inflight() {
|
|
2219
2491
|
return inflight;
|
|
2220
2492
|
},
|
|
@@ -2267,6 +2539,7 @@ function trackedLink(send, release) {
|
|
|
2267
2539
|
} catch {}
|
|
2268
2540
|
}
|
|
2269
2541
|
};
|
|
2542
|
+
return asyncDisposable(link, () => link.close());
|
|
2270
2543
|
}
|
|
2271
2544
|
/**
|
|
2272
2545
|
* A response body that reports its end (done, cancelled or failed) and fails
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/webrun-http-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "ServiceWorker-based HTTP and WebSocket server for browsers: handlers in pages and Dedicated Workers, same-origin and relay modes, streams with backpressure",
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
],
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"idb-keyval": "^6.3.0",
|
|
61
|
-
"@statewalker/webrun-
|
|
62
|
-
"@statewalker/webrun-rpc": "^0.5.
|
|
63
|
-
"@statewalker/webrun-streams": "^0.
|
|
61
|
+
"@statewalker/webrun-streams": "^0.2.3",
|
|
62
|
+
"@statewalker/webrun-rpc": "^0.5.1",
|
|
63
|
+
"@statewalker/webrun-http-streams": "^0.3.1"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@biomejs/biome": "^2.5.15",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
3
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
4
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
5
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
6
|
+
* declared types, which would require consumers to compile with the
|
|
7
|
+
* `esnext.disposable` lib.
|
|
8
|
+
*/
|
|
9
|
+
export function asyncDisposable<T extends object>(target: T, dispose: () => Promise<unknown>): T {
|
|
10
|
+
// Read here, not at module level: a top-level read is code a bundler keeps.
|
|
11
|
+
const asyncDispose = (Symbol as { asyncDispose?: symbol }).asyncDispose;
|
|
12
|
+
if (asyncDispose && !(asyncDispose in target)) {
|
|
13
|
+
Object.defineProperty(target, asyncDispose, {
|
|
14
|
+
value: async () => {
|
|
15
|
+
await dispose();
|
|
16
|
+
},
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return target;
|
|
22
|
+
}
|
package/src/link/http-link.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fetchOverDuplex, serveFetchOverDuplex } from "@statewalker/webrun-http-streams";
|
|
2
2
|
import { connect, overPipe, serve, structuredCodec } from "@statewalker/webrun-rpc";
|
|
3
|
+
import { asyncDisposable } from "./async-dispose.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* The version of the HTTP-over-`MessagePort` protocol this package speaks,
|
|
@@ -19,6 +20,20 @@ export interface HttpLinkOptions {
|
|
|
19
20
|
* none: a slow consumer is throttled, never failed.
|
|
20
21
|
*/
|
|
21
22
|
streamTimeout?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Ceiling on the requests in flight on the link at once: webrun-rpc's
|
|
25
|
+
* `maxPorts`, one multiplexed port per request (default 1024). A request
|
|
26
|
+
* past it fails on the calling end; one the answering end has no room for is
|
|
27
|
+
* refused. Every exchange gives its port back when it ends, however it ends.
|
|
28
|
+
*/
|
|
29
|
+
maxPorts?: number;
|
|
30
|
+
/**
|
|
31
|
+
* The answering end only: once a response is out, how long the request body
|
|
32
|
+
* may go unread before it is abandoned and the exchange ends (default 30 s;
|
|
33
|
+
* webrun-http-streams' `requestIdleMs`). A handler that answers first and
|
|
34
|
+
* reads the upload afterwards keeps it as long as it keeps reading.
|
|
35
|
+
*/
|
|
36
|
+
requestIdleMs?: number;
|
|
22
37
|
}
|
|
23
38
|
|
|
24
39
|
/** The calling end of a link: sends requests, gets responses. */
|
|
@@ -62,10 +77,10 @@ const LINK_CLOSE = "webrun-http:link-close";
|
|
|
62
77
|
/** Opens the calling end of a link on `port` (one port per link). */
|
|
63
78
|
export async function connectHttpLink(
|
|
64
79
|
port: MessagePort,
|
|
65
|
-
{ streamTimeout }: HttpLinkOptions = {},
|
|
80
|
+
{ streamTimeout, maxPorts }: HttpLinkOptions = {},
|
|
66
81
|
): Promise<HttpLink> {
|
|
67
82
|
const { call, close } = await connect({
|
|
68
|
-
mux: overPipe(port, { codec: structuredCodec, side: "initiator" }),
|
|
83
|
+
mux: overPipe(port, { codec: structuredCodec, side: "initiator", maxPorts }),
|
|
69
84
|
timeout: streamTimeout,
|
|
70
85
|
});
|
|
71
86
|
return trackedLink(
|
|
@@ -87,14 +102,14 @@ export async function connectHttpLink(
|
|
|
87
102
|
export async function serveHttpLink(
|
|
88
103
|
port: MessagePort,
|
|
89
104
|
handler: (request: Request) => Response | Promise<Response>,
|
|
90
|
-
{ streamTimeout }: HttpLinkOptions = {},
|
|
105
|
+
{ streamTimeout, maxPorts, requestIdleMs }: HttpLinkOptions = {},
|
|
91
106
|
): Promise<() => Promise<void>> {
|
|
92
107
|
const teardown = await serve(
|
|
93
108
|
{
|
|
94
|
-
mux: overPipe(port, { codec: structuredCodec, side: "responder" }),
|
|
109
|
+
mux: overPipe(port, { codec: structuredCodec, side: "responder", maxPorts }),
|
|
95
110
|
timeout: streamTimeout,
|
|
96
111
|
},
|
|
97
|
-
serveFetchOverDuplex(async (request) => await handler(request)),
|
|
112
|
+
serveFetchOverDuplex(async (request) => await handler(request), { requestIdleMs }),
|
|
98
113
|
);
|
|
99
114
|
const onClose = (event: MessageEvent) => {
|
|
100
115
|
if ((event.data as { type?: unknown } | null)?.type === LINK_CLOSE) void close();
|
|
@@ -184,7 +199,8 @@ export function trackedLink(
|
|
|
184
199
|
} catch {}
|
|
185
200
|
},
|
|
186
201
|
};
|
|
187
|
-
|
|
202
|
+
// `await using link = await connectHttpLink(port)` closes it at the end of the block.
|
|
203
|
+
return asyncDisposable(link, () => link.close());
|
|
188
204
|
}
|
|
189
205
|
|
|
190
206
|
/**
|