@tranquilload/adapters 0.1.0 → 0.1.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/.turbo/turbo-build.log +20 -14
- package/CHANGELOG.md +34 -0
- package/dist/simple-http-upload.cjs +25 -3
- package/dist/simple-http-upload.cjs.map +1 -1
- package/dist/simple-http-upload.d.cts +21 -0
- package/dist/simple-http-upload.d.cts.map +1 -1
- package/dist/simple-http-upload.d.mts +21 -0
- package/dist/simple-http-upload.d.mts.map +1 -1
- package/dist/simple-http-upload.mjs +25 -3
- package/dist/simple-http-upload.mjs.map +1 -1
- package/package.json +3 -3
- package/src/protocols/simple-http-upload.test.ts +88 -0
- package/src/protocols/simple-http-upload.ts +60 -7
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @tranquilload/adapters@0.1.
|
|
2
|
+
> @tranquilload/adapters@0.1.1 build /home/runner/work/Tranquilload/Tranquilload/packages/tranquilload-adapters
|
|
3
3
|
> tsdown
|
|
4
4
|
|
|
5
5
|
[34mℹ[39m tsdown [2mv0.21.0[22m powered by rolldown [2mv1.0.0-rc.7[22m
|
|
@@ -8,55 +8,61 @@
|
|
|
8
8
|
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
9
9
|
[34mℹ[39m Build start
|
|
10
10
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[1ms3-multipart-upload.cjs[22m [2m2.15 kB[22m [2m│ gzip: 0.90 kB[22m
|
|
11
|
-
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[1msimple-http-upload.cjs[22m [
|
|
11
|
+
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[1msimple-http-upload.cjs[22m [2m1.54 kB[22m [2m│ gzip: 0.69 kB[22m
|
|
12
12
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[1mnetwork-multiplier.cjs[22m [2m0.92 kB[22m [2m│ gzip: 0.49 kB[22m
|
|
13
13
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[1moptimal-part-size.cjs[22m [2m0.59 kB[22m [2m│ gzip: 0.32 kB[22m
|
|
14
14
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[1mfrom-node-readable.cjs[22m [2m0.35 kB[22m [2m│ gzip: 0.23 kB[22m
|
|
15
15
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[1mfrom-file.cjs[22m [2m0.28 kB[22m [2m│ gzip: 0.22 kB[22m
|
|
16
16
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22ms3-multipart-upload.cjs.map [2m4.38 kB[22m [2m│ gzip: 1.64 kB[22m
|
|
17
|
+
[34mℹ[39m [33m[CJS][39m [2mdist/[22msimple-http-upload.cjs.map [2m4.18 kB[22m [2m│ gzip: 1.80 kB[22m
|
|
17
18
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22mnetwork-multiplier.cjs.map [2m2.21 kB[22m [2m│ gzip: 1.00 kB[22m
|
|
18
|
-
[34mℹ[39m [33m[CJS][39m [2mdist/[22msimple-http-upload.cjs.map [2m1.65 kB[22m [2m│ gzip: 0.80 kB[22m
|
|
19
19
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22moptimal-part-size.cjs.map [2m1.45 kB[22m [2m│ gzip: 0.71 kB[22m
|
|
20
20
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22mfrom-node-readable.cjs.map [2m0.41 kB[22m [2m│ gzip: 0.26 kB[22m
|
|
21
21
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22mfrom-file.cjs.map [2m0.38 kB[22m [2m│ gzip: 0.27 kB[22m
|
|
22
|
-
[34mℹ[39m [33m[CJS][39m 12 files, total:
|
|
22
|
+
[34mℹ[39m [33m[CJS][39m 12 files, total: 18.83 kB
|
|
23
|
+
[33m[PLUGIN_TIMINGS] Warning:[0m Your build spent significant time in plugin `tsdown:report`. See https://rolldown.rs/options/checks#plugintimings for more details.
|
|
24
|
+
|
|
23
25
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22ms3-multipart-upload.d.cts.map [2m0.69 kB[22m [2m│ gzip: 0.33 kB[22m
|
|
26
|
+
[34mℹ[39m [33m[CJS][39m [2mdist/[22msimple-http-upload.d.cts.map [2m0.34 kB[22m [2m│ gzip: 0.21 kB[22m
|
|
24
27
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22mnetwork-multiplier.d.cts.map [2m0.31 kB[22m [2m│ gzip: 0.22 kB[22m
|
|
25
|
-
[34mℹ[39m [33m[CJS][39m [2mdist/[22msimple-http-upload.d.cts.map [2m0.30 kB[22m [2m│ gzip: 0.20 kB[22m
|
|
26
28
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22moptimal-part-size.d.cts.map [2m0.25 kB[22m [2m│ gzip: 0.19 kB[22m
|
|
27
29
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22mfrom-file.d.cts.map [2m0.18 kB[22m [2m│ gzip: 0.15 kB[22m
|
|
28
30
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22mfrom-node-readable.d.cts.map [2m0.18 kB[22m [2m│ gzip: 0.15 kB[22m
|
|
31
|
+
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[32m[1msimple-http-upload.d.cts[22m[39m [2m1.41 kB[22m [2m│ gzip: 0.74 kB[22m
|
|
29
32
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[32m[1ms3-multipart-upload.d.cts[22m[39m [2m1.20 kB[22m [2m│ gzip: 0.49 kB[22m
|
|
30
33
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[32m[1mnetwork-multiplier.d.cts[22m[39m [2m0.97 kB[22m [2m│ gzip: 0.49 kB[22m
|
|
31
34
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[32m[1moptimal-part-size.d.cts[22m[39m [2m0.87 kB[22m [2m│ gzip: 0.46 kB[22m
|
|
32
|
-
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[32m[1msimple-http-upload.d.cts[22m[39m [2m0.44 kB[22m [2m│ gzip: 0.28 kB[22m
|
|
33
35
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[32m[1mfrom-node-readable.d.cts[22m[39m [2m0.26 kB[22m [2m│ gzip: 0.18 kB[22m
|
|
34
36
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[32m[1mfrom-file.d.cts[22m[39m [2m0.21 kB[22m [2m│ gzip: 0.18 kB[22m
|
|
35
|
-
[34mℹ[39m [33m[CJS][39m 12 files, total:
|
|
36
|
-
[
|
|
37
|
+
[34mℹ[39m [33m[CJS][39m 12 files, total: 6.87 kB
|
|
38
|
+
[33m[PLUGIN_TIMINGS] Warning:[0m Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
|
|
39
|
+
|
|
40
|
+
[32m✔[39m Build complete in [32m3501ms[39m
|
|
37
41
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[1ms3-multipart-upload.mjs[22m [2m1.96 kB[22m [2m│ gzip: 0.84 kB[22m
|
|
42
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[1msimple-http-upload.mjs[22m [2m1.33 kB[22m [2m│ gzip: 0.62 kB[22m
|
|
38
43
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[1mnetwork-multiplier.mjs[22m [2m0.83 kB[22m [2m│ gzip: 0.44 kB[22m
|
|
39
|
-
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[1msimple-http-upload.mjs[22m [2m0.77 kB[22m [2m│ gzip: 0.43 kB[22m
|
|
40
44
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[1moptimal-part-size.mjs[22m [2m0.49 kB[22m [2m│ gzip: 0.27 kB[22m
|
|
41
45
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[1mfrom-node-readable.mjs[22m [2m0.25 kB[22m [2m│ gzip: 0.17 kB[22m
|
|
42
46
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[1mfrom-file.mjs[22m [2m0.20 kB[22m [2m│ gzip: 0.17 kB[22m
|
|
43
47
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22ms3-multipart-upload.mjs.map [2m4.29 kB[22m [2m│ gzip: 1.62 kB[22m
|
|
48
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22msimple-http-upload.mjs.map [2m4.10 kB[22m [2m│ gzip: 1.78 kB[22m
|
|
44
49
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22mnetwork-multiplier.mjs.map [2m2.21 kB[22m [2m│ gzip: 1.00 kB[22m
|
|
45
|
-
[34mℹ[39m [34m[ESM][39m [2mdist/[22msimple-http-upload.mjs.map [2m1.59 kB[22m [2m│ gzip: 0.78 kB[22m
|
|
46
50
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22moptimal-part-size.mjs.map [2m1.44 kB[22m [2m│ gzip: 0.71 kB[22m
|
|
47
51
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22ms3-multipart-upload.d.mts.map [2m0.69 kB[22m [2m│ gzip: 0.33 kB[22m
|
|
48
52
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22mfrom-node-readable.mjs.map [2m0.39 kB[22m [2m│ gzip: 0.25 kB[22m
|
|
49
53
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22mfrom-file.mjs.map [2m0.38 kB[22m [2m│ gzip: 0.27 kB[22m
|
|
54
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22msimple-http-upload.d.mts.map [2m0.34 kB[22m [2m│ gzip: 0.21 kB[22m
|
|
50
55
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22mnetwork-multiplier.d.mts.map [2m0.31 kB[22m [2m│ gzip: 0.22 kB[22m
|
|
51
|
-
[34mℹ[39m [34m[ESM][39m [2mdist/[22msimple-http-upload.d.mts.map [2m0.30 kB[22m [2m│ gzip: 0.20 kB[22m
|
|
52
56
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22moptimal-part-size.d.mts.map [2m0.25 kB[22m [2m│ gzip: 0.19 kB[22m
|
|
53
57
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22mfrom-file.d.mts.map [2m0.18 kB[22m [2m│ gzip: 0.15 kB[22m
|
|
54
58
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22mfrom-node-readable.d.mts.map [2m0.18 kB[22m [2m│ gzip: 0.15 kB[22m
|
|
59
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[32m[1msimple-http-upload.d.mts[22m[39m [2m1.41 kB[22m [2m│ gzip: 0.74 kB[22m
|
|
55
60
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[32m[1ms3-multipart-upload.d.mts[22m[39m [2m1.20 kB[22m [2m│ gzip: 0.49 kB[22m
|
|
56
61
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[32m[1mnetwork-multiplier.d.mts[22m[39m [2m0.97 kB[22m [2m│ gzip: 0.49 kB[22m
|
|
57
62
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[32m[1moptimal-part-size.d.mts[22m[39m [2m0.87 kB[22m [2m│ gzip: 0.46 kB[22m
|
|
58
|
-
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[32m[1msimple-http-upload.d.mts[22m[39m [2m0.44 kB[22m [2m│ gzip: 0.28 kB[22m
|
|
59
63
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[32m[1mfrom-node-readable.d.mts[22m[39m [2m0.26 kB[22m [2m│ gzip: 0.18 kB[22m
|
|
60
64
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[32m[1mfrom-file.d.mts[22m[39m [2m0.21 kB[22m [2m│ gzip: 0.18 kB[22m
|
|
61
|
-
[34mℹ[39m [34m[ESM][39m 24 files, total:
|
|
62
|
-
[
|
|
65
|
+
[34mℹ[39m [34m[ESM][39m 24 files, total: 24.74 kB
|
|
66
|
+
[33m[PLUGIN_TIMINGS] Warning:[0m Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
|
|
67
|
+
|
|
68
|
+
[32m✔[39m Build complete in [32m3514ms[39m
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @tranquilload/adapters
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 6ca9186: `simpleHttpUpload`: add `bufferMode` option + set `duplex: 'half'` on streaming uploads.
|
|
8
|
+
|
|
9
|
+
The streaming PUT path now sets `duplex: 'half'`, which modern browsers and
|
|
10
|
+
Node 22+ require to accept a `ReadableStream` as a `fetch` body. **Streaming
|
|
11
|
+
PUT now requires an HTTP/2 endpoint** — HTTP/1.x will reject the request.
|
|
12
|
+
|
|
13
|
+
For HTTP/1.x targets (or environments where the `duplex` flag is unavailable),
|
|
14
|
+
opt in to `bufferMode: true`. The adapter drains the entire source stream
|
|
15
|
+
into a `Blob` before issuing the request:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
const adapter = simpleHttpUpload({
|
|
19
|
+
url: "https://legacy-http1.example.com/upload",
|
|
20
|
+
bufferMode: true,
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Memory caveat.** `bufferMode: true` buffers the whole source into memory —
|
|
25
|
+
do not enable for files larger than available memory. Use it as the HTTP/1.x
|
|
26
|
+
escape hatch, not as a default.
|
|
27
|
+
|
|
28
|
+
The drain loop is signal-aware: aborting via `AbortSignal` between read
|
|
29
|
+
iterations rejects with `AbortError` (not `CompleteUploadError`), preserving
|
|
30
|
+
the abort phase mapping.
|
|
31
|
+
|
|
32
|
+
- Updated dependencies [6ca9186]
|
|
33
|
+
- Updated dependencies [6ca9186]
|
|
34
|
+
- @tranquilload/core@0.1.1
|
|
@@ -2,17 +2,39 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
let _tranquilload_core_errors = require("@tranquilload/core/errors");
|
|
3
3
|
//#region src/protocols/simple-http-upload.ts
|
|
4
4
|
function simpleHttpUpload(options) {
|
|
5
|
-
const { url, method = "PUT", headers, signal } = options;
|
|
5
|
+
const { url, method = "PUT", headers, signal, bufferMode = false } = options;
|
|
6
6
|
const upload = async (stream) => {
|
|
7
7
|
let response;
|
|
8
8
|
try {
|
|
9
|
-
|
|
9
|
+
if (bufferMode) {
|
|
10
|
+
const reader = stream.getReader();
|
|
11
|
+
const chunks = [];
|
|
12
|
+
try {
|
|
13
|
+
while (true) {
|
|
14
|
+
if (signal?.aborted) throw new _tranquilload_core_errors.AbortError();
|
|
15
|
+
const { done, value } = await reader.read();
|
|
16
|
+
if (done) break;
|
|
17
|
+
if (value) chunks.push(value);
|
|
18
|
+
}
|
|
19
|
+
} finally {
|
|
20
|
+
reader.releaseLock();
|
|
21
|
+
}
|
|
22
|
+
const blob = new Blob(chunks);
|
|
23
|
+
response = await fetch(url, {
|
|
24
|
+
method,
|
|
25
|
+
headers,
|
|
26
|
+
body: blob,
|
|
27
|
+
signal
|
|
28
|
+
});
|
|
29
|
+
} else response = await fetch(url, {
|
|
10
30
|
method,
|
|
11
31
|
headers,
|
|
12
32
|
body: stream,
|
|
13
|
-
signal
|
|
33
|
+
signal,
|
|
34
|
+
duplex: "half"
|
|
14
35
|
});
|
|
15
36
|
} catch (cause) {
|
|
37
|
+
if (cause instanceof _tranquilload_core_errors.AbortError) throw cause;
|
|
16
38
|
if (cause instanceof Error && cause.name === "AbortError") throw new _tranquilload_core_errors.AbortError();
|
|
17
39
|
throw new _tranquilload_core_errors.CompleteUploadError(cause);
|
|
18
40
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"simple-http-upload.cjs","names":["AbortError","CompleteUploadError"],"sources":["../src/protocols/simple-http-upload.ts"],"sourcesContent":["import { AbortError, CompleteUploadError } from \"@tranquilload/core/errors\"\n\nexport interface SimpleHttpUploadOptions {\n url: string\n method?: \"PUT\" | \"POST\"\n headers?: Record<string, string>\n signal?: AbortSignal\n}\n\nexport function simpleHttpUpload(options: SimpleHttpUploadOptions): {\n upload: (stream: ReadableStream<Uint8Array>) => Promise<void>\n} {\n const { url, method = \"PUT\", headers, signal } = options\n\n const upload = async (stream: ReadableStream<Uint8Array>): Promise<void> => {\n let response: Response\n try {\n response = await fetch(url, {\n
|
|
1
|
+
{"version":3,"file":"simple-http-upload.cjs","names":["AbortError","CompleteUploadError"],"sources":["../src/protocols/simple-http-upload.ts"],"sourcesContent":["import { AbortError, CompleteUploadError } from \"@tranquilload/core/errors\"\n\n/**\n * Adapter for the simplest possible HTTP upload: PUT/POST a stream of bytes\n * directly to a single URL.\n *\n * **Streaming PUT requires HTTP/2 and `duplex: 'half'`.** This is the default\n * mode and is the most memory-efficient option. If your target only speaks\n * HTTP/1.x, or your runtime does not understand `duplex: 'half'`, set\n * `bufferMode: true` to buffer the whole source into a `Blob` before sending.\n */\nexport interface SimpleHttpUploadOptions {\n url: string\n method?: \"PUT\" | \"POST\"\n headers?: Record<string, string>\n signal?: AbortSignal\n /**\n * When `true`, drains the entire source stream into a `Blob` before issuing\n * the PUT/POST request. The request is then a normal buffered upload (no\n * `duplex: 'half'` required, works on HTTP/1.x).\n *\n * **Memory usage equals the source size — DO NOT enable for files larger\n * than available memory.** Use only when streaming PUT isn't supported\n * (HTTP/1.x, environments where `duplex: 'half'` is unavailable).\n *\n * Default: `false` (streaming with `duplex: 'half'`, requires HTTP/2).\n */\n bufferMode?: boolean\n}\n\nexport function simpleHttpUpload(options: SimpleHttpUploadOptions): {\n upload: (stream: ReadableStream<Uint8Array>) => Promise<void>\n} {\n const { url, method = \"PUT\", headers, signal, bufferMode = false } = options\n\n const upload = async (stream: ReadableStream<Uint8Array>): Promise<void> => {\n let response: Response\n try {\n if (bufferMode) {\n const reader = stream.getReader()\n const chunks: Uint8Array[] = []\n try {\n // Manual drain loop with per-iteration abort check: `Response#blob()`\n // ignores AbortSignal, so we cannot rely on `new Response(stream).blob()`.\n while (true) {\n if (signal?.aborted) {\n throw new AbortError()\n }\n const { done, value } = await reader.read()\n if (done) break\n if (value) chunks.push(value)\n }\n } finally {\n reader.releaseLock()\n }\n const blob = new Blob(chunks as BlobPart[])\n response = await fetch(url, {\n method,\n headers,\n body: blob,\n signal,\n })\n } else {\n // duplex: 'half' is not in lib.dom.d.ts but is required by modern\n // browsers/Node 22+ to accept a ReadableStream as a fetch body.\n response = await fetch(url, {\n method,\n headers,\n body: stream as unknown as BodyInit,\n signal,\n duplex: \"half\",\n } as RequestInit & { duplex: \"half\" })\n }\n } catch (cause) {\n if (cause instanceof AbortError) {\n throw cause\n }\n if (cause instanceof Error && cause.name === \"AbortError\") {\n throw new AbortError()\n }\n throw new CompleteUploadError(cause)\n }\n if (!response.ok) {\n throw new CompleteUploadError(\n new Error(`HTTP ${response.status} ${response.statusText}`)\n )\n }\n }\n\n return { upload }\n}\n"],"mappings":";;;AA8BA,SAAgB,iBAAiB,SAE/B;CACA,MAAM,EAAE,KAAK,SAAS,OAAO,SAAS,QAAQ,aAAa,UAAU;CAErE,MAAM,SAAS,OAAO,WAAsD;EAC1E,IAAI;AACJ,MAAI;AACF,OAAI,YAAY;IACd,MAAM,SAAS,OAAO,WAAW;IACjC,MAAM,SAAuB,EAAE;AAC/B,QAAI;AAGF,YAAO,MAAM;AACX,UAAI,QAAQ,QACV,OAAM,IAAIA,0BAAAA,YAAY;MAExB,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,UAAI,KAAM;AACV,UAAI,MAAO,QAAO,KAAK,MAAM;;cAEvB;AACR,YAAO,aAAa;;IAEtB,MAAM,OAAO,IAAI,KAAK,OAAqB;AAC3C,eAAW,MAAM,MAAM,KAAK;KAC1B;KACA;KACA,MAAM;KACN;KACD,CAAC;SAIF,YAAW,MAAM,MAAM,KAAK;IAC1B;IACA;IACA,MAAM;IACN;IACA,QAAQ;IACT,CAAqC;WAEjC,OAAO;AACd,OAAI,iBAAiBA,0BAAAA,WACnB,OAAM;AAER,OAAI,iBAAiB,SAAS,MAAM,SAAS,aAC3C,OAAM,IAAIA,0BAAAA,YAAY;AAExB,SAAM,IAAIC,0BAAAA,oBAAoB,MAAM;;AAEtC,MAAI,CAAC,SAAS,GACZ,OAAM,IAAIA,0BAAAA,oCACR,IAAI,MAAM,QAAQ,SAAS,OAAO,GAAG,SAAS,aAAa,CAC5D;;AAIL,QAAO,EAAE,QAAQ"}
|
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
//#region src/protocols/simple-http-upload.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Adapter for the simplest possible HTTP upload: PUT/POST a stream of bytes
|
|
4
|
+
* directly to a single URL.
|
|
5
|
+
*
|
|
6
|
+
* **Streaming PUT requires HTTP/2 and `duplex: 'half'`.** This is the default
|
|
7
|
+
* mode and is the most memory-efficient option. If your target only speaks
|
|
8
|
+
* HTTP/1.x, or your runtime does not understand `duplex: 'half'`, set
|
|
9
|
+
* `bufferMode: true` to buffer the whole source into a `Blob` before sending.
|
|
10
|
+
*/
|
|
2
11
|
interface SimpleHttpUploadOptions {
|
|
3
12
|
url: string;
|
|
4
13
|
method?: "PUT" | "POST";
|
|
5
14
|
headers?: Record<string, string>;
|
|
6
15
|
signal?: AbortSignal;
|
|
16
|
+
/**
|
|
17
|
+
* When `true`, drains the entire source stream into a `Blob` before issuing
|
|
18
|
+
* the PUT/POST request. The request is then a normal buffered upload (no
|
|
19
|
+
* `duplex: 'half'` required, works on HTTP/1.x).
|
|
20
|
+
*
|
|
21
|
+
* **Memory usage equals the source size — DO NOT enable for files larger
|
|
22
|
+
* than available memory.** Use only when streaming PUT isn't supported
|
|
23
|
+
* (HTTP/1.x, environments where `duplex: 'half'` is unavailable).
|
|
24
|
+
*
|
|
25
|
+
* Default: `false` (streaming with `duplex: 'half'`, requires HTTP/2).
|
|
26
|
+
*/
|
|
27
|
+
bufferMode?: boolean;
|
|
7
28
|
}
|
|
8
29
|
declare function simpleHttpUpload(options: SimpleHttpUploadOptions): {
|
|
9
30
|
upload: (stream: ReadableStream<Uint8Array>) => Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"simple-http-upload.d.cts","names":[],"sources":["../src/protocols/simple-http-upload.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"simple-http-upload.d.cts","names":[],"sources":["../src/protocols/simple-http-upload.ts"],"mappings":";;AAWA;;;;;;;;UAAiB,uBAAA;EACf,GAAA;EACA,MAAA;EACA,OAAA,GAAU,MAAA;EACV,MAAA,GAAS,WAAA;EAeK;;;;;;;;;;;EAHd,UAAA;AAAA;AAAA,iBAGc,gBAAA,CAAiB,OAAA,EAAS,uBAAA;EACxC,MAAA,GAAS,MAAA,EAAQ,cAAA,CAAe,UAAA,MAAgB,OAAA;AAAA"}
|
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
//#region src/protocols/simple-http-upload.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Adapter for the simplest possible HTTP upload: PUT/POST a stream of bytes
|
|
4
|
+
* directly to a single URL.
|
|
5
|
+
*
|
|
6
|
+
* **Streaming PUT requires HTTP/2 and `duplex: 'half'`.** This is the default
|
|
7
|
+
* mode and is the most memory-efficient option. If your target only speaks
|
|
8
|
+
* HTTP/1.x, or your runtime does not understand `duplex: 'half'`, set
|
|
9
|
+
* `bufferMode: true` to buffer the whole source into a `Blob` before sending.
|
|
10
|
+
*/
|
|
2
11
|
interface SimpleHttpUploadOptions {
|
|
3
12
|
url: string;
|
|
4
13
|
method?: "PUT" | "POST";
|
|
5
14
|
headers?: Record<string, string>;
|
|
6
15
|
signal?: AbortSignal;
|
|
16
|
+
/**
|
|
17
|
+
* When `true`, drains the entire source stream into a `Blob` before issuing
|
|
18
|
+
* the PUT/POST request. The request is then a normal buffered upload (no
|
|
19
|
+
* `duplex: 'half'` required, works on HTTP/1.x).
|
|
20
|
+
*
|
|
21
|
+
* **Memory usage equals the source size — DO NOT enable for files larger
|
|
22
|
+
* than available memory.** Use only when streaming PUT isn't supported
|
|
23
|
+
* (HTTP/1.x, environments where `duplex: 'half'` is unavailable).
|
|
24
|
+
*
|
|
25
|
+
* Default: `false` (streaming with `duplex: 'half'`, requires HTTP/2).
|
|
26
|
+
*/
|
|
27
|
+
bufferMode?: boolean;
|
|
7
28
|
}
|
|
8
29
|
declare function simpleHttpUpload(options: SimpleHttpUploadOptions): {
|
|
9
30
|
upload: (stream: ReadableStream<Uint8Array>) => Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"simple-http-upload.d.mts","names":[],"sources":["../src/protocols/simple-http-upload.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"simple-http-upload.d.mts","names":[],"sources":["../src/protocols/simple-http-upload.ts"],"mappings":";;AAWA;;;;;;;;UAAiB,uBAAA;EACf,GAAA;EACA,MAAA;EACA,OAAA,GAAU,MAAA;EACV,MAAA,GAAS,WAAA;EAeK;;;;;;;;;;;EAHd,UAAA;AAAA;AAAA,iBAGc,gBAAA,CAAiB,OAAA,EAAS,uBAAA;EACxC,MAAA,GAAS,MAAA,EAAQ,cAAA,CAAe,UAAA,MAAgB,OAAA;AAAA"}
|
|
@@ -1,17 +1,39 @@
|
|
|
1
1
|
import { AbortError, CompleteUploadError } from "@tranquilload/core/errors";
|
|
2
2
|
//#region src/protocols/simple-http-upload.ts
|
|
3
3
|
function simpleHttpUpload(options) {
|
|
4
|
-
const { url, method = "PUT", headers, signal } = options;
|
|
4
|
+
const { url, method = "PUT", headers, signal, bufferMode = false } = options;
|
|
5
5
|
const upload = async (stream) => {
|
|
6
6
|
let response;
|
|
7
7
|
try {
|
|
8
|
-
|
|
8
|
+
if (bufferMode) {
|
|
9
|
+
const reader = stream.getReader();
|
|
10
|
+
const chunks = [];
|
|
11
|
+
try {
|
|
12
|
+
while (true) {
|
|
13
|
+
if (signal?.aborted) throw new AbortError();
|
|
14
|
+
const { done, value } = await reader.read();
|
|
15
|
+
if (done) break;
|
|
16
|
+
if (value) chunks.push(value);
|
|
17
|
+
}
|
|
18
|
+
} finally {
|
|
19
|
+
reader.releaseLock();
|
|
20
|
+
}
|
|
21
|
+
const blob = new Blob(chunks);
|
|
22
|
+
response = await fetch(url, {
|
|
23
|
+
method,
|
|
24
|
+
headers,
|
|
25
|
+
body: blob,
|
|
26
|
+
signal
|
|
27
|
+
});
|
|
28
|
+
} else response = await fetch(url, {
|
|
9
29
|
method,
|
|
10
30
|
headers,
|
|
11
31
|
body: stream,
|
|
12
|
-
signal
|
|
32
|
+
signal,
|
|
33
|
+
duplex: "half"
|
|
13
34
|
});
|
|
14
35
|
} catch (cause) {
|
|
36
|
+
if (cause instanceof AbortError) throw cause;
|
|
15
37
|
if (cause instanceof Error && cause.name === "AbortError") throw new AbortError();
|
|
16
38
|
throw new CompleteUploadError(cause);
|
|
17
39
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"simple-http-upload.mjs","names":[],"sources":["../src/protocols/simple-http-upload.ts"],"sourcesContent":["import { AbortError, CompleteUploadError } from \"@tranquilload/core/errors\"\n\nexport interface SimpleHttpUploadOptions {\n url: string\n method?: \"PUT\" | \"POST\"\n headers?: Record<string, string>\n signal?: AbortSignal\n}\n\nexport function simpleHttpUpload(options: SimpleHttpUploadOptions): {\n upload: (stream: ReadableStream<Uint8Array>) => Promise<void>\n} {\n const { url, method = \"PUT\", headers, signal } = options\n\n const upload = async (stream: ReadableStream<Uint8Array>): Promise<void> => {\n let response: Response\n try {\n response = await fetch(url, {\n
|
|
1
|
+
{"version":3,"file":"simple-http-upload.mjs","names":[],"sources":["../src/protocols/simple-http-upload.ts"],"sourcesContent":["import { AbortError, CompleteUploadError } from \"@tranquilload/core/errors\"\n\n/**\n * Adapter for the simplest possible HTTP upload: PUT/POST a stream of bytes\n * directly to a single URL.\n *\n * **Streaming PUT requires HTTP/2 and `duplex: 'half'`.** This is the default\n * mode and is the most memory-efficient option. If your target only speaks\n * HTTP/1.x, or your runtime does not understand `duplex: 'half'`, set\n * `bufferMode: true` to buffer the whole source into a `Blob` before sending.\n */\nexport interface SimpleHttpUploadOptions {\n url: string\n method?: \"PUT\" | \"POST\"\n headers?: Record<string, string>\n signal?: AbortSignal\n /**\n * When `true`, drains the entire source stream into a `Blob` before issuing\n * the PUT/POST request. The request is then a normal buffered upload (no\n * `duplex: 'half'` required, works on HTTP/1.x).\n *\n * **Memory usage equals the source size — DO NOT enable for files larger\n * than available memory.** Use only when streaming PUT isn't supported\n * (HTTP/1.x, environments where `duplex: 'half'` is unavailable).\n *\n * Default: `false` (streaming with `duplex: 'half'`, requires HTTP/2).\n */\n bufferMode?: boolean\n}\n\nexport function simpleHttpUpload(options: SimpleHttpUploadOptions): {\n upload: (stream: ReadableStream<Uint8Array>) => Promise<void>\n} {\n const { url, method = \"PUT\", headers, signal, bufferMode = false } = options\n\n const upload = async (stream: ReadableStream<Uint8Array>): Promise<void> => {\n let response: Response\n try {\n if (bufferMode) {\n const reader = stream.getReader()\n const chunks: Uint8Array[] = []\n try {\n // Manual drain loop with per-iteration abort check: `Response#blob()`\n // ignores AbortSignal, so we cannot rely on `new Response(stream).blob()`.\n while (true) {\n if (signal?.aborted) {\n throw new AbortError()\n }\n const { done, value } = await reader.read()\n if (done) break\n if (value) chunks.push(value)\n }\n } finally {\n reader.releaseLock()\n }\n const blob = new Blob(chunks as BlobPart[])\n response = await fetch(url, {\n method,\n headers,\n body: blob,\n signal,\n })\n } else {\n // duplex: 'half' is not in lib.dom.d.ts but is required by modern\n // browsers/Node 22+ to accept a ReadableStream as a fetch body.\n response = await fetch(url, {\n method,\n headers,\n body: stream as unknown as BodyInit,\n signal,\n duplex: \"half\",\n } as RequestInit & { duplex: \"half\" })\n }\n } catch (cause) {\n if (cause instanceof AbortError) {\n throw cause\n }\n if (cause instanceof Error && cause.name === \"AbortError\") {\n throw new AbortError()\n }\n throw new CompleteUploadError(cause)\n }\n if (!response.ok) {\n throw new CompleteUploadError(\n new Error(`HTTP ${response.status} ${response.statusText}`)\n )\n }\n }\n\n return { upload }\n}\n"],"mappings":";;AA8BA,SAAgB,iBAAiB,SAE/B;CACA,MAAM,EAAE,KAAK,SAAS,OAAO,SAAS,QAAQ,aAAa,UAAU;CAErE,MAAM,SAAS,OAAO,WAAsD;EAC1E,IAAI;AACJ,MAAI;AACF,OAAI,YAAY;IACd,MAAM,SAAS,OAAO,WAAW;IACjC,MAAM,SAAuB,EAAE;AAC/B,QAAI;AAGF,YAAO,MAAM;AACX,UAAI,QAAQ,QACV,OAAM,IAAI,YAAY;MAExB,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,UAAI,KAAM;AACV,UAAI,MAAO,QAAO,KAAK,MAAM;;cAEvB;AACR,YAAO,aAAa;;IAEtB,MAAM,OAAO,IAAI,KAAK,OAAqB;AAC3C,eAAW,MAAM,MAAM,KAAK;KAC1B;KACA;KACA,MAAM;KACN;KACD,CAAC;SAIF,YAAW,MAAM,MAAM,KAAK;IAC1B;IACA;IACA,MAAM;IACN;IACA,QAAQ;IACT,CAAqC;WAEjC,OAAO;AACd,OAAI,iBAAiB,WACnB,OAAM;AAER,OAAI,iBAAiB,SAAS,MAAM,SAAS,aAC3C,OAAM,IAAI,YAAY;AAExB,SAAM,IAAI,oBAAoB,MAAM;;AAEtC,MAAI,CAAC,SAAS,GACZ,OAAM,IAAI,oCACR,IAAI,MAAM,QAAQ,SAAS,OAAO,GAAG,SAAS,aAAa,CAC5D;;AAIL,QAAO,EAAE,QAAQ"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tranquilload/adapters",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Adapters for Tranquilload (S3, File, Node, HTTP)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"effect": ">=3.19.19",
|
|
56
|
-
"@tranquilload/core": "^0.1.
|
|
56
|
+
"@tranquilload/core": "^0.1.1"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/node": "^25.5.0",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"tsdown": "^0.21.0",
|
|
63
63
|
"typescript": "^5.5.0",
|
|
64
64
|
"vitest": "^3.2.0",
|
|
65
|
-
"@tranquilload/core": "0.1.
|
|
65
|
+
"@tranquilload/core": "0.1.1"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"build": "tsdown",
|
|
@@ -72,4 +72,92 @@ describe("simpleHttpUpload", () => {
|
|
|
72
72
|
expect(error).toBeInstanceOf(CompleteUploadError)
|
|
73
73
|
expect(error.cause).toBe(networkError)
|
|
74
74
|
})
|
|
75
|
+
|
|
76
|
+
it("passes duplex: 'half' on streaming uploads (default)", async () => {
|
|
77
|
+
const stream = new ReadableStream<Uint8Array>()
|
|
78
|
+
const fetchMock = vi.fn().mockResolvedValue({ ok: true })
|
|
79
|
+
vi.stubGlobal("fetch", fetchMock)
|
|
80
|
+
|
|
81
|
+
const adapter = simpleHttpUpload({ url: "https://example.com/upload" })
|
|
82
|
+
await adapter.upload(stream)
|
|
83
|
+
|
|
84
|
+
const [, init] = fetchMock.mock.calls[0]!
|
|
85
|
+
expect(init.duplex).toBe("half")
|
|
86
|
+
expect(init.body).toBe(stream)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("buffers the stream into a Blob when bufferMode is true", async () => {
|
|
90
|
+
const chunkA = new Uint8Array([1, 2, 3])
|
|
91
|
+
const chunkB = new Uint8Array([4, 5])
|
|
92
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
93
|
+
start(c) {
|
|
94
|
+
c.enqueue(chunkA)
|
|
95
|
+
c.enqueue(chunkB)
|
|
96
|
+
c.close()
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
const fetchMock = vi.fn().mockResolvedValue({ ok: true })
|
|
100
|
+
vi.stubGlobal("fetch", fetchMock)
|
|
101
|
+
|
|
102
|
+
const adapter = simpleHttpUpload({
|
|
103
|
+
url: "https://example.com/upload",
|
|
104
|
+
bufferMode: true,
|
|
105
|
+
})
|
|
106
|
+
await adapter.upload(stream)
|
|
107
|
+
|
|
108
|
+
const [, init] = fetchMock.mock.calls[0]!
|
|
109
|
+
expect(init.body).toBeInstanceOf(Blob)
|
|
110
|
+
expect((init.body as Blob).size).toBe(chunkA.length + chunkB.length)
|
|
111
|
+
expect(init.duplex).toBeUndefined()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("rejects with CompleteUploadError on mid-stream read errors when bufferMode is true", async () => {
|
|
115
|
+
const readError = new Error("source read failed")
|
|
116
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
117
|
+
start(c) {
|
|
118
|
+
c.enqueue(new Uint8Array([1, 2, 3]))
|
|
119
|
+
},
|
|
120
|
+
pull(c) {
|
|
121
|
+
c.error(readError)
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
const fetchMock = vi.fn().mockResolvedValue({ ok: true })
|
|
125
|
+
vi.stubGlobal("fetch", fetchMock)
|
|
126
|
+
|
|
127
|
+
const adapter = simpleHttpUpload({
|
|
128
|
+
url: "https://example.com/upload",
|
|
129
|
+
bufferMode: true,
|
|
130
|
+
})
|
|
131
|
+
const error = await adapter.upload(stream).catch((e) => e)
|
|
132
|
+
expect(error).toBeInstanceOf(CompleteUploadError)
|
|
133
|
+
expect((error as CompleteUploadError).cause).toBe(readError)
|
|
134
|
+
expect(fetchMock).not.toHaveBeenCalled()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it("rejects with AbortError when signal aborts during bufferMode drain", async () => {
|
|
138
|
+
const controller = new AbortController()
|
|
139
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
140
|
+
start(c) {
|
|
141
|
+
c.enqueue(new Uint8Array([1, 2, 3]))
|
|
142
|
+
},
|
|
143
|
+
pull(c) {
|
|
144
|
+
// After the first chunk is consumed, abort before the next read resolves.
|
|
145
|
+
controller.abort()
|
|
146
|
+
// Then leave the stream pending so the loop checks `signal.aborted` first.
|
|
147
|
+
setTimeout(() => c.enqueue(new Uint8Array([4, 5])), 10)
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
const fetchMock = vi.fn().mockResolvedValue({ ok: true })
|
|
151
|
+
vi.stubGlobal("fetch", fetchMock)
|
|
152
|
+
|
|
153
|
+
const adapter = simpleHttpUpload({
|
|
154
|
+
url: "https://example.com/upload",
|
|
155
|
+
bufferMode: true,
|
|
156
|
+
signal: controller.signal,
|
|
157
|
+
})
|
|
158
|
+
const error = await adapter.upload(stream).catch((e) => e)
|
|
159
|
+
expect(error).toBeInstanceOf(AbortError)
|
|
160
|
+
expect(error).not.toBeInstanceOf(CompleteUploadError)
|
|
161
|
+
expect(fetchMock).not.toHaveBeenCalled()
|
|
162
|
+
})
|
|
75
163
|
})
|
|
@@ -1,27 +1,80 @@
|
|
|
1
1
|
import { AbortError, CompleteUploadError } from "@tranquilload/core/errors"
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Adapter for the simplest possible HTTP upload: PUT/POST a stream of bytes
|
|
5
|
+
* directly to a single URL.
|
|
6
|
+
*
|
|
7
|
+
* **Streaming PUT requires HTTP/2 and `duplex: 'half'`.** This is the default
|
|
8
|
+
* mode and is the most memory-efficient option. If your target only speaks
|
|
9
|
+
* HTTP/1.x, or your runtime does not understand `duplex: 'half'`, set
|
|
10
|
+
* `bufferMode: true` to buffer the whole source into a `Blob` before sending.
|
|
11
|
+
*/
|
|
3
12
|
export interface SimpleHttpUploadOptions {
|
|
4
13
|
url: string
|
|
5
14
|
method?: "PUT" | "POST"
|
|
6
15
|
headers?: Record<string, string>
|
|
7
16
|
signal?: AbortSignal
|
|
17
|
+
/**
|
|
18
|
+
* When `true`, drains the entire source stream into a `Blob` before issuing
|
|
19
|
+
* the PUT/POST request. The request is then a normal buffered upload (no
|
|
20
|
+
* `duplex: 'half'` required, works on HTTP/1.x).
|
|
21
|
+
*
|
|
22
|
+
* **Memory usage equals the source size — DO NOT enable for files larger
|
|
23
|
+
* than available memory.** Use only when streaming PUT isn't supported
|
|
24
|
+
* (HTTP/1.x, environments where `duplex: 'half'` is unavailable).
|
|
25
|
+
*
|
|
26
|
+
* Default: `false` (streaming with `duplex: 'half'`, requires HTTP/2).
|
|
27
|
+
*/
|
|
28
|
+
bufferMode?: boolean
|
|
8
29
|
}
|
|
9
30
|
|
|
10
31
|
export function simpleHttpUpload(options: SimpleHttpUploadOptions): {
|
|
11
32
|
upload: (stream: ReadableStream<Uint8Array>) => Promise<void>
|
|
12
33
|
} {
|
|
13
|
-
const { url, method = "PUT", headers, signal } = options
|
|
34
|
+
const { url, method = "PUT", headers, signal, bufferMode = false } = options
|
|
14
35
|
|
|
15
36
|
const upload = async (stream: ReadableStream<Uint8Array>): Promise<void> => {
|
|
16
37
|
let response: Response
|
|
17
38
|
try {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
39
|
+
if (bufferMode) {
|
|
40
|
+
const reader = stream.getReader()
|
|
41
|
+
const chunks: Uint8Array[] = []
|
|
42
|
+
try {
|
|
43
|
+
// Manual drain loop with per-iteration abort check: `Response#blob()`
|
|
44
|
+
// ignores AbortSignal, so we cannot rely on `new Response(stream).blob()`.
|
|
45
|
+
while (true) {
|
|
46
|
+
if (signal?.aborted) {
|
|
47
|
+
throw new AbortError()
|
|
48
|
+
}
|
|
49
|
+
const { done, value } = await reader.read()
|
|
50
|
+
if (done) break
|
|
51
|
+
if (value) chunks.push(value)
|
|
52
|
+
}
|
|
53
|
+
} finally {
|
|
54
|
+
reader.releaseLock()
|
|
55
|
+
}
|
|
56
|
+
const blob = new Blob(chunks as BlobPart[])
|
|
57
|
+
response = await fetch(url, {
|
|
58
|
+
method,
|
|
59
|
+
headers,
|
|
60
|
+
body: blob,
|
|
61
|
+
signal,
|
|
62
|
+
})
|
|
63
|
+
} else {
|
|
64
|
+
// duplex: 'half' is not in lib.dom.d.ts but is required by modern
|
|
65
|
+
// browsers/Node 22+ to accept a ReadableStream as a fetch body.
|
|
66
|
+
response = await fetch(url, {
|
|
67
|
+
method,
|
|
68
|
+
headers,
|
|
69
|
+
body: stream as unknown as BodyInit,
|
|
70
|
+
signal,
|
|
71
|
+
duplex: "half",
|
|
72
|
+
} as RequestInit & { duplex: "half" })
|
|
73
|
+
}
|
|
24
74
|
} catch (cause) {
|
|
75
|
+
if (cause instanceof AbortError) {
|
|
76
|
+
throw cause
|
|
77
|
+
}
|
|
25
78
|
if (cause instanceof Error && cause.name === "AbortError") {
|
|
26
79
|
throw new AbortError()
|
|
27
80
|
}
|