@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/relay-sw.js
CHANGED
|
@@ -23,8 +23,6 @@
|
|
|
23
23
|
const payload = typeof error === "string" ? { message: error } : error;
|
|
24
24
|
return Object.assign(new Error(payload.message), payload);
|
|
25
25
|
}
|
|
26
|
-
new TextEncoder();
|
|
27
|
-
new TextDecoder();
|
|
28
26
|
/**
|
|
29
27
|
* The newAsyncGenerator function creates async generators from callback-based initialization
|
|
30
28
|
* functions, providing a bridge between imperative event handling and declarative async
|
|
@@ -177,7 +175,6 @@
|
|
|
177
175
|
drainQueue();
|
|
178
176
|
}
|
|
179
177
|
}
|
|
180
|
-
new TextEncoder();
|
|
181
178
|
/**
|
|
182
179
|
* The iterator ↔ `ReadableStream` boundary.
|
|
183
180
|
*
|
|
@@ -447,8 +444,8 @@
|
|
|
447
444
|
const NEWLINE = 10;
|
|
448
445
|
/** Mirrors the HTTP/1.1 codec's default `maxHeaderBytes`. */
|
|
449
446
|
const MAX_ENVELOPE_BYTES = 65536;
|
|
450
|
-
const encoder$1 = new TextEncoder();
|
|
451
|
-
const decoder = new TextDecoder();
|
|
447
|
+
const encoder$1 = /* @__PURE__ */ new TextEncoder();
|
|
448
|
+
const decoder = /* @__PURE__ */ new TextDecoder();
|
|
452
449
|
/**
|
|
453
450
|
* Encode an HTTP envelope plus optional body as one continuous byte stream:
|
|
454
451
|
*
|
|
@@ -702,15 +699,15 @@
|
|
|
702
699
|
};
|
|
703
700
|
return { kind: "none" };
|
|
704
701
|
}
|
|
705
|
-
const CRLF = new Uint8Array([13, 10]);
|
|
706
|
-
const LAST_CHUNK = new Uint8Array([
|
|
702
|
+
const CRLF = /* @__PURE__ */ new Uint8Array([13, 10]);
|
|
703
|
+
const LAST_CHUNK = /* @__PURE__ */ new Uint8Array([
|
|
707
704
|
48,
|
|
708
705
|
13,
|
|
709
706
|
10,
|
|
710
707
|
13,
|
|
711
708
|
10
|
|
712
709
|
]);
|
|
713
|
-
const encoder = new TextEncoder();
|
|
710
|
+
const encoder = /* @__PURE__ */ new TextEncoder();
|
|
714
711
|
/**
|
|
715
712
|
* Chunk sizes are written in HEXADECIMAL — a 21-byte chunk is `15`. Writing
|
|
716
713
|
* them in decimal is the defect note 15 found in @libp2p/http, and it is the
|
|
@@ -1015,7 +1012,7 @@
|
|
|
1015
1012
|
decodeResponse: (input, o) => decodeResponse(input, opts, o.method)
|
|
1016
1013
|
};
|
|
1017
1014
|
}
|
|
1018
|
-
const httpCodec = newHttpCodec();
|
|
1015
|
+
const httpCodec = /* @__PURE__ */ newHttpCodec();
|
|
1019
1016
|
/**
|
|
1020
1017
|
* Dispatches on byte 0. The formats are self-identifying — a JSON envelope
|
|
1021
1018
|
* always begins `{`, which is not a token character and so can never begin an
|
|
@@ -1065,7 +1062,7 @@
|
|
|
1065
1062
|
* Writes HTTP/1.1; accepts HTTP/1.1 or the legacy JSON envelope. Used whenever
|
|
1066
1063
|
* no `codec` option is supplied.
|
|
1067
1064
|
*/
|
|
1068
|
-
const defaultCodec = newSniffingCodec({
|
|
1065
|
+
const defaultCodec = /* @__PURE__ */ newSniffingCodec({
|
|
1069
1066
|
write: httpCodec,
|
|
1070
1067
|
accept: [httpCodec, jsonEnvelopeCodec]
|
|
1071
1068
|
});
|
|
@@ -1119,41 +1116,105 @@
|
|
|
1119
1116
|
*/
|
|
1120
1117
|
async function httpFetch(call, env, body, options = {}) {
|
|
1121
1118
|
const codec = options.codec ?? defaultCodec;
|
|
1119
|
+
const { signal } = options;
|
|
1120
|
+
if (signal?.aborted) throw abortReason$1(signal);
|
|
1122
1121
|
const output = call(codec.encodeRequest(env, body));
|
|
1123
|
-
|
|
1124
|
-
const release =
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1122
|
+
let released;
|
|
1123
|
+
const release = () => {
|
|
1124
|
+
released ??= (async () => {
|
|
1125
|
+
try {
|
|
1126
|
+
await output.return?.(void 0);
|
|
1127
|
+
} catch {}
|
|
1128
|
+
})();
|
|
1129
|
+
return released;
|
|
1130
|
+
};
|
|
1131
|
+
let finished = false;
|
|
1132
|
+
const finish = () => {
|
|
1133
|
+
if (finished || released) return;
|
|
1134
|
+
finished = true;
|
|
1135
|
+
if (signal?.aborted) {
|
|
1136
|
+
release();
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
const onAbort = () => void release();
|
|
1140
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1141
|
+
drain(output).finally(() => signal?.removeEventListener("abort", onAbort));
|
|
1128
1142
|
};
|
|
1143
|
+
let decoded;
|
|
1144
|
+
try {
|
|
1145
|
+
decoded = await untilAborted(codec.decodeResponse(output, { method: env.method }), signal);
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
if (signal?.aborted) release();
|
|
1148
|
+
else await release();
|
|
1149
|
+
throw error;
|
|
1150
|
+
}
|
|
1129
1151
|
const result = {
|
|
1130
1152
|
envelope: decoded.envelope,
|
|
1131
|
-
body: releasingOnExit(decoded.body, release),
|
|
1132
|
-
release
|
|
1153
|
+
body: releasingOnExit(decoded.body, finish, release),
|
|
1154
|
+
release,
|
|
1155
|
+
finish
|
|
1133
1156
|
};
|
|
1134
1157
|
await throwIfPeerError(result, output);
|
|
1135
1158
|
return result;
|
|
1136
1159
|
}
|
|
1160
|
+
/** `promise`, rejected with the signal's reason if it aborts first. */
|
|
1161
|
+
function untilAborted(promise, signal) {
|
|
1162
|
+
if (!signal) return promise;
|
|
1163
|
+
return new Promise((resolve, reject) => {
|
|
1164
|
+
const onAbort = () => reject(abortReason$1(signal));
|
|
1165
|
+
if (signal.aborted) return onAbort();
|
|
1166
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1167
|
+
promise.then((value) => {
|
|
1168
|
+
signal.removeEventListener("abort", onAbort);
|
|
1169
|
+
resolve(value);
|
|
1170
|
+
}, (error) => {
|
|
1171
|
+
signal.removeEventListener("abort", onAbort);
|
|
1172
|
+
reject(error);
|
|
1173
|
+
});
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
/** The signal's reason as an `Error` (`AbortError` when it gave none). */
|
|
1177
|
+
function abortReason$1(signal) {
|
|
1178
|
+
const reason = signal.reason;
|
|
1179
|
+
if (reason instanceof Error) return reason;
|
|
1180
|
+
const err = new Error(reason === void 0 ? "Aborted" : String(reason));
|
|
1181
|
+
err.name = "AbortError";
|
|
1182
|
+
return err;
|
|
1183
|
+
}
|
|
1137
1184
|
/**
|
|
1138
|
-
* `body`,
|
|
1185
|
+
* `body`, ending the call however it is left: read to its end, or left early.
|
|
1139
1186
|
*
|
|
1140
1187
|
* The body the codec hands back is a generator of its own over a `ByteReader`,
|
|
1141
|
-
* and
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1147
|
-
|
|
1148
|
-
|
|
1188
|
+
* and its ending never reaches the call's `output` (see `throwIfPeerError` for
|
|
1189
|
+
* why the codec must not do that itself). A caller that stopped reading — an
|
|
1190
|
+
* aborted `fetch`, a cancelled response stream — left the peer's producer
|
|
1191
|
+
* parked forever; and one that read the body to its end left the call itself
|
|
1192
|
+
* open, which on a multiplexed transport is one port per exchange until the
|
|
1193
|
+
* multiplexer refuses to open more (`maxPorts`).
|
|
1194
|
+
*
|
|
1195
|
+
* Left early, the call is abandoned (`.return()`): the peer's producer is
|
|
1196
|
+
* released. Read to its end, the call is not cut short but read to ITS end, in
|
|
1197
|
+
* the background: the peer ends it once its side of the exchange is over,
|
|
1198
|
+
* which may be later than the response — a handler that answered before it
|
|
1199
|
+
* read the request body is still receiving it (`HttpDataHandlerResult.settled`),
|
|
1200
|
+
* and abandoning the call would abort that upload.
|
|
1201
|
+
*/
|
|
1202
|
+
async function* releasingOnExit(body, finish, release) {
|
|
1149
1203
|
let completed = false;
|
|
1150
1204
|
try {
|
|
1151
1205
|
yield* body;
|
|
1152
1206
|
completed = true;
|
|
1153
1207
|
} finally {
|
|
1154
|
-
if (
|
|
1208
|
+
if (completed) finish();
|
|
1209
|
+
else await release();
|
|
1155
1210
|
}
|
|
1156
1211
|
}
|
|
1212
|
+
/** Reads `output` to its end, discarding what is left, ignoring a failure. */
|
|
1213
|
+
async function drain(output) {
|
|
1214
|
+
try {
|
|
1215
|
+
for (let r = await output.next(); !r.done; r = await output.next());
|
|
1216
|
+
} catch {}
|
|
1217
|
+
}
|
|
1157
1218
|
/**
|
|
1158
1219
|
* The one place that knows a runtime may not implement request body streams,
|
|
1159
1220
|
* and the buffering both fallbacks need. `fetch.ts` and `http-stubs.ts` each
|
|
@@ -1345,14 +1406,18 @@
|
|
|
1345
1406
|
const buffered = new Uint8Array(await request.arrayBuffer());
|
|
1346
1407
|
if (buffered.byteLength > 0) body = [buffered];
|
|
1347
1408
|
}
|
|
1348
|
-
const { envelope, body: respBody, release } = await httpFetch(call, env, body,
|
|
1409
|
+
const { envelope, body: respBody, release, finish } = await httpFetch(call, env, body, {
|
|
1410
|
+
...options,
|
|
1411
|
+
signal: request.signal ?? void 0
|
|
1412
|
+
});
|
|
1349
1413
|
const responseInit = {
|
|
1350
1414
|
status: envelope.status,
|
|
1351
1415
|
statusText: envelope.statusText,
|
|
1352
1416
|
headers: forwardableHeaders(envelope.headers)
|
|
1353
1417
|
};
|
|
1354
1418
|
if (request.method === "HEAD" || request.method === "OPTIONS" || NULL_BODY_STATUSES.has(envelope.status)) {
|
|
1355
|
-
|
|
1419
|
+
if (finish) finish();
|
|
1420
|
+
else await release?.();
|
|
1356
1421
|
return new Response(null, responseInit);
|
|
1357
1422
|
}
|
|
1358
1423
|
return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
|
|
@@ -1367,13 +1432,6 @@
|
|
|
1367
1432
|
yield chunk;
|
|
1368
1433
|
}
|
|
1369
1434
|
}
|
|
1370
|
-
function abortReason$1(signal) {
|
|
1371
|
-
const reason = signal.reason;
|
|
1372
|
-
if (reason instanceof Error) return reason;
|
|
1373
|
-
const err = new Error(reason === void 0 ? "Aborted" : String(reason));
|
|
1374
|
-
err.name = "AbortError";
|
|
1375
|
-
return err;
|
|
1376
|
-
}
|
|
1377
1435
|
var HttpError = class HttpError extends Error {
|
|
1378
1436
|
status;
|
|
1379
1437
|
statusText;
|
|
@@ -1748,6 +1806,69 @@
|
|
|
1748
1806
|
}
|
|
1749
1807
|
}
|
|
1750
1808
|
/**
|
|
1809
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
1810
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
1811
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
1812
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
1813
|
+
* declared types, which would require consumers to compile with the
|
|
1814
|
+
* `esnext.disposable` lib.
|
|
1815
|
+
*/
|
|
1816
|
+
function asyncDisposable$1(target, dispose) {
|
|
1817
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
1818
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
1819
|
+
value: async () => {
|
|
1820
|
+
await dispose();
|
|
1821
|
+
},
|
|
1822
|
+
configurable: true,
|
|
1823
|
+
writable: true
|
|
1824
|
+
});
|
|
1825
|
+
return target;
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* `gen`, except that `return()` and `throw()` take effect at once.
|
|
1829
|
+
*
|
|
1830
|
+
* On an async generator both are queued behind a `next()` that is still
|
|
1831
|
+
* pending: a consumer that gives up on a stream while it waits for the peer's
|
|
1832
|
+
* next chunk — an aborted fetch whose response head has not come, say — cannot
|
|
1833
|
+
* end it until that chunk arrives, which for a peer that never answers is
|
|
1834
|
+
* never. The stream's `finally` (which closes its port and tells the peer)
|
|
1835
|
+
* then never runs, and on a multiplexer the port is kept for good.
|
|
1836
|
+
*
|
|
1837
|
+
* So `abandon` runs first, synchronously: it must make the pending `next()`
|
|
1838
|
+
* settle (abort the stream), which then resolves done, after which the
|
|
1839
|
+
* generator's own `return()` / `throw()` proceeds as usual. On a generator
|
|
1840
|
+
* suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
|
|
1841
|
+
*/
|
|
1842
|
+
function returningAtOnce(gen, abandon) {
|
|
1843
|
+
let abandoned = false;
|
|
1844
|
+
const giveUp = (reason) => {
|
|
1845
|
+
if (abandoned) return;
|
|
1846
|
+
abandoned = true;
|
|
1847
|
+
abandon(reason);
|
|
1848
|
+
};
|
|
1849
|
+
const wrapped = {
|
|
1850
|
+
next: (...args) => gen.next(...args).catch((error) => {
|
|
1851
|
+
if (abandoned) return {
|
|
1852
|
+
done: true,
|
|
1853
|
+
value: void 0
|
|
1854
|
+
};
|
|
1855
|
+
throw error;
|
|
1856
|
+
}),
|
|
1857
|
+
return(value) {
|
|
1858
|
+
giveUp();
|
|
1859
|
+
return gen.return(value);
|
|
1860
|
+
},
|
|
1861
|
+
throw(error) {
|
|
1862
|
+
giveUp(error);
|
|
1863
|
+
return gen.throw(error);
|
|
1864
|
+
},
|
|
1865
|
+
[Symbol.asyncIterator]() {
|
|
1866
|
+
return wrapped;
|
|
1867
|
+
}
|
|
1868
|
+
};
|
|
1869
|
+
return asyncDisposable$1(wrapped, () => wrapped.return(void 0));
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1751
1872
|
* The `type` of the out-of-band notice a side posts when it abandons a stream.
|
|
1752
1873
|
*
|
|
1753
1874
|
* Layer 1's `close` is not observable to layer 2 — a closed virtual port drops
|
|
@@ -1784,7 +1905,7 @@
|
|
|
1784
1905
|
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
1785
1906
|
const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
|
|
1786
1907
|
pump.catch(() => {});
|
|
1787
|
-
return (async function* () {
|
|
1908
|
+
return returningAtOnce((async function* () {
|
|
1788
1909
|
try {
|
|
1789
1910
|
yield* inbound.stream;
|
|
1790
1911
|
} finally {
|
|
@@ -1798,7 +1919,9 @@
|
|
|
1798
1919
|
await port.close?.();
|
|
1799
1920
|
} catch {}
|
|
1800
1921
|
}
|
|
1801
|
-
})()
|
|
1922
|
+
})(), () => {
|
|
1923
|
+
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
1924
|
+
});
|
|
1802
1925
|
}
|
|
1803
1926
|
/**
|
|
1804
1927
|
* Listens for the peer's abort notice, and can post our own. The notice is a
|
|
@@ -2194,9 +2317,22 @@
|
|
|
2194
2317
|
maxMessageSize: mux.maxMessageSize,
|
|
2195
2318
|
timeout
|
|
2196
2319
|
};
|
|
2197
|
-
const call = (input) =>
|
|
2198
|
-
|
|
2199
|
-
|
|
2320
|
+
const call = (input) => {
|
|
2321
|
+
let stream;
|
|
2322
|
+
let abandoned = false;
|
|
2323
|
+
return returningAtOnce((async function* () {
|
|
2324
|
+
const port = await mux.openPort(STREAM_META);
|
|
2325
|
+
if (abandoned) {
|
|
2326
|
+
await port.close?.();
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
stream = duplexOverPort(port, streamOptions)(input);
|
|
2330
|
+
yield* stream;
|
|
2331
|
+
})(), () => {
|
|
2332
|
+
abandoned = true;
|
|
2333
|
+
stream?.return(void 0).catch(() => {});
|
|
2334
|
+
});
|
|
2335
|
+
};
|
|
2200
2336
|
return {
|
|
2201
2337
|
call,
|
|
2202
2338
|
async close() {
|
|
@@ -2242,6 +2378,27 @@
|
|
|
2242
2378
|
}
|
|
2243
2379
|
};
|
|
2244
2380
|
//#endregion
|
|
2381
|
+
//#region src/link/async-dispose.ts
|
|
2382
|
+
/**
|
|
2383
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
2384
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
2385
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
2386
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
2387
|
+
* declared types, which would require consumers to compile with the
|
|
2388
|
+
* `esnext.disposable` lib.
|
|
2389
|
+
*/
|
|
2390
|
+
function asyncDisposable(target, dispose) {
|
|
2391
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
2392
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
2393
|
+
value: async () => {
|
|
2394
|
+
await dispose();
|
|
2395
|
+
},
|
|
2396
|
+
configurable: true,
|
|
2397
|
+
writable: true
|
|
2398
|
+
});
|
|
2399
|
+
return target;
|
|
2400
|
+
}
|
|
2401
|
+
//#endregion
|
|
2245
2402
|
//#region src/link/http-link.ts
|
|
2246
2403
|
/**
|
|
2247
2404
|
* The version of the HTTP-over-`MessagePort` protocol this package speaks,
|
|
@@ -2271,11 +2428,12 @@
|
|
|
2271
2428
|
*/
|
|
2272
2429
|
const LINK_CLOSE = "webrun-http:link-close";
|
|
2273
2430
|
/** Opens the calling end of a link on `port` (one port per link). */
|
|
2274
|
-
async function connectHttpLink(port, { streamTimeout } = {}) {
|
|
2431
|
+
async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
|
|
2275
2432
|
const { call, close } = await connect({
|
|
2276
2433
|
mux: overPipe(port, {
|
|
2277
2434
|
codec: structuredCodec,
|
|
2278
|
-
side: "initiator"
|
|
2435
|
+
side: "initiator",
|
|
2436
|
+
maxPorts
|
|
2279
2437
|
}),
|
|
2280
2438
|
timeout: streamTimeout
|
|
2281
2439
|
});
|
|
@@ -2300,7 +2458,7 @@
|
|
|
2300
2458
|
inflight--;
|
|
2301
2459
|
if (inflight === 0) for (const resolve of idleWaiters.splice(0)) resolve();
|
|
2302
2460
|
};
|
|
2303
|
-
|
|
2461
|
+
const link = {
|
|
2304
2462
|
get inflight() {
|
|
2305
2463
|
return inflight;
|
|
2306
2464
|
},
|
|
@@ -2353,6 +2511,7 @@
|
|
|
2353
2511
|
} catch {}
|
|
2354
2512
|
}
|
|
2355
2513
|
};
|
|
2514
|
+
return asyncDisposable(link, () => link.close());
|
|
2356
2515
|
}
|
|
2357
2516
|
/**
|
|
2358
2517
|
* A response body that reports its end (done, cancelled or failed) and fails
|