@statewalker/webrun-http-browser 0.6.5 → 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/README.md +302 -52
- package/dist/core/data-calls.d.ts +17 -0
- package/dist/core/data-calls.d.ts.map +1 -1
- package/dist/core/service-worker-control.d.ts +3 -1
- package/dist/core/service-worker-control.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4218 -301
- package/dist/link/async-dispose.d.ts +10 -0
- package/dist/link/async-dispose.d.ts.map +1 -0
- package/dist/link/errors.d.ts +2 -0
- package/dist/link/errors.d.ts.map +1 -0
- package/dist/link/http-link.d.ts +72 -0
- package/dist/link/http-link.d.ts.map +1 -0
- package/dist/link/index.d.ts +4 -0
- package/dist/link/index.d.ts.map +1 -0
- package/dist/link/locks.d.ts +39 -0
- package/dist/link/locks.d.ts.map +1 -0
- package/dist/link/rendezvous.d.ts +21 -0
- package/dist/link/rendezvous.d.ts.map +1 -0
- package/dist/relay/index-sw.d.ts +12 -2
- package/dist/relay/index-sw.d.ts.map +1 -1
- package/dist/relay/index.d.ts +66 -8
- package/dist/relay/index.d.ts.map +1 -1
- package/dist/relay-sw.js +1328 -64
- package/dist/relay-worker.js +1329 -64
- package/dist/sw/handler-endpoint.d.ts +81 -0
- package/dist/sw/handler-endpoint.d.ts.map +1 -0
- package/dist/sw/handler-table.d.ts +53 -0
- package/dist/sw/handler-table.d.ts.map +1 -0
- package/dist/sw/http-sw-dispatcher.d.ts +162 -21
- package/dist/sw/http-sw-dispatcher.d.ts.map +1 -1
- package/dist/sw/index.d.ts +5 -0
- package/dist/sw/index.d.ts.map +1 -1
- package/dist/sw/protocol.d.ts +115 -0
- package/dist/sw/protocol.d.ts.map +1 -0
- package/dist/sw/sw-dispatcher.d.ts.map +1 -1
- package/dist/sw/sw-http-adapter.d.ts +199 -0
- package/dist/sw/sw-http-adapter.d.ts.map +1 -0
- package/dist/sw/sw-websocket.d.ts +16 -0
- package/dist/sw/sw-websocket.d.ts.map +1 -0
- package/dist/sw/worker-service.d.ts +45 -0
- package/dist/sw/worker-service.d.ts.map +1 -0
- package/dist/sw/worker-url.d.ts +9 -0
- package/dist/sw/worker-url.d.ts.map +1 -0
- package/dist/sw-worker.js +2403 -265
- package/dist/sw.d.ts +2 -0
- package/dist/sw.d.ts.map +1 -1
- package/dist/sw.js +4189 -546
- package/dist/worker.d.ts +10 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +3780 -0
- package/dist/ws/client.d.ts +73 -0
- package/dist/ws/client.d.ts.map +1 -0
- package/dist/ws/frames.d.ts +68 -0
- package/dist/ws/frames.d.ts.map +1 -0
- package/dist/ws/index.d.ts +6 -0
- package/dist/ws/index.d.ts.map +1 -0
- package/dist/ws/server.d.ts +53 -0
- package/dist/ws/server.d.ts.map +1 -0
- package/dist/ws/socket-core.d.ts +91 -0
- package/dist/ws/socket-core.d.ts.map +1 -0
- package/dist/ws/upgrade.d.ts +93 -0
- package/dist/ws/upgrade.d.ts.map +1 -0
- package/package.json +10 -5
- package/src/core/data-calls.ts +52 -2
- package/src/core/service-worker-control.ts +7 -1
- package/src/index.ts +3 -0
- package/src/link/async-dispose.ts +22 -0
- package/src/link/errors.ts +3 -0
- package/src/link/http-link.ts +238 -0
- package/src/link/index.ts +3 -0
- package/src/link/locks.ts +97 -0
- package/src/link/rendezvous.ts +60 -0
- package/src/relay/index-sw.ts +84 -12
- package/src/relay/index.ts +203 -27
- package/src/sw/handler-endpoint.ts +278 -0
- package/src/sw/handler-table.ts +130 -0
- package/src/sw/http-sw-dispatcher.ts +649 -90
- package/src/sw/index.ts +5 -0
- package/src/sw/protocol.ts +103 -0
- package/src/sw/sw-dispatcher.ts +1 -20
- package/src/sw/sw-http-adapter.ts +613 -0
- package/src/sw/sw-websocket.ts +43 -0
- package/src/sw/worker-service.ts +133 -0
- package/src/sw/worker-url.ts +19 -0
- package/src/sw-worker.ts +8 -3
- package/src/sw.ts +2 -0
- package/src/worker.ts +9 -0
- package/src/ws/client.ts +346 -0
- package/src/ws/frames.ts +119 -0
- package/src/ws/index.ts +11 -0
- package/src/ws/server.ts +132 -0
- package/src/ws/socket-core.ts +251 -0
- package/src/ws/upgrade.ts +200 -0
package/dist/relay-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
|
* The iterator ↔ `ReadableStream` boundary.
|
|
182
179
|
*
|
|
@@ -281,6 +278,34 @@ async function sendIterator(send, it) {
|
|
|
281
278
|
});
|
|
282
279
|
}
|
|
283
280
|
}
|
|
281
|
+
const DEFAULT_CHUNK_SIZE = 16384;
|
|
282
|
+
/**
|
|
283
|
+
* Curried `Duplex`-shaped transformer that splits incoming `Uint8Array`s into
|
|
284
|
+
* chunks no larger than `size` bytes. Empty input chunks are skipped; small
|
|
285
|
+
* input chunks pass through unchanged (zero-copy `subarray` views are used
|
|
286
|
+
* when splitting, so no allocation per chunk).
|
|
287
|
+
*
|
|
288
|
+
* Use to respect transport MTUs before reaching `channel.send`. Reassembly on
|
|
289
|
+
* the receive side is not needed — consumers iterate a continuous byte stream.
|
|
290
|
+
*/
|
|
291
|
+
function toChunks(size = DEFAULT_CHUNK_SIZE) {
|
|
292
|
+
if (!(Number.isInteger(size) && size > 0)) throw new RangeError(`toChunks: size must be a positive integer, got ${size}`);
|
|
293
|
+
return async function* (input) {
|
|
294
|
+
for await (const block of input) {
|
|
295
|
+
if (block.byteLength === 0) continue;
|
|
296
|
+
if (block.byteLength <= size) {
|
|
297
|
+
yield block;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
let offset = 0;
|
|
301
|
+
while (offset < block.byteLength) {
|
|
302
|
+
const end = Math.min(offset + size, block.byteLength);
|
|
303
|
+
yield block.subarray(offset, end);
|
|
304
|
+
offset = end;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
284
309
|
//#endregion
|
|
285
310
|
//#region ../webrun-http-streams/dist/index.js
|
|
286
311
|
const CR = 13;
|
|
@@ -418,8 +443,8 @@ var HttpParseError = class extends Error {
|
|
|
418
443
|
const NEWLINE = 10;
|
|
419
444
|
/** Mirrors the HTTP/1.1 codec's default `maxHeaderBytes`. */
|
|
420
445
|
const MAX_ENVELOPE_BYTES = 65536;
|
|
421
|
-
const encoder$1 = new TextEncoder();
|
|
422
|
-
const decoder = new TextDecoder();
|
|
446
|
+
const encoder$1 = /* @__PURE__ */ new TextEncoder();
|
|
447
|
+
const decoder = /* @__PURE__ */ new TextDecoder();
|
|
423
448
|
/**
|
|
424
449
|
* Encode an HTTP envelope plus optional body as one continuous byte stream:
|
|
425
450
|
*
|
|
@@ -673,15 +698,15 @@ function resolveFraming(headers, version) {
|
|
|
673
698
|
};
|
|
674
699
|
return { kind: "none" };
|
|
675
700
|
}
|
|
676
|
-
const CRLF = new Uint8Array([13, 10]);
|
|
677
|
-
const LAST_CHUNK = new Uint8Array([
|
|
701
|
+
const CRLF = /* @__PURE__ */ new Uint8Array([13, 10]);
|
|
702
|
+
const LAST_CHUNK = /* @__PURE__ */ new Uint8Array([
|
|
678
703
|
48,
|
|
679
704
|
13,
|
|
680
705
|
10,
|
|
681
706
|
13,
|
|
682
707
|
10
|
|
683
708
|
]);
|
|
684
|
-
const encoder = new TextEncoder();
|
|
709
|
+
const encoder = /* @__PURE__ */ new TextEncoder();
|
|
685
710
|
/**
|
|
686
711
|
* Chunk sizes are written in HEXADECIMAL — a 21-byte chunk is `15`. Writing
|
|
687
712
|
* them in decimal is the defect note 15 found in @libp2p/http, and it is the
|
|
@@ -986,7 +1011,7 @@ function newHttpCodec(options = {}) {
|
|
|
986
1011
|
decodeResponse: (input, o) => decodeResponse(input, opts, o.method)
|
|
987
1012
|
};
|
|
988
1013
|
}
|
|
989
|
-
const httpCodec = newHttpCodec();
|
|
1014
|
+
const httpCodec = /* @__PURE__ */ newHttpCodec();
|
|
990
1015
|
/**
|
|
991
1016
|
* Dispatches on byte 0. The formats are self-identifying — a JSON envelope
|
|
992
1017
|
* always begins `{`, which is not a token character and so can never begin an
|
|
@@ -1032,10 +1057,163 @@ function newSniffingCodec(options) {
|
|
|
1032
1057
|
}
|
|
1033
1058
|
};
|
|
1034
1059
|
}
|
|
1035
|
-
|
|
1060
|
+
/**
|
|
1061
|
+
* Writes HTTP/1.1; accepts HTTP/1.1 or the legacy JSON envelope. Used whenever
|
|
1062
|
+
* no `codec` option is supplied.
|
|
1063
|
+
*/
|
|
1064
|
+
const defaultCodec = /* @__PURE__ */ newSniffingCodec({
|
|
1036
1065
|
write: httpCodec,
|
|
1037
1066
|
accept: [httpCodec, jsonEnvelopeCodec]
|
|
1038
1067
|
});
|
|
1068
|
+
/** Carries the serialized JS error between two webrun peers. */
|
|
1069
|
+
const PEER_ERROR_HEADER = "x-webrun-error";
|
|
1070
|
+
/**
|
|
1071
|
+
* `output` is the `Duplex` call's own generator — one logical call, per the
|
|
1072
|
+
* `Duplex` contract in `@statewalker/webrun-streams`: "Consumer `.return()`
|
|
1073
|
+
* on the output → producer's `finally` runs." On a mux transport
|
|
1074
|
+
* (`emulateMux`), that `finally` is what frees the stream-table slot; skip it
|
|
1075
|
+
* and every peer-error response leaks one slot, unboundedly, until the mux
|
|
1076
|
+
* itself is exhausted (`maxStreams` reached, every further call rejected).
|
|
1077
|
+
*
|
|
1078
|
+
* Cancelling `output` here — not inside the codec's own decode logic — is
|
|
1079
|
+
* deliberate. `codec.decodeResponse` already pulled from `output` to read the
|
|
1080
|
+
* head before this function runs, so unlike the body case above there's no
|
|
1081
|
+
* suspended-start no-op to worry about. And it's safe specifically *because*
|
|
1082
|
+
* this is the client's own response-only generator: the request was already
|
|
1083
|
+
* fully sent before we got here, so there is nothing left to write on it.
|
|
1084
|
+
* The equivalent is NOT safe inside `src/http1/decode.ts`'s `ByteReader` —
|
|
1085
|
+
* that code also runs when a caller wires `codec.decodeRequest`/
|
|
1086
|
+
* `decodeResponse` directly onto a raw bidirectional socket (see
|
|
1087
|
+
* `tests/http1-node-interop.test.ts`), where the *same* object is read from
|
|
1088
|
+
* and then written back to (a server reads the request, then replies on the
|
|
1089
|
+
* same socket); cancelling the read side there tears down the whole
|
|
1090
|
+
* connection out from under the pending write. That was tried and reverted —
|
|
1091
|
+
* see the Task 11 report for the "socket hang up" failure it caused.
|
|
1092
|
+
*/
|
|
1093
|
+
async function throwIfPeerError(result, output) {
|
|
1094
|
+
const found = result.envelope.headers.find(([k]) => k.toLowerCase() === PEER_ERROR_HEADER);
|
|
1095
|
+
if (!found) return;
|
|
1096
|
+
await discard(result.body);
|
|
1097
|
+
try {
|
|
1098
|
+
await output.return?.(void 0);
|
|
1099
|
+
} catch {}
|
|
1100
|
+
let payload;
|
|
1101
|
+
try {
|
|
1102
|
+
payload = JSON.parse(found[1]);
|
|
1103
|
+
} catch {
|
|
1104
|
+
payload = { message: found[1] };
|
|
1105
|
+
}
|
|
1106
|
+
throw deserializeError(payload);
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Initiate an HTTP call over a `Duplex`. The caller's `call: Duplex` is
|
|
1110
|
+
* obtained from any `webrun-streams-*` adapter's `connect`. Returns the
|
|
1111
|
+
* response envelope and an async iterable over the response body bytes.
|
|
1112
|
+
*
|
|
1113
|
+
* The call is one logical Duplex invocation; multiplexing of concurrent
|
|
1114
|
+
* calls is the adapter's concern (native or `emulateMux`).
|
|
1115
|
+
*/
|
|
1116
|
+
async function httpFetch(call, env, body, options = {}) {
|
|
1117
|
+
const codec = options.codec ?? defaultCodec;
|
|
1118
|
+
const { signal } = options;
|
|
1119
|
+
if (signal?.aborted) throw abortReason$1(signal);
|
|
1120
|
+
const output = call(codec.encodeRequest(env, body));
|
|
1121
|
+
let released;
|
|
1122
|
+
const release = () => {
|
|
1123
|
+
released ??= (async () => {
|
|
1124
|
+
try {
|
|
1125
|
+
await output.return?.(void 0);
|
|
1126
|
+
} catch {}
|
|
1127
|
+
})();
|
|
1128
|
+
return released;
|
|
1129
|
+
};
|
|
1130
|
+
let finished = false;
|
|
1131
|
+
const finish = () => {
|
|
1132
|
+
if (finished || released) return;
|
|
1133
|
+
finished = true;
|
|
1134
|
+
if (signal?.aborted) {
|
|
1135
|
+
release();
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
const onAbort = () => void release();
|
|
1139
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1140
|
+
drain(output).finally(() => signal?.removeEventListener("abort", onAbort));
|
|
1141
|
+
};
|
|
1142
|
+
let decoded;
|
|
1143
|
+
try {
|
|
1144
|
+
decoded = await untilAborted(codec.decodeResponse(output, { method: env.method }), signal);
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
if (signal?.aborted) release();
|
|
1147
|
+
else await release();
|
|
1148
|
+
throw error;
|
|
1149
|
+
}
|
|
1150
|
+
const result = {
|
|
1151
|
+
envelope: decoded.envelope,
|
|
1152
|
+
body: releasingOnExit(decoded.body, finish, release),
|
|
1153
|
+
release,
|
|
1154
|
+
finish
|
|
1155
|
+
};
|
|
1156
|
+
await throwIfPeerError(result, output);
|
|
1157
|
+
return result;
|
|
1158
|
+
}
|
|
1159
|
+
/** `promise`, rejected with the signal's reason if it aborts first. */
|
|
1160
|
+
function untilAborted(promise, signal) {
|
|
1161
|
+
if (!signal) return promise;
|
|
1162
|
+
return new Promise((resolve, reject) => {
|
|
1163
|
+
const onAbort = () => reject(abortReason$1(signal));
|
|
1164
|
+
if (signal.aborted) return onAbort();
|
|
1165
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1166
|
+
promise.then((value) => {
|
|
1167
|
+
signal.removeEventListener("abort", onAbort);
|
|
1168
|
+
resolve(value);
|
|
1169
|
+
}, (error) => {
|
|
1170
|
+
signal.removeEventListener("abort", onAbort);
|
|
1171
|
+
reject(error);
|
|
1172
|
+
});
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
/** The signal's reason as an `Error` (`AbortError` when it gave none). */
|
|
1176
|
+
function abortReason$1(signal) {
|
|
1177
|
+
const reason = signal.reason;
|
|
1178
|
+
if (reason instanceof Error) return reason;
|
|
1179
|
+
const err = new Error(reason === void 0 ? "Aborted" : String(reason));
|
|
1180
|
+
err.name = "AbortError";
|
|
1181
|
+
return err;
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* `body`, ending the call however it is left: read to its end, or left early.
|
|
1185
|
+
*
|
|
1186
|
+
* The body the codec hands back is a generator of its own over a `ByteReader`,
|
|
1187
|
+
* and its ending never reaches the call's `output` (see `throwIfPeerError` for
|
|
1188
|
+
* why the codec must not do that itself). A caller that stopped reading — an
|
|
1189
|
+
* aborted `fetch`, a cancelled response stream — left the peer's producer
|
|
1190
|
+
* parked forever; and one that read the body to its end left the call itself
|
|
1191
|
+
* open, which on a multiplexed transport is one port per exchange until the
|
|
1192
|
+
* multiplexer refuses to open more (`maxPorts`).
|
|
1193
|
+
*
|
|
1194
|
+
* Left early, the call is abandoned (`.return()`): the peer's producer is
|
|
1195
|
+
* released. Read to its end, the call is not cut short but read to ITS end, in
|
|
1196
|
+
* the background: the peer ends it once its side of the exchange is over,
|
|
1197
|
+
* which may be later than the response — a handler that answered before it
|
|
1198
|
+
* read the request body is still receiving it (`HttpDataHandlerResult.settled`),
|
|
1199
|
+
* and abandoning the call would abort that upload.
|
|
1200
|
+
*/
|
|
1201
|
+
async function* releasingOnExit(body, finish, release) {
|
|
1202
|
+
let completed = false;
|
|
1203
|
+
try {
|
|
1204
|
+
yield* body;
|
|
1205
|
+
completed = true;
|
|
1206
|
+
} finally {
|
|
1207
|
+
if (completed) finish();
|
|
1208
|
+
else await release();
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
/** Reads `output` to its end, discarding what is left, ignoring a failure. */
|
|
1212
|
+
async function drain(output) {
|
|
1213
|
+
try {
|
|
1214
|
+
for (let r = await output.next(); !r.done; r = await output.next());
|
|
1215
|
+
} catch {}
|
|
1216
|
+
}
|
|
1039
1217
|
/**
|
|
1040
1218
|
* The one place that knows a runtime may not implement request body streams,
|
|
1041
1219
|
* and the buffering both fallbacks need. `fetch.ts` and `http-stubs.ts` each
|
|
@@ -1141,6 +1319,118 @@ function newHttpClientStub(send) {
|
|
|
1141
1319
|
return new Response(toReadableStream(result.content[Symbol.asyncIterator]()), responseOptions);
|
|
1142
1320
|
};
|
|
1143
1321
|
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Connection-scoped headers. The codec surfaces them verbatim (decision 12),
|
|
1324
|
+
* but they are meaningless to a `Request`/`Response`, and re-emitting them
|
|
1325
|
+
* from a relay would corrupt its framing.
|
|
1326
|
+
*/
|
|
1327
|
+
const HOP_BY_HOP = /* @__PURE__ */ new Set([
|
|
1328
|
+
"connection",
|
|
1329
|
+
"host",
|
|
1330
|
+
"keep-alive",
|
|
1331
|
+
"proxy-authenticate",
|
|
1332
|
+
"proxy-authorization",
|
|
1333
|
+
"te",
|
|
1334
|
+
"trailer",
|
|
1335
|
+
"transfer-encoding",
|
|
1336
|
+
"upgrade"
|
|
1337
|
+
]);
|
|
1338
|
+
function forwardableHeaders(headers) {
|
|
1339
|
+
return headers.filter(([name]) => !HOP_BY_HOP.has(name.toLowerCase()));
|
|
1340
|
+
}
|
|
1341
|
+
function headersToArray(headers) {
|
|
1342
|
+
const out = [];
|
|
1343
|
+
headers.forEach((value, key) => {
|
|
1344
|
+
out.push([key, value]);
|
|
1345
|
+
});
|
|
1346
|
+
return out;
|
|
1347
|
+
}
|
|
1348
|
+
async function* readableToAsyncIterable(stream) {
|
|
1349
|
+
if (!stream) return;
|
|
1350
|
+
const reader = stream.getReader();
|
|
1351
|
+
let consumed = false;
|
|
1352
|
+
try {
|
|
1353
|
+
while (true) {
|
|
1354
|
+
const { done, value } = await reader.read();
|
|
1355
|
+
if (done) {
|
|
1356
|
+
consumed = true;
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
if (value) yield value;
|
|
1360
|
+
}
|
|
1361
|
+
} finally {
|
|
1362
|
+
if (!consumed) try {
|
|
1363
|
+
await reader.cancel();
|
|
1364
|
+
} catch {}
|
|
1365
|
+
try {
|
|
1366
|
+
reader.releaseLock();
|
|
1367
|
+
} catch {}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
function asyncIterableToReadable(iter) {
|
|
1371
|
+
const it = iter[Symbol.asyncIterator]();
|
|
1372
|
+
return new ReadableStream({
|
|
1373
|
+
async pull(controller) {
|
|
1374
|
+
try {
|
|
1375
|
+
const { done, value } = await it.next();
|
|
1376
|
+
if (done) {
|
|
1377
|
+
controller.close();
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
controller.enqueue(value);
|
|
1381
|
+
} catch (err) {
|
|
1382
|
+
controller.error(err);
|
|
1383
|
+
}
|
|
1384
|
+
},
|
|
1385
|
+
async cancel() {
|
|
1386
|
+
await it.return?.();
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Run a `Request` through a `Duplex` call and reconstruct the `Response` on
|
|
1392
|
+
* the other side. The request's `signal` is plumbed into the body iteration —
|
|
1393
|
+
* abort terminates the underlying call.
|
|
1394
|
+
*/
|
|
1395
|
+
async function fetchOverDuplex(call, request, options = {}) {
|
|
1396
|
+
if (request.signal?.aborted) throw abortReason$1(request.signal);
|
|
1397
|
+
const env = {
|
|
1398
|
+
url: request.url,
|
|
1399
|
+
method: request.method,
|
|
1400
|
+
headers: headersToArray(request.headers)
|
|
1401
|
+
};
|
|
1402
|
+
let body;
|
|
1403
|
+
if (request.body != null) body = readableToAsyncIterable(request.body);
|
|
1404
|
+
else if (!supportsRequestStreams()) {
|
|
1405
|
+
const buffered = new Uint8Array(await request.arrayBuffer());
|
|
1406
|
+
if (buffered.byteLength > 0) body = [buffered];
|
|
1407
|
+
}
|
|
1408
|
+
const { envelope, body: respBody, release, finish } = await httpFetch(call, env, body, {
|
|
1409
|
+
...options,
|
|
1410
|
+
signal: request.signal ?? void 0
|
|
1411
|
+
});
|
|
1412
|
+
const responseInit = {
|
|
1413
|
+
status: envelope.status,
|
|
1414
|
+
statusText: envelope.statusText,
|
|
1415
|
+
headers: forwardableHeaders(envelope.headers)
|
|
1416
|
+
};
|
|
1417
|
+
if (request.method === "HEAD" || request.method === "OPTIONS" || NULL_BODY_STATUSES.has(envelope.status)) {
|
|
1418
|
+
if (finish) finish();
|
|
1419
|
+
else await release?.();
|
|
1420
|
+
return new Response(null, responseInit);
|
|
1421
|
+
}
|
|
1422
|
+
return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
|
|
1423
|
+
}
|
|
1424
|
+
async function* withAbort(iter, signal) {
|
|
1425
|
+
if (!signal) {
|
|
1426
|
+
yield* iter;
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
for await (const chunk of iter) {
|
|
1430
|
+
if (signal.aborted) throw abortReason$1(signal);
|
|
1431
|
+
yield chunk;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1144
1434
|
var HttpError = class HttpError extends Error {
|
|
1145
1435
|
status;
|
|
1146
1436
|
statusText;
|
|
@@ -1253,6 +1543,32 @@ function set(key, value, customStore = defaultGetStore()) {
|
|
|
1253
1543
|
}
|
|
1254
1544
|
//#endregion
|
|
1255
1545
|
//#region src/core/data-calls.ts
|
|
1546
|
+
const TRANSFER = Symbol.for("webrun-http-browser:transfer");
|
|
1547
|
+
/**
|
|
1548
|
+
* A channel call's result that carries transferables, a `MessagePort` say:
|
|
1549
|
+
* return `withTransfer({ port }, [port])` from a `handleChannelCalls` handler
|
|
1550
|
+
* and `callChannel` resolves with `{ port }`.
|
|
1551
|
+
*/
|
|
1552
|
+
function withTransfer(value, transfer) {
|
|
1553
|
+
return {
|
|
1554
|
+
[TRANSFER]: true,
|
|
1555
|
+
value,
|
|
1556
|
+
transfer
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
function isTransferResult(value) {
|
|
1560
|
+
return typeof value === "object" && value !== null && TRANSFER in value;
|
|
1561
|
+
}
|
|
1562
|
+
/** Splits a handler's result into the value to post and its transfer list. */
|
|
1563
|
+
function unwrapTransfer(result) {
|
|
1564
|
+
return isTransferResult(result) ? {
|
|
1565
|
+
value: result.value,
|
|
1566
|
+
transfer: result.transfer
|
|
1567
|
+
} : {
|
|
1568
|
+
value: result,
|
|
1569
|
+
transfer: []
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1256
1572
|
function callChannel(target, callType, params, ...transfers) {
|
|
1257
1573
|
return new Promise((resolve, reject) => {
|
|
1258
1574
|
const channel = new MessageChannel();
|
|
@@ -1272,12 +1588,14 @@ function handleChannelCalls(target, callType, handler) {
|
|
|
1272
1588
|
if (!event.data || event.data.type !== callType) return;
|
|
1273
1589
|
const [port, ...transfers] = event.ports ?? [];
|
|
1274
1590
|
const response = {};
|
|
1591
|
+
let transfer = [];
|
|
1275
1592
|
try {
|
|
1276
|
-
|
|
1593
|
+
const result = await handler(event, event.data.params, ...transfers);
|
|
1594
|
+
({value: response.result, transfer} = unwrapTransfer(result));
|
|
1277
1595
|
} catch (error) {
|
|
1278
1596
|
response.error = serializeError(error);
|
|
1279
1597
|
}
|
|
1280
|
-
port?.postMessage(response);
|
|
1598
|
+
port?.postMessage(response, transfer);
|
|
1281
1599
|
};
|
|
1282
1600
|
target.addEventListener("message", listener);
|
|
1283
1601
|
target.start?.();
|
|
@@ -1334,6 +1652,899 @@ function handleClaimRequests(self) {
|
|
|
1334
1652
|
});
|
|
1335
1653
|
}
|
|
1336
1654
|
//#endregion
|
|
1655
|
+
//#region ../webrun-rpc/dist/index.js
|
|
1656
|
+
const closeSignals = /* @__PURE__ */ new WeakMap();
|
|
1657
|
+
/**
|
|
1658
|
+
* Internal: register a port's close signal. Called once by `bindBytesToPort`.
|
|
1659
|
+
*
|
|
1660
|
+
* @internal
|
|
1661
|
+
*/
|
|
1662
|
+
function setPortCloseSignal(port, signal) {
|
|
1663
|
+
closeSignals.set(port, signal);
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Return the AbortSignal that fires when `port`'s transport closes, or
|
|
1667
|
+
* `undefined` if `port` is not transport-backed (e.g., a raw
|
|
1668
|
+
* `MessageChannel().port1`).
|
|
1669
|
+
*/
|
|
1670
|
+
function getPortCloseSignal(port) {
|
|
1671
|
+
return closeSignals.get(port);
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Pass as `timeout` to install no deadline at all. Used by the stream tier,
|
|
1675
|
+
* where the deadline belongs to the stream rather than to one chunk (spec D8):
|
|
1676
|
+
* a slow consumer is throttled, never failed.
|
|
1677
|
+
*/
|
|
1678
|
+
const NO_TIMEOUT = Number.POSITIVE_INFINITY;
|
|
1679
|
+
/**
|
|
1680
|
+
* Asynchronous request/response over any `MessageTarget`.
|
|
1681
|
+
*
|
|
1682
|
+
* Sends `params` to the peer listening with `listenPort`, waits up to
|
|
1683
|
+
* `timeout` ms for a matching reply, and either resolves with the result or
|
|
1684
|
+
* rejects with the deserialised error.
|
|
1685
|
+
*/
|
|
1686
|
+
function callPort(port, params, { timeout = 1e3, channelName = "", log = () => {}, newCallId = () => `call-${Date.now()}-${String(Math.random()).substring(2)}`, signal } = {}) {
|
|
1687
|
+
const callId = newCallId();
|
|
1688
|
+
log("[callPort]", {
|
|
1689
|
+
channelName,
|
|
1690
|
+
callId,
|
|
1691
|
+
params
|
|
1692
|
+
});
|
|
1693
|
+
const combinedSignal = combineSignals(signal, getPortCloseSignal(port));
|
|
1694
|
+
let timerId;
|
|
1695
|
+
let onMessage;
|
|
1696
|
+
let onAbort;
|
|
1697
|
+
const promise = new Promise((resolve, reject) => {
|
|
1698
|
+
if (combinedSignal?.aborted) {
|
|
1699
|
+
reject(abortReason(combinedSignal));
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
if (Number.isFinite(timeout) && timeout > 0) timerId = setTimeout(() => reject(/* @__PURE__ */ new Error(`Call timeout. CallId: "${callId}".`)), timeout);
|
|
1703
|
+
onMessage = (event) => {
|
|
1704
|
+
const data = event.data;
|
|
1705
|
+
if (!data) return;
|
|
1706
|
+
if (data.channelName !== channelName) return;
|
|
1707
|
+
if (data.callId !== callId) return;
|
|
1708
|
+
if (data.type === "response:error") reject(deserializeError(data.error));
|
|
1709
|
+
else if (data.type === "response:result") resolve(data.result);
|
|
1710
|
+
};
|
|
1711
|
+
port.addEventListener("message", onMessage);
|
|
1712
|
+
if (combinedSignal) {
|
|
1713
|
+
onAbort = () => reject(abortReason(combinedSignal));
|
|
1714
|
+
combinedSignal.addEventListener("abort", onAbort, { once: true });
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
promise.catch(() => {}).finally(() => {
|
|
1718
|
+
if (timerId !== void 0) clearTimeout(timerId);
|
|
1719
|
+
if (onMessage) port.removeEventListener("message", onMessage);
|
|
1720
|
+
if (onAbort && combinedSignal) combinedSignal.removeEventListener("abort", onAbort);
|
|
1721
|
+
});
|
|
1722
|
+
port.postMessage({
|
|
1723
|
+
type: "request",
|
|
1724
|
+
channelName,
|
|
1725
|
+
callId,
|
|
1726
|
+
params
|
|
1727
|
+
});
|
|
1728
|
+
return promise;
|
|
1729
|
+
}
|
|
1730
|
+
function abortReason(signal) {
|
|
1731
|
+
const reason = signal.reason;
|
|
1732
|
+
if (reason instanceof Error) return reason;
|
|
1733
|
+
const err = new Error(reason === void 0 ? "Aborted" : String(reason));
|
|
1734
|
+
err.name = "AbortError";
|
|
1735
|
+
return err;
|
|
1736
|
+
}
|
|
1737
|
+
function combineSignals(...signals) {
|
|
1738
|
+
const live = signals.filter((s) => s !== void 0);
|
|
1739
|
+
if (live.length === 0) return void 0;
|
|
1740
|
+
if (live.length === 1) return live[0];
|
|
1741
|
+
return AbortSignal.any(live);
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* Installs `handler` as the server side of a `callPort` / `listenPort`
|
|
1745
|
+
* request/response pair on `port`.
|
|
1746
|
+
*
|
|
1747
|
+
* Returns a cleanup function that removes the listener.
|
|
1748
|
+
*/
|
|
1749
|
+
function listenPort(port, handler, { channelName = "", log = () => {} } = {}) {
|
|
1750
|
+
const onMessage = async (event) => {
|
|
1751
|
+
const data = event.data;
|
|
1752
|
+
if (!data || data.channelName !== channelName || data.type !== "request") return;
|
|
1753
|
+
const { callId, params } = data;
|
|
1754
|
+
log("[listenPort]", {
|
|
1755
|
+
channelName,
|
|
1756
|
+
callId,
|
|
1757
|
+
params
|
|
1758
|
+
});
|
|
1759
|
+
let result;
|
|
1760
|
+
let error;
|
|
1761
|
+
let type;
|
|
1762
|
+
try {
|
|
1763
|
+
result = await handler(params);
|
|
1764
|
+
type = "response:result";
|
|
1765
|
+
} catch (e) {
|
|
1766
|
+
error = serializeError(e);
|
|
1767
|
+
type = "response:error";
|
|
1768
|
+
}
|
|
1769
|
+
port.postMessage({
|
|
1770
|
+
callId,
|
|
1771
|
+
channelName,
|
|
1772
|
+
type,
|
|
1773
|
+
result,
|
|
1774
|
+
error
|
|
1775
|
+
});
|
|
1776
|
+
};
|
|
1777
|
+
port.addEventListener("message", onMessage);
|
|
1778
|
+
return () => port.removeEventListener("message", onMessage);
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
|
|
1782
|
+
* to return cleanly, forwarding `return()` to the underlying iterator so the
|
|
1783
|
+
* producer (e.g., a user-supplied generator) sees its own `finally` blocks
|
|
1784
|
+
* run immediately rather than waiting for the next yield.
|
|
1785
|
+
*/
|
|
1786
|
+
async function* throughAbort(input, signal) {
|
|
1787
|
+
const iter = input[Symbol.asyncIterator] ? input[Symbol.asyncIterator]() : input[Symbol.iterator]();
|
|
1788
|
+
const onAbort = () => {
|
|
1789
|
+
iter.return?.(void 0);
|
|
1790
|
+
};
|
|
1791
|
+
if (signal.aborted) {
|
|
1792
|
+
iter.return?.(void 0);
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1796
|
+
try {
|
|
1797
|
+
while (true) {
|
|
1798
|
+
const r = await iter.next();
|
|
1799
|
+
if (r.done) return;
|
|
1800
|
+
if (signal.aborted) return;
|
|
1801
|
+
yield r.value;
|
|
1802
|
+
}
|
|
1803
|
+
} finally {
|
|
1804
|
+
signal.removeEventListener("abort", onAbort);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
1809
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
1810
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
1811
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
1812
|
+
* declared types, which would require consumers to compile with the
|
|
1813
|
+
* `esnext.disposable` lib.
|
|
1814
|
+
*/
|
|
1815
|
+
function asyncDisposable$1(target, dispose) {
|
|
1816
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
1817
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
1818
|
+
value: async () => {
|
|
1819
|
+
await dispose();
|
|
1820
|
+
},
|
|
1821
|
+
configurable: true,
|
|
1822
|
+
writable: true
|
|
1823
|
+
});
|
|
1824
|
+
return target;
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* `gen`, except that `return()` and `throw()` take effect at once.
|
|
1828
|
+
*
|
|
1829
|
+
* On an async generator both are queued behind a `next()` that is still
|
|
1830
|
+
* pending: a consumer that gives up on a stream while it waits for the peer's
|
|
1831
|
+
* next chunk — an aborted fetch whose response head has not come, say — cannot
|
|
1832
|
+
* end it until that chunk arrives, which for a peer that never answers is
|
|
1833
|
+
* never. The stream's `finally` (which closes its port and tells the peer)
|
|
1834
|
+
* then never runs, and on a multiplexer the port is kept for good.
|
|
1835
|
+
*
|
|
1836
|
+
* So `abandon` runs first, synchronously: it must make the pending `next()`
|
|
1837
|
+
* settle (abort the stream), which then resolves done, after which the
|
|
1838
|
+
* generator's own `return()` / `throw()` proceeds as usual. On a generator
|
|
1839
|
+
* suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
|
|
1840
|
+
*/
|
|
1841
|
+
function returningAtOnce(gen, abandon) {
|
|
1842
|
+
let abandoned = false;
|
|
1843
|
+
const giveUp = (reason) => {
|
|
1844
|
+
if (abandoned) return;
|
|
1845
|
+
abandoned = true;
|
|
1846
|
+
abandon(reason);
|
|
1847
|
+
};
|
|
1848
|
+
const wrapped = {
|
|
1849
|
+
next: (...args) => gen.next(...args).catch((error) => {
|
|
1850
|
+
if (abandoned) return {
|
|
1851
|
+
done: true,
|
|
1852
|
+
value: void 0
|
|
1853
|
+
};
|
|
1854
|
+
throw error;
|
|
1855
|
+
}),
|
|
1856
|
+
return(value) {
|
|
1857
|
+
giveUp();
|
|
1858
|
+
return gen.return(value);
|
|
1859
|
+
},
|
|
1860
|
+
throw(error) {
|
|
1861
|
+
giveUp(error);
|
|
1862
|
+
return gen.throw(error);
|
|
1863
|
+
},
|
|
1864
|
+
[Symbol.asyncIterator]() {
|
|
1865
|
+
return wrapped;
|
|
1866
|
+
}
|
|
1867
|
+
};
|
|
1868
|
+
return asyncDisposable$1(wrapped, () => wrapped.return(void 0));
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* The `type` of the out-of-band notice a side posts when it abandons a stream.
|
|
1872
|
+
*
|
|
1873
|
+
* Layer 1's `close` is not observable to layer 2 — a closed virtual port drops
|
|
1874
|
+
* its listeners silently and is indistinguishable from a working port nobody
|
|
1875
|
+
* is answering — so the peer would otherwise wait forever. Exported because
|
|
1876
|
+
* tests and adapters assert on it.
|
|
1877
|
+
*/
|
|
1878
|
+
const STREAM_ABORT = "webrun-rpc:stream-abort";
|
|
1879
|
+
/** Caller's input travels on this channel; the handler listens on it. */
|
|
1880
|
+
const CHANNEL_IN = "in";
|
|
1881
|
+
/** The handler's output travels on this channel; the caller listens on it. */
|
|
1882
|
+
const CHANNEL_OUT = "out";
|
|
1883
|
+
/**
|
|
1884
|
+
* One port in, one `Duplex` out (spec D9).
|
|
1885
|
+
*
|
|
1886
|
+
* The returned `Duplex` runs a single stream on `port`: the caller's `input`
|
|
1887
|
+
* is sent chunk by chunk with `callPort`, and the handler's output arrives the
|
|
1888
|
+
* same way on the other channel. Within each direction the next chunk is never
|
|
1889
|
+
* sent until the previous one has been delivered *and* pulled past by the
|
|
1890
|
+
* consumer (spec D11) — the reply to a chunk call *is* the confirmation, and
|
|
1891
|
+
* `listenPort` withholds it until then.
|
|
1892
|
+
*
|
|
1893
|
+
* A stream port carries exactly one invocation. To make several calls, open
|
|
1894
|
+
* several ports: `mux.openPort({ kind: "stream" })` per call.
|
|
1895
|
+
*/
|
|
1896
|
+
function duplexOverPort(port, options = {}) {
|
|
1897
|
+
return (input) => runCallerSide(port, input, options);
|
|
1898
|
+
}
|
|
1899
|
+
function runCallerSide(port, input, options) {
|
|
1900
|
+
const controller = new AbortController();
|
|
1901
|
+
const notice = installAbortNotice(port, controller);
|
|
1902
|
+
abortOnPortClose(port, controller);
|
|
1903
|
+
const clock = installStreamTimeout(controller, options.timeout);
|
|
1904
|
+
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
1905
|
+
const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
|
|
1906
|
+
pump.catch(() => {});
|
|
1907
|
+
return returningAtOnce((async function* () {
|
|
1908
|
+
try {
|
|
1909
|
+
yield* inbound.stream;
|
|
1910
|
+
} finally {
|
|
1911
|
+
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
1912
|
+
notice.post();
|
|
1913
|
+
notice.stop();
|
|
1914
|
+
clock.stop();
|
|
1915
|
+
inbound.stop();
|
|
1916
|
+
await pump.catch(() => {});
|
|
1917
|
+
try {
|
|
1918
|
+
await port.close?.();
|
|
1919
|
+
} catch {}
|
|
1920
|
+
}
|
|
1921
|
+
})(), () => {
|
|
1922
|
+
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Listens for the peer's abort notice, and can post our own. The notice is a
|
|
1927
|
+
* plain message with no `channelName` and a `type` that is not `"request"`,
|
|
1928
|
+
* so neither `callPort` nor `listenPort` reacts to it, and it carries no
|
|
1929
|
+
* numeric `id`, so `structuredCodec` never mistakes it for a layer 1 envelope.
|
|
1930
|
+
*/
|
|
1931
|
+
function installAbortNotice(port, controller) {
|
|
1932
|
+
const onMessage = (event) => {
|
|
1933
|
+
const data = event.data;
|
|
1934
|
+
if (!data || data.type !== "webrun-rpc:stream-abort") return;
|
|
1935
|
+
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the peer abandoned the stream"));
|
|
1936
|
+
};
|
|
1937
|
+
port.addEventListener("message", onMessage);
|
|
1938
|
+
let posted = false;
|
|
1939
|
+
return {
|
|
1940
|
+
post() {
|
|
1941
|
+
if (posted) return;
|
|
1942
|
+
posted = true;
|
|
1943
|
+
try {
|
|
1944
|
+
port.postMessage({ type: STREAM_ABORT });
|
|
1945
|
+
} catch {}
|
|
1946
|
+
},
|
|
1947
|
+
stop() {
|
|
1948
|
+
port.removeEventListener("message", onMessage);
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
/**
|
|
1953
|
+
* Aborts the stream when its port closes underneath it — the multiplexer was
|
|
1954
|
+
* closed, or the peer closed the port — for ports that report it (see
|
|
1955
|
+
* `getPortCloseSignal`). Without this a stream waiting for the peer's next
|
|
1956
|
+
* chunk would wait forever on a port that can no longer deliver one: the
|
|
1957
|
+
* outbound half's `callPort` already fails on the close signal, but nothing
|
|
1958
|
+
* failed the inbound half.
|
|
1959
|
+
*/
|
|
1960
|
+
function abortOnPortClose(port, controller) {
|
|
1961
|
+
const closed = getPortCloseSignal(port);
|
|
1962
|
+
if (!closed) return;
|
|
1963
|
+
const onClose = () => {
|
|
1964
|
+
if (!controller.signal.aborted) controller.abort(closed.reason);
|
|
1965
|
+
};
|
|
1966
|
+
if (closed.aborted) {
|
|
1967
|
+
onClose();
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
closed.addEventListener("abort", onClose, { once: true });
|
|
1971
|
+
controller.signal.addEventListener("abort", () => closed.removeEventListener("abort", onClose), { once: true });
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* The per-stream inactivity timeout (spec D8). Reset by any chunk in either
|
|
1975
|
+
* direction; elapsing aborts the stream. Unset, zero or non-finite installs no
|
|
1976
|
+
* timer at all, which is the default: a slow consumer is throttled, not failed.
|
|
1977
|
+
*/
|
|
1978
|
+
function installStreamTimeout(controller, timeout) {
|
|
1979
|
+
if (timeout === void 0 || !Number.isFinite(timeout) || timeout <= 0) return {
|
|
1980
|
+
touch() {},
|
|
1981
|
+
stop() {}
|
|
1982
|
+
};
|
|
1983
|
+
let timer;
|
|
1984
|
+
let stopped = false;
|
|
1985
|
+
const arm = () => {
|
|
1986
|
+
timer = setTimeout(() => {
|
|
1987
|
+
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error(`webrun-rpc: stream idle for ${timeout} ms`));
|
|
1988
|
+
}, timeout);
|
|
1989
|
+
};
|
|
1990
|
+
arm();
|
|
1991
|
+
return {
|
|
1992
|
+
touch() {
|
|
1993
|
+
if (stopped) return;
|
|
1994
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1995
|
+
arm();
|
|
1996
|
+
},
|
|
1997
|
+
stop() {
|
|
1998
|
+
stopped = true;
|
|
1999
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2000
|
+
timer = void 0;
|
|
2001
|
+
}
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* The receiving half of one direction.
|
|
2006
|
+
*
|
|
2007
|
+
* The `listenPort` listener is installed **eagerly**, not lazily inside
|
|
2008
|
+
* `recieveIterator`'s installer, because a handler that never drains its input
|
|
2009
|
+
* would otherwise leave the peer's chunk calls with nobody to answer them. If
|
|
2010
|
+
* the local consumer has not started iterating, an inbound chunk waits for it —
|
|
2011
|
+
* which is the correct backpressure, and different from having no listener.
|
|
2012
|
+
*/
|
|
2013
|
+
function receiveChunks(port, channelName, controller, touch) {
|
|
2014
|
+
let deliver;
|
|
2015
|
+
const waiting = [];
|
|
2016
|
+
let finished = false;
|
|
2017
|
+
let outstanding = false;
|
|
2018
|
+
let poison;
|
|
2019
|
+
let resolveEnded = () => {};
|
|
2020
|
+
const ended = new Promise((resolve) => {
|
|
2021
|
+
resolveEnded = resolve;
|
|
2022
|
+
});
|
|
2023
|
+
let aborted = false;
|
|
2024
|
+
let abortReason;
|
|
2025
|
+
const ready = () => deliver || finished ? Promise.resolve() : new Promise((r) => waiting.push(r));
|
|
2026
|
+
const wake = () => {
|
|
2027
|
+
for (const r of waiting.splice(0)) r();
|
|
2028
|
+
};
|
|
2029
|
+
const off = listenPort(port, async ({ done, value, error }) => {
|
|
2030
|
+
touch();
|
|
2031
|
+
if (poison) throw poison;
|
|
2032
|
+
if (outstanding) {
|
|
2033
|
+
poison = /* @__PURE__ */ new Error("webrun-rpc: peer sent a second unconfirmed chunk; the stream port is closed");
|
|
2034
|
+
controller.abort(poison);
|
|
2035
|
+
setTimeout(() => {
|
|
2036
|
+
try {
|
|
2037
|
+
port.close?.();
|
|
2038
|
+
} catch {}
|
|
2039
|
+
}, 0);
|
|
2040
|
+
throw poison;
|
|
2041
|
+
}
|
|
2042
|
+
outstanding = true;
|
|
2043
|
+
try {
|
|
2044
|
+
await ready();
|
|
2045
|
+
if (finished) throw poison ?? /* @__PURE__ */ new Error("webrun-rpc: the stream is closed");
|
|
2046
|
+
await deliver?.({
|
|
2047
|
+
done,
|
|
2048
|
+
value,
|
|
2049
|
+
error: error ? deserializeError(error) : void 0
|
|
2050
|
+
});
|
|
2051
|
+
} finally {
|
|
2052
|
+
outstanding = false;
|
|
2053
|
+
}
|
|
2054
|
+
if (done) resolveEnded();
|
|
2055
|
+
}, { channelName });
|
|
2056
|
+
const detach = () => {
|
|
2057
|
+
finished = true;
|
|
2058
|
+
wake();
|
|
2059
|
+
off();
|
|
2060
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
2061
|
+
resolveEnded();
|
|
2062
|
+
};
|
|
2063
|
+
const onAbort = () => {
|
|
2064
|
+
aborted = true;
|
|
2065
|
+
abortReason = controller.signal.reason;
|
|
2066
|
+
finished = true;
|
|
2067
|
+
wake();
|
|
2068
|
+
deliver?.({
|
|
2069
|
+
done: true,
|
|
2070
|
+
error: abortReason
|
|
2071
|
+
});
|
|
2072
|
+
resolveEnded();
|
|
2073
|
+
};
|
|
2074
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
2075
|
+
return {
|
|
2076
|
+
stream: recieveIterator((d) => {
|
|
2077
|
+
if (aborted) {
|
|
2078
|
+
d({
|
|
2079
|
+
done: true,
|
|
2080
|
+
error: abortReason
|
|
2081
|
+
});
|
|
2082
|
+
return detach;
|
|
2083
|
+
}
|
|
2084
|
+
deliver = d;
|
|
2085
|
+
wake();
|
|
2086
|
+
return detach;
|
|
2087
|
+
}),
|
|
2088
|
+
stop: detach,
|
|
2089
|
+
ended
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
/**
|
|
2093
|
+
* The sending half of one direction: one `callPort` per chunk, one call
|
|
2094
|
+
* outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8).
|
|
2095
|
+
*/
|
|
2096
|
+
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
|
|
2097
|
+
const stream = throughAbort(maxMessageSize ? toChunks(maxMessageSize)(output) : output, signal);
|
|
2098
|
+
try {
|
|
2099
|
+
await sendIterator(async ({ done, value, error }) => {
|
|
2100
|
+
if (signal.aborted) return;
|
|
2101
|
+
const chunk = {
|
|
2102
|
+
done,
|
|
2103
|
+
value,
|
|
2104
|
+
error: error === void 0 ? void 0 : serializeError(error)
|
|
2105
|
+
};
|
|
2106
|
+
log?.("[duplexOverPort] send", {
|
|
2107
|
+
channelName,
|
|
2108
|
+
done,
|
|
2109
|
+
size: value?.byteLength
|
|
2110
|
+
});
|
|
2111
|
+
await callPort(port, chunk, {
|
|
2112
|
+
channelName,
|
|
2113
|
+
timeout: NO_TIMEOUT,
|
|
2114
|
+
signal
|
|
2115
|
+
});
|
|
2116
|
+
touch();
|
|
2117
|
+
}, stream);
|
|
2118
|
+
} catch (err) {
|
|
2119
|
+
if (signal.aborted) return;
|
|
2120
|
+
throw err;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* One virtual port.
|
|
2125
|
+
*
|
|
2126
|
+
* `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
|
|
2127
|
+
* only a `MessageTarget`, so it cannot forge inbound traffic or close the port
|
|
2128
|
+
* out from under the multiplexer's bookkeeping.
|
|
2129
|
+
*/
|
|
2130
|
+
function newVirtualPort(send, requestClose) {
|
|
2131
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2132
|
+
let closed = false;
|
|
2133
|
+
const closing = new AbortController();
|
|
2134
|
+
const abort = (reason) => {
|
|
2135
|
+
if (closing.signal.aborted) return;
|
|
2136
|
+
closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
|
|
2137
|
+
};
|
|
2138
|
+
const port = {
|
|
2139
|
+
addEventListener(_type, listener) {
|
|
2140
|
+
listeners.add(listener);
|
|
2141
|
+
},
|
|
2142
|
+
removeEventListener(_type, listener) {
|
|
2143
|
+
listeners.delete(listener);
|
|
2144
|
+
},
|
|
2145
|
+
postMessage(message, transfer) {
|
|
2146
|
+
if (closed) return;
|
|
2147
|
+
send(message, transfer);
|
|
2148
|
+
},
|
|
2149
|
+
close() {
|
|
2150
|
+
if (closed) return;
|
|
2151
|
+
closed = true;
|
|
2152
|
+
const notify = requestClose;
|
|
2153
|
+
listeners.clear();
|
|
2154
|
+
notify();
|
|
2155
|
+
abort();
|
|
2156
|
+
}
|
|
2157
|
+
};
|
|
2158
|
+
setPortCloseSignal(port, closing.signal);
|
|
2159
|
+
return {
|
|
2160
|
+
port,
|
|
2161
|
+
deliver(payload) {
|
|
2162
|
+
if (closed) return;
|
|
2163
|
+
const event = new MessageEvent("message", { data: payload });
|
|
2164
|
+
for (const listener of [...listeners]) try {
|
|
2165
|
+
listener(event);
|
|
2166
|
+
} catch {}
|
|
2167
|
+
},
|
|
2168
|
+
markClosed(reason) {
|
|
2169
|
+
closed = true;
|
|
2170
|
+
listeners.clear();
|
|
2171
|
+
abort(reason);
|
|
2172
|
+
},
|
|
2173
|
+
isClosed() {
|
|
2174
|
+
return closed;
|
|
2175
|
+
}
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
/**
|
|
2179
|
+
* The message of the error that fails the calls still in flight on a
|
|
2180
|
+
* multiplexer when it is closed (`PortMux.close()`).
|
|
2181
|
+
*/
|
|
2182
|
+
const MUX_CLOSED_MESSAGE = "webrun-rpc: the multiplexer was closed";
|
|
2183
|
+
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
2184
|
+
const DEFAULT_MAX_PORTS = 1024;
|
|
2185
|
+
/**
|
|
2186
|
+
* The default `PortMux`: emulates multiplexing over a single port.
|
|
2187
|
+
*
|
|
2188
|
+
* A transport that already multiplexes natively supplies its own `PortMux`
|
|
2189
|
+
* instead — this implementation is for transports that offer one pipe.
|
|
2190
|
+
*/
|
|
2191
|
+
function multiplexPort(port, options) {
|
|
2192
|
+
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
|
|
2193
|
+
const open = /* @__PURE__ */ new Map();
|
|
2194
|
+
let nextId = side === "initiator" ? 0 : 1;
|
|
2195
|
+
let muxClosed = false;
|
|
2196
|
+
const post = (envelope, transfer) => {
|
|
2197
|
+
if (muxClosed) return;
|
|
2198
|
+
try {
|
|
2199
|
+
codec.post(port, envelope, transfer);
|
|
2200
|
+
} catch {}
|
|
2201
|
+
};
|
|
2202
|
+
const attach = (id) => {
|
|
2203
|
+
let self;
|
|
2204
|
+
self = newVirtualPort((payload, transfer) => post({
|
|
2205
|
+
type: "message",
|
|
2206
|
+
id,
|
|
2207
|
+
payload
|
|
2208
|
+
}, transfer), (reason) => {
|
|
2209
|
+
post({
|
|
2210
|
+
type: "close",
|
|
2211
|
+
id,
|
|
2212
|
+
reason
|
|
2213
|
+
});
|
|
2214
|
+
open.delete(id);
|
|
2215
|
+
self.markClosed();
|
|
2216
|
+
});
|
|
2217
|
+
open.set(id, self);
|
|
2218
|
+
return self;
|
|
2219
|
+
};
|
|
2220
|
+
const handleEnvelope = (envelope) => {
|
|
2221
|
+
if (muxClosed) return;
|
|
2222
|
+
if (envelope.type === "open") {
|
|
2223
|
+
if (open.has(envelope.id)) return;
|
|
2224
|
+
if (open.size >= maxPorts) {
|
|
2225
|
+
post({
|
|
2226
|
+
type: "close",
|
|
2227
|
+
id: envelope.id,
|
|
2228
|
+
reason: "max-ports"
|
|
2229
|
+
});
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
const handle = attach(envelope.id);
|
|
2233
|
+
let accepted = false;
|
|
2234
|
+
if (onPort) try {
|
|
2235
|
+
accepted = onPort(handle.port, envelope.meta) !== false;
|
|
2236
|
+
} catch {
|
|
2237
|
+
accepted = false;
|
|
2238
|
+
}
|
|
2239
|
+
if (!accepted) {
|
|
2240
|
+
open.delete(envelope.id);
|
|
2241
|
+
handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was rejected"));
|
|
2242
|
+
post({
|
|
2243
|
+
type: "close",
|
|
2244
|
+
id: envelope.id,
|
|
2245
|
+
reason: "rejected"
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
const handle = open.get(envelope.id);
|
|
2251
|
+
if (envelope.type === "message") {
|
|
2252
|
+
if (!handle) return;
|
|
2253
|
+
handle.deliver(envelope.payload);
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
if (!handle) return;
|
|
2257
|
+
open.delete(envelope.id);
|
|
2258
|
+
handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
|
|
2259
|
+
};
|
|
2260
|
+
const listener = (event) => {
|
|
2261
|
+
const envelope = codec.read(event);
|
|
2262
|
+
if (envelope) handleEnvelope(envelope);
|
|
2263
|
+
};
|
|
2264
|
+
port.addEventListener("message", listener);
|
|
2265
|
+
port.start?.();
|
|
2266
|
+
return {
|
|
2267
|
+
maxMessageSize,
|
|
2268
|
+
async openPort(meta) {
|
|
2269
|
+
if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
|
|
2270
|
+
if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached`);
|
|
2271
|
+
while (open.has(nextId)) nextId += 2;
|
|
2272
|
+
const id = nextId;
|
|
2273
|
+
nextId += 2;
|
|
2274
|
+
const handle = attach(id);
|
|
2275
|
+
post({
|
|
2276
|
+
type: "open",
|
|
2277
|
+
id,
|
|
2278
|
+
meta
|
|
2279
|
+
});
|
|
2280
|
+
return handle.port;
|
|
2281
|
+
},
|
|
2282
|
+
async close() {
|
|
2283
|
+
if (muxClosed) return;
|
|
2284
|
+
for (const [id, handle] of [...open]) {
|
|
2285
|
+
post({
|
|
2286
|
+
type: "close",
|
|
2287
|
+
id
|
|
2288
|
+
});
|
|
2289
|
+
open.delete(id);
|
|
2290
|
+
handle.markClosed(/* @__PURE__ */ new Error(MUX_CLOSED_MESSAGE));
|
|
2291
|
+
}
|
|
2292
|
+
muxClosed = true;
|
|
2293
|
+
port.removeEventListener("message", listener);
|
|
2294
|
+
await port.close?.();
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Announced with every port this pair opens. Layer 1 never reads `meta` — it is
|
|
2300
|
+
* here so a peer running something else over the same multiplexer can tell a
|
|
2301
|
+
* stream port from whatever else it hands out, and so a packet capture says
|
|
2302
|
+
* what the port is for.
|
|
2303
|
+
*/
|
|
2304
|
+
const STREAM_META = { kind: "stream" };
|
|
2305
|
+
/**
|
|
2306
|
+
* One port source in, one caller `Duplex` out.
|
|
2307
|
+
*
|
|
2308
|
+
* A call is a port: the mux allocates one, `duplexOverPort` runs the single
|
|
2309
|
+
* invocation on it, and closing the stream closes the port. No stream ids, no
|
|
2310
|
+
* framing and no credit accounting live here — the mux owns the first, and
|
|
2311
|
+
* `duplexOverPort` owns the one-chunk window that makes memory bounded.
|
|
2312
|
+
*/
|
|
2313
|
+
const connect = async ({ mux: factory, timeout }) => {
|
|
2314
|
+
const mux = await factory();
|
|
2315
|
+
const streamOptions = {
|
|
2316
|
+
maxMessageSize: mux.maxMessageSize,
|
|
2317
|
+
timeout
|
|
2318
|
+
};
|
|
2319
|
+
const call = (input) => {
|
|
2320
|
+
let stream;
|
|
2321
|
+
let abandoned = false;
|
|
2322
|
+
return returningAtOnce((async function* () {
|
|
2323
|
+
const port = await mux.openPort(STREAM_META);
|
|
2324
|
+
if (abandoned) {
|
|
2325
|
+
await port.close?.();
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
stream = duplexOverPort(port, streamOptions)(input);
|
|
2329
|
+
yield* stream;
|
|
2330
|
+
})(), () => {
|
|
2331
|
+
abandoned = true;
|
|
2332
|
+
stream?.return(void 0).catch(() => {});
|
|
2333
|
+
});
|
|
2334
|
+
};
|
|
2335
|
+
return {
|
|
2336
|
+
call,
|
|
2337
|
+
async close() {
|
|
2338
|
+
await mux.close();
|
|
2339
|
+
}
|
|
2340
|
+
};
|
|
2341
|
+
};
|
|
2342
|
+
/**
|
|
2343
|
+
* Ports over ONE PIPE OF BYTES — a `MessagePort`, a worker, a WebSocket.
|
|
2344
|
+
*
|
|
2345
|
+
* There is no second port to be had, so `multiplexPort`'s id table invents
|
|
2346
|
+
* them. Use this when the transport gives you exactly one channel.
|
|
2347
|
+
*/
|
|
2348
|
+
function overPipe(pipe, options) {
|
|
2349
|
+
return (onPort) => multiplexPort(pipe, {
|
|
2350
|
+
...options,
|
|
2351
|
+
onPort
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
function isEnvelope(value) {
|
|
2355
|
+
if (typeof value !== "object" || value === null) return false;
|
|
2356
|
+
const candidate = value;
|
|
2357
|
+
if (typeof candidate.id !== "number") return false;
|
|
2358
|
+
if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
|
|
2359
|
+
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
|
|
2360
|
+
}
|
|
2361
|
+
/**
|
|
2362
|
+
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
2363
|
+
* worker, an iframe.
|
|
2364
|
+
*
|
|
2365
|
+
* Envelopes are posted as-is, so nothing is encoded, `ArrayBuffer`s move
|
|
2366
|
+
* zero-copy through the transfer list, and structured clone does the work the
|
|
2367
|
+
* platform already does well. This is a performance choice only: layer 2 may
|
|
2368
|
+
* not send anything a byte codec could not also carry.
|
|
2369
|
+
*/
|
|
2370
|
+
const structuredCodec = {
|
|
2371
|
+
post(port, envelope, transfer) {
|
|
2372
|
+
if (transfer && transfer.length > 0) port.postMessage(envelope, transfer);
|
|
2373
|
+
else port.postMessage(envelope);
|
|
2374
|
+
},
|
|
2375
|
+
read(event) {
|
|
2376
|
+
return isEnvelope(event.data) ? event.data : void 0;
|
|
2377
|
+
}
|
|
2378
|
+
};
|
|
2379
|
+
//#endregion
|
|
2380
|
+
//#region src/link/async-dispose.ts
|
|
2381
|
+
/**
|
|
2382
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
2383
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
2384
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
2385
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
2386
|
+
* declared types, which would require consumers to compile with the
|
|
2387
|
+
* `esnext.disposable` lib.
|
|
2388
|
+
*/
|
|
2389
|
+
function asyncDisposable(target, dispose) {
|
|
2390
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
2391
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
2392
|
+
value: async () => {
|
|
2393
|
+
await dispose();
|
|
2394
|
+
},
|
|
2395
|
+
configurable: true,
|
|
2396
|
+
writable: true
|
|
2397
|
+
});
|
|
2398
|
+
return target;
|
|
2399
|
+
}
|
|
2400
|
+
//#endregion
|
|
2401
|
+
//#region src/link/http-link.ts
|
|
2402
|
+
/**
|
|
2403
|
+
* The version of the HTTP-over-`MessagePort` protocol this package speaks,
|
|
2404
|
+
* sent in every hand-shake that sets up a link. Version 1 was this package's
|
|
2405
|
+
* own `handleStreams` / `sendStream` (no backpressure); version 2 is
|
|
2406
|
+
* webrun-rpc's `connect` / `serve` over `overPipe` with webrun-http-streams'
|
|
2407
|
+
* `fetchOverDuplex` / `serveFetchOverDuplex`: one port per link, calls
|
|
2408
|
+
* multiplexed on it, each call a stream with a one-chunk window in both
|
|
2409
|
+
* directions, and cancellation that reaches the handler.
|
|
2410
|
+
*/
|
|
2411
|
+
const HTTP_LINK_PROTOCOL = "webrun-http-link/2";
|
|
2412
|
+
/**
|
|
2413
|
+
* Why a request failed when its link went away under it: the far side (the
|
|
2414
|
+
* handler, or the ServiceWorker) was lost. The request may or may not have
|
|
2415
|
+
* been applied.
|
|
2416
|
+
*/
|
|
2417
|
+
var LinkLostError = class extends Error {
|
|
2418
|
+
constructor(message = "The link was lost while this request was in flight") {
|
|
2419
|
+
super(`${message}; it may or may not have been applied.`);
|
|
2420
|
+
this.name = "LinkLostError";
|
|
2421
|
+
}
|
|
2422
|
+
};
|
|
2423
|
+
/**
|
|
2424
|
+
* Posted on a link's port by the calling end when it closes the link, so the
|
|
2425
|
+
* answering end can release it: a `MessagePort` never reports that its other
|
|
2426
|
+
* end is gone.
|
|
2427
|
+
*/
|
|
2428
|
+
const LINK_CLOSE = "webrun-http:link-close";
|
|
2429
|
+
/** Opens the calling end of a link on `port` (one port per link). */
|
|
2430
|
+
async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
|
|
2431
|
+
const { call, close } = await connect({
|
|
2432
|
+
mux: overPipe(port, {
|
|
2433
|
+
codec: structuredCodec,
|
|
2434
|
+
side: "initiator",
|
|
2435
|
+
maxPorts
|
|
2436
|
+
}),
|
|
2437
|
+
timeout: streamTimeout
|
|
2438
|
+
});
|
|
2439
|
+
return trackedLink((request) => fetchOverDuplex(call, request), async () => {
|
|
2440
|
+
try {
|
|
2441
|
+
port.postMessage({ type: LINK_CLOSE });
|
|
2442
|
+
} catch {}
|
|
2443
|
+
await close();
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
/**
|
|
2447
|
+
* Wraps a `send` so the link knows its requests in flight (for draining and
|
|
2448
|
+
* load-based routing) and can fail them all at once when it is closed.
|
|
2449
|
+
*/
|
|
2450
|
+
function trackedLink(send, release) {
|
|
2451
|
+
let inflight = 0;
|
|
2452
|
+
let closed = false;
|
|
2453
|
+
let lostReason;
|
|
2454
|
+
const failers = /* @__PURE__ */ new Set();
|
|
2455
|
+
const idleWaiters = [];
|
|
2456
|
+
const done = () => {
|
|
2457
|
+
inflight--;
|
|
2458
|
+
if (inflight === 0) for (const resolve of idleWaiters.splice(0)) resolve();
|
|
2459
|
+
};
|
|
2460
|
+
const link = {
|
|
2461
|
+
get inflight() {
|
|
2462
|
+
return inflight;
|
|
2463
|
+
},
|
|
2464
|
+
get closed() {
|
|
2465
|
+
return closed;
|
|
2466
|
+
},
|
|
2467
|
+
whenIdle() {
|
|
2468
|
+
return inflight === 0 ? Promise.resolve() : new Promise((r) => idleWaiters.push(r));
|
|
2469
|
+
},
|
|
2470
|
+
async send(request) {
|
|
2471
|
+
if (closed) throw lostReason ?? new LinkLostError("The link is closed");
|
|
2472
|
+
inflight++;
|
|
2473
|
+
let finished = false;
|
|
2474
|
+
const finish = () => {
|
|
2475
|
+
if (finished) return;
|
|
2476
|
+
finished = true;
|
|
2477
|
+
failers.delete(fail);
|
|
2478
|
+
done();
|
|
2479
|
+
};
|
|
2480
|
+
let fail = () => {};
|
|
2481
|
+
const lost = new Promise((_, reject) => {
|
|
2482
|
+
fail = reject;
|
|
2483
|
+
});
|
|
2484
|
+
failers.add(fail);
|
|
2485
|
+
lost.catch(() => {});
|
|
2486
|
+
let response;
|
|
2487
|
+
try {
|
|
2488
|
+
response = await Promise.race([send(request), lost]);
|
|
2489
|
+
} catch (error) {
|
|
2490
|
+
finish();
|
|
2491
|
+
throw lostReason ?? error;
|
|
2492
|
+
}
|
|
2493
|
+
if (!response.body) {
|
|
2494
|
+
finish();
|
|
2495
|
+
return response;
|
|
2496
|
+
}
|
|
2497
|
+
return new Response(guardBody(response.body, lost, () => lostReason, finish), {
|
|
2498
|
+
status: response.status,
|
|
2499
|
+
statusText: response.statusText,
|
|
2500
|
+
headers: response.headers
|
|
2501
|
+
});
|
|
2502
|
+
},
|
|
2503
|
+
async close(reason) {
|
|
2504
|
+
if (closed) return;
|
|
2505
|
+
closed = true;
|
|
2506
|
+
lostReason = reason ?? new LinkLostError("The link was closed");
|
|
2507
|
+
for (const fail of [...failers]) fail(lostReason);
|
|
2508
|
+
try {
|
|
2509
|
+
await release();
|
|
2510
|
+
} catch {}
|
|
2511
|
+
}
|
|
2512
|
+
};
|
|
2513
|
+
return asyncDisposable(link, () => link.close());
|
|
2514
|
+
}
|
|
2515
|
+
/**
|
|
2516
|
+
* A response body that reports its end (done, cancelled or failed) and fails
|
|
2517
|
+
* with the link's reason when the link is lost mid-stream.
|
|
2518
|
+
*/
|
|
2519
|
+
function guardBody(body, lost, reason, finish) {
|
|
2520
|
+
const reader = body.getReader();
|
|
2521
|
+
return new ReadableStream({
|
|
2522
|
+
async pull(controller) {
|
|
2523
|
+
try {
|
|
2524
|
+
const { done, value } = await Promise.race([reader.read(), lost]);
|
|
2525
|
+
if (done) {
|
|
2526
|
+
finish();
|
|
2527
|
+
controller.close();
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
controller.enqueue(value);
|
|
2531
|
+
} catch (error) {
|
|
2532
|
+
finish();
|
|
2533
|
+
reader.cancel().catch(() => {});
|
|
2534
|
+
controller.error(reason() ?? error);
|
|
2535
|
+
}
|
|
2536
|
+
},
|
|
2537
|
+
async cancel(why) {
|
|
2538
|
+
finish();
|
|
2539
|
+
await reader.cancel(why).catch(() => {});
|
|
2540
|
+
}
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
//#endregion
|
|
2544
|
+
//#region src/sw/protocol.ts
|
|
2545
|
+
/** Page → ServiceWorker: open a WebSocket to a handler. */
|
|
2546
|
+
const WS_CONNECT_CALL = "webrun-http:ws-connect";
|
|
2547
|
+
//#endregion
|
|
1337
2548
|
//#region src/core/data-channels.ts
|
|
1338
2549
|
const MESSAGE_TYPE_REQUEST = "REQUEST";
|
|
1339
2550
|
const MESSAGE_TYPE_RESPONSE = "RESPONSE";
|
|
@@ -1532,51 +2743,6 @@ async function sendHttpRequest(communicationPort, request) {
|
|
|
1532
2743
|
})(request);
|
|
1533
2744
|
}
|
|
1534
2745
|
//#endregion
|
|
1535
|
-
//#region src/relay/mount-table.ts
|
|
1536
|
-
/** `/` stays `/`; `/peers` and `/peers/` both become `/peers/`. */
|
|
1537
|
-
function normalise(path) {
|
|
1538
|
-
if (path === "" || path === "/") return "/";
|
|
1539
|
-
const withSlash = path.startsWith("/") ? path : `/${path}`;
|
|
1540
|
-
return withSlash.endsWith("/") ? withSlash : `${withSlash}/`;
|
|
1541
|
-
}
|
|
1542
|
-
/** Does `prefix` own `pathname`? `/peers/` owns `/peers/`, `/peers` and `/peers/x`. */
|
|
1543
|
-
function owns(prefix, pathname) {
|
|
1544
|
-
if (prefix === "/") return true;
|
|
1545
|
-
if (pathname.startsWith(prefix)) return true;
|
|
1546
|
-
return `${pathname}/` === prefix;
|
|
1547
|
-
}
|
|
1548
|
-
function newMountTable(options = {}) {
|
|
1549
|
-
const entries = /* @__PURE__ */ new Map();
|
|
1550
|
-
let seq = 0;
|
|
1551
|
-
return {
|
|
1552
|
-
set(key, spec) {
|
|
1553
|
-
entries.set(key, {
|
|
1554
|
-
key,
|
|
1555
|
-
prefix: spec.path == null ? "" : normalise(spec.path),
|
|
1556
|
-
match: spec.match,
|
|
1557
|
-
seq: seq++
|
|
1558
|
-
});
|
|
1559
|
-
},
|
|
1560
|
-
remove(key) {
|
|
1561
|
-
entries.delete(key);
|
|
1562
|
-
},
|
|
1563
|
-
excludes(url) {
|
|
1564
|
-
return options.exclude?.(url) === true;
|
|
1565
|
-
},
|
|
1566
|
-
find(url) {
|
|
1567
|
-
if (options.exclude?.(url) === true) return void 0;
|
|
1568
|
-
let best;
|
|
1569
|
-
for (const entry of entries.values()) {
|
|
1570
|
-
if (entry.prefix === "" || !owns(entry.prefix, url.pathname)) continue;
|
|
1571
|
-
if (best == null || entry.prefix.length > best.prefix.length) best = entry;
|
|
1572
|
-
}
|
|
1573
|
-
if (best != null) return best.key;
|
|
1574
|
-
const predicates = [...entries.values()].filter((entry) => entry.match != null).sort((a, b) => a.seq - b.seq);
|
|
1575
|
-
for (const entry of predicates) if (entry.match?.(url) === true) return entry.key;
|
|
1576
|
-
}
|
|
1577
|
-
};
|
|
1578
|
-
}
|
|
1579
|
-
//#endregion
|
|
1580
2746
|
//#region src/relay/split-service-url.ts
|
|
1581
2747
|
/**
|
|
1582
2748
|
* Splits a URL of the form `<base>/<separator><key>/<path>` into parts.
|
|
@@ -1635,6 +2801,70 @@ function splitServiceUrl(url, separator = "~") {
|
|
|
1635
2801
|
};
|
|
1636
2802
|
}
|
|
1637
2803
|
//#endregion
|
|
2804
|
+
//#region src/relay/index.ts
|
|
2805
|
+
/**
|
|
2806
|
+
* Sends `request` on a port a service accepted with `reply`: over an HTTP link
|
|
2807
|
+
* when the service speaks `HTTP_LINK_PROTOCOL`, over version 1's transport
|
|
2808
|
+
* when it answered `true`. The link is closed once the response is done.
|
|
2809
|
+
*/
|
|
2810
|
+
async function sendOverConnection(port, reply, request, options = {}) {
|
|
2811
|
+
if (reply === true) return await sendHttpRequest(port, request);
|
|
2812
|
+
const link = await connectHttpLink(port, options);
|
|
2813
|
+
try {
|
|
2814
|
+
const response = await link.send(request);
|
|
2815
|
+
link.whenIdle().then(() => link.close());
|
|
2816
|
+
return response;
|
|
2817
|
+
} catch (error) {
|
|
2818
|
+
await link.close();
|
|
2819
|
+
throw error;
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
//#endregion
|
|
2823
|
+
//#region src/relay/mount-table.ts
|
|
2824
|
+
/** `/` stays `/`; `/peers` and `/peers/` both become `/peers/`. */
|
|
2825
|
+
function normalise(path) {
|
|
2826
|
+
if (path === "" || path === "/") return "/";
|
|
2827
|
+
const withSlash = path.startsWith("/") ? path : `/${path}`;
|
|
2828
|
+
return withSlash.endsWith("/") ? withSlash : `${withSlash}/`;
|
|
2829
|
+
}
|
|
2830
|
+
/** Does `prefix` own `pathname`? `/peers/` owns `/peers/`, `/peers` and `/peers/x`. */
|
|
2831
|
+
function owns(prefix, pathname) {
|
|
2832
|
+
if (prefix === "/") return true;
|
|
2833
|
+
if (pathname.startsWith(prefix)) return true;
|
|
2834
|
+
return `${pathname}/` === prefix;
|
|
2835
|
+
}
|
|
2836
|
+
function newMountTable(options = {}) {
|
|
2837
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2838
|
+
let seq = 0;
|
|
2839
|
+
return {
|
|
2840
|
+
set(key, spec) {
|
|
2841
|
+
entries.set(key, {
|
|
2842
|
+
key,
|
|
2843
|
+
prefix: spec.path == null ? "" : normalise(spec.path),
|
|
2844
|
+
match: spec.match,
|
|
2845
|
+
seq: seq++
|
|
2846
|
+
});
|
|
2847
|
+
},
|
|
2848
|
+
remove(key) {
|
|
2849
|
+
entries.delete(key);
|
|
2850
|
+
},
|
|
2851
|
+
excludes(url) {
|
|
2852
|
+
return options.exclude?.(url) === true;
|
|
2853
|
+
},
|
|
2854
|
+
find(url) {
|
|
2855
|
+
if (options.exclude?.(url) === true) return void 0;
|
|
2856
|
+
let best;
|
|
2857
|
+
for (const entry of entries.values()) {
|
|
2858
|
+
if (entry.prefix === "" || !owns(entry.prefix, url.pathname)) continue;
|
|
2859
|
+
if (best == null || entry.prefix.length > best.prefix.length) best = entry;
|
|
2860
|
+
}
|
|
2861
|
+
if (best != null) return best.key;
|
|
2862
|
+
const predicates = [...entries.values()].filter((entry) => entry.match != null).sort((a, b) => a.seq - b.seq);
|
|
2863
|
+
for (const entry of predicates) if (entry.match?.(url) === true) return entry.key;
|
|
2864
|
+
}
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
//#endregion
|
|
1638
2868
|
//#region src/relay/index-sw.ts
|
|
1639
2869
|
/**
|
|
1640
2870
|
* One stored registry entry, whatever shape it is on disk.
|
|
@@ -1761,7 +2991,7 @@ function startRelayServiceWorker(self, options = {}) {
|
|
|
1761
2991
|
const source = event.source;
|
|
1762
2992
|
if (!source) return false;
|
|
1763
2993
|
const { key, path } = data;
|
|
1764
|
-
if (options.canRegister != null && !await options.canRegister(source, key)) throw new Error(`this client may not register "${key}"`);
|
|
2994
|
+
if (options.canRegister != null && !await options.canRegister(source, key, path)) throw new Error(path == null ? `this client may not register "${key}"` : `this client may not register "${key}" at "${path}"`);
|
|
1765
2995
|
if (takeover === "first-wins") {
|
|
1766
2996
|
const current = await clientsRegistry.getMount(key);
|
|
1767
2997
|
const isCurrentLive = current != null && await clientsRegistry.getClient(key) != null;
|
|
@@ -1776,8 +3006,14 @@ function startRelayServiceWorker(self, options = {}) {
|
|
|
1776
3006
|
applyRegisteredMount(mounts, key, path, hostKeys);
|
|
1777
3007
|
return added;
|
|
1778
3008
|
}));
|
|
1779
|
-
register(handleChannelCalls(self, "UNREGISTER", async (
|
|
3009
|
+
register(handleChannelCalls(self, "UNREGISTER", async (event, data) => {
|
|
3010
|
+
const source = event.source;
|
|
3011
|
+
if (!source) return false;
|
|
1780
3012
|
const { key } = data;
|
|
3013
|
+
const current = await clientsRegistry.getMount(key);
|
|
3014
|
+
if (current == null) return false;
|
|
3015
|
+
if (await clientsRegistry.getClient(key) != null && current.clientId !== source.id) throw new Error(`"${key}" is served by another client`);
|
|
3016
|
+
if (options.canRegister != null && !await options.canRegister(source, key, current.path)) throw new Error(`this client may not unregister "${key}"`);
|
|
1781
3017
|
removeRegisteredMount(mounts, key, hostKeys);
|
|
1782
3018
|
return await clientsRegistry.removeClient(key);
|
|
1783
3019
|
}));
|
|
@@ -1785,7 +3021,34 @@ function startRelayServiceWorker(self, options = {}) {
|
|
|
1785
3021
|
const { key } = data;
|
|
1786
3022
|
const client = await clientsRegistry.getClient(key);
|
|
1787
3023
|
if (!client) throw new Error(`Target client was not found. Target key: "${key}".`);
|
|
1788
|
-
|
|
3024
|
+
const reply = await callChannel(client, "CONNECT", data, port);
|
|
3025
|
+
const socketPort = reply?.port;
|
|
3026
|
+
return socketPort ? withTransfer(reply, [socketPort]) : reply;
|
|
3027
|
+
}));
|
|
3028
|
+
register(handleChannelCalls(self, WS_CONNECT_CALL, async (event, data) => {
|
|
3029
|
+
const source = event.source;
|
|
3030
|
+
if (!source?.url || new URL(source.url).origin !== self.location.origin) throw new Error("Only this origin's pages may open sockets");
|
|
3031
|
+
const { url, protocols = [] } = data;
|
|
3032
|
+
const httpUrl = new URL(url);
|
|
3033
|
+
if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
|
|
3034
|
+
else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
|
|
3035
|
+
await restored.catch(() => {});
|
|
3036
|
+
const key = resolveServiceKey(httpUrl, mounts, self.location.origin);
|
|
3037
|
+
if (key == null) throw new Error(`No service serves ${url}`);
|
|
3038
|
+
const client = await clientsRegistry.getClient(key);
|
|
3039
|
+
if (!client) throw new Error(`No service serves ${url}`);
|
|
3040
|
+
const channel = new MessageChannel();
|
|
3041
|
+
try {
|
|
3042
|
+
const connection = await callChannel(client, "CONNECT", {
|
|
3043
|
+
type: "ws",
|
|
3044
|
+
key,
|
|
3045
|
+
url: `${httpUrl}`,
|
|
3046
|
+
protocols
|
|
3047
|
+
}, channel.port2);
|
|
3048
|
+
return withTransfer(connection, [connection.port]);
|
|
3049
|
+
} finally {
|
|
3050
|
+
channel.port1.close();
|
|
3051
|
+
}
|
|
1789
3052
|
}));
|
|
1790
3053
|
/** Answers `request` as the service registered under `key`. */
|
|
1791
3054
|
async function serve(key, request, url) {
|
|
@@ -1794,11 +3057,13 @@ function startRelayServiceWorker(self, options = {}) {
|
|
|
1794
3057
|
const channel = new MessageChannel();
|
|
1795
3058
|
const client = await clientsRegistry.getClient(key);
|
|
1796
3059
|
if (!client) throw HttpError.errorResourceGone(params);
|
|
1797
|
-
|
|
3060
|
+
const reply = await callChannel(client, "CONNECT", {
|
|
1798
3061
|
type: "http",
|
|
1799
|
-
key
|
|
1800
|
-
|
|
1801
|
-
|
|
3062
|
+
key,
|
|
3063
|
+
wire: HTTP_LINK_PROTOCOL
|
|
3064
|
+
}, channel.port2);
|
|
3065
|
+
if (!reply) throw HttpError.errorForbidden(params);
|
|
3066
|
+
const response = await sendOverConnection(channel.port1, reply, request, { streamTimeout: options.streamTimeout });
|
|
1802
3067
|
return options.decorateResponse?.(response, request) ?? response;
|
|
1803
3068
|
} catch (error) {
|
|
1804
3069
|
const httpError = HttpError.fromError(error);
|