@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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @tranquilload/adapters@0.1.0 build /home/runner/work/Tranquilload/Tranquilload/packages/tranquilload-adapters
2
+ > @tranquilload/adapters@0.1.1 build /home/runner/work/Tranquilload/Tranquilload/packages/tranquilload-adapters
3
3
  > tsdown
4
4
 
5
5
  ℹ tsdown v0.21.0 powered by rolldown v1.0.0-rc.7
@@ -8,55 +8,61 @@
8
8
  ℹ tsconfig: tsconfig.json
9
9
  ℹ Build start
10
10
  ℹ [CJS] dist/s3-multipart-upload.cjs 2.15 kB │ gzip: 0.90 kB
11
- ℹ [CJS] dist/simple-http-upload.cjs 0.93 kB │ gzip: 0.49 kB
11
+ ℹ [CJS] dist/simple-http-upload.cjs 1.54 kB │ gzip: 0.69 kB
12
12
  ℹ [CJS] dist/network-multiplier.cjs 0.92 kB │ gzip: 0.49 kB
13
13
  ℹ [CJS] dist/optimal-part-size.cjs 0.59 kB │ gzip: 0.32 kB
14
14
  ℹ [CJS] dist/from-node-readable.cjs 0.35 kB │ gzip: 0.23 kB
15
15
  ℹ [CJS] dist/from-file.cjs 0.28 kB │ gzip: 0.22 kB
16
16
  ℹ [CJS] dist/s3-multipart-upload.cjs.map 4.38 kB │ gzip: 1.64 kB
17
+ ℹ [CJS] dist/simple-http-upload.cjs.map 4.18 kB │ gzip: 1.80 kB
17
18
  ℹ [CJS] dist/network-multiplier.cjs.map 2.21 kB │ gzip: 1.00 kB
18
- ℹ [CJS] dist/simple-http-upload.cjs.map 1.65 kB │ gzip: 0.80 kB
19
19
  ℹ [CJS] dist/optimal-part-size.cjs.map 1.45 kB │ gzip: 0.71 kB
20
20
  ℹ [CJS] dist/from-node-readable.cjs.map 0.41 kB │ gzip: 0.26 kB
21
21
  ℹ [CJS] dist/from-file.cjs.map 0.38 kB │ gzip: 0.27 kB
22
- ℹ [CJS] 12 files, total: 15.69 kB
22
+ ℹ [CJS] 12 files, total: 18.83 kB
23
+ [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugin `tsdown:report`. See https://rolldown.rs/options/checks#plugintimings for more details.
24
+
23
25
  ℹ [CJS] dist/s3-multipart-upload.d.cts.map 0.69 kB │ gzip: 0.33 kB
26
+ ℹ [CJS] dist/simple-http-upload.d.cts.map 0.34 kB │ gzip: 0.21 kB
24
27
  ℹ [CJS] dist/network-multiplier.d.cts.map 0.31 kB │ gzip: 0.22 kB
25
- ℹ [CJS] dist/simple-http-upload.d.cts.map 0.30 kB │ gzip: 0.20 kB
26
28
  ℹ [CJS] dist/optimal-part-size.d.cts.map 0.25 kB │ gzip: 0.19 kB
27
29
  ℹ [CJS] dist/from-file.d.cts.map 0.18 kB │ gzip: 0.15 kB
28
30
  ℹ [CJS] dist/from-node-readable.d.cts.map 0.18 kB │ gzip: 0.15 kB
31
+ ℹ [CJS] dist/simple-http-upload.d.cts 1.41 kB │ gzip: 0.74 kB
29
32
  ℹ [CJS] dist/s3-multipart-upload.d.cts 1.20 kB │ gzip: 0.49 kB
30
33
  ℹ [CJS] dist/network-multiplier.d.cts 0.97 kB │ gzip: 0.49 kB
31
34
  ℹ [CJS] dist/optimal-part-size.d.cts 0.87 kB │ gzip: 0.46 kB
32
- ℹ [CJS] dist/simple-http-upload.d.cts 0.44 kB │ gzip: 0.28 kB
33
35
  ℹ [CJS] dist/from-node-readable.d.cts 0.26 kB │ gzip: 0.18 kB
34
36
  ℹ [CJS] dist/from-file.d.cts 0.21 kB │ gzip: 0.18 kB
35
- ℹ [CJS] 12 files, total: 5.86 kB
36
- ✔ Build complete in 2822ms
37
+ ℹ [CJS] 12 files, total: 6.87 kB
38
+ [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
39
+
40
+ ✔ Build complete in 3501ms
37
41
  ℹ [ESM] dist/s3-multipart-upload.mjs 1.96 kB │ gzip: 0.84 kB
42
+ ℹ [ESM] dist/simple-http-upload.mjs 1.33 kB │ gzip: 0.62 kB
38
43
  ℹ [ESM] dist/network-multiplier.mjs 0.83 kB │ gzip: 0.44 kB
39
- ℹ [ESM] dist/simple-http-upload.mjs 0.77 kB │ gzip: 0.43 kB
40
44
  ℹ [ESM] dist/optimal-part-size.mjs 0.49 kB │ gzip: 0.27 kB
41
45
  ℹ [ESM] dist/from-node-readable.mjs 0.25 kB │ gzip: 0.17 kB
42
46
  ℹ [ESM] dist/from-file.mjs 0.20 kB │ gzip: 0.17 kB
43
47
  ℹ [ESM] dist/s3-multipart-upload.mjs.map 4.29 kB │ gzip: 1.62 kB
48
+ ℹ [ESM] dist/simple-http-upload.mjs.map 4.10 kB │ gzip: 1.78 kB
44
49
  ℹ [ESM] dist/network-multiplier.mjs.map 2.21 kB │ gzip: 1.00 kB
45
- ℹ [ESM] dist/simple-http-upload.mjs.map 1.59 kB │ gzip: 0.78 kB
46
50
  ℹ [ESM] dist/optimal-part-size.mjs.map 1.44 kB │ gzip: 0.71 kB
47
51
  ℹ [ESM] dist/s3-multipart-upload.d.mts.map 0.69 kB │ gzip: 0.33 kB
48
52
  ℹ [ESM] dist/from-node-readable.mjs.map 0.39 kB │ gzip: 0.25 kB
49
53
  ℹ [ESM] dist/from-file.mjs.map 0.38 kB │ gzip: 0.27 kB
54
+ ℹ [ESM] dist/simple-http-upload.d.mts.map 0.34 kB │ gzip: 0.21 kB
50
55
  ℹ [ESM] dist/network-multiplier.d.mts.map 0.31 kB │ gzip: 0.22 kB
51
- ℹ [ESM] dist/simple-http-upload.d.mts.map 0.30 kB │ gzip: 0.20 kB
52
56
  ℹ [ESM] dist/optimal-part-size.d.mts.map 0.25 kB │ gzip: 0.19 kB
53
57
  ℹ [ESM] dist/from-file.d.mts.map 0.18 kB │ gzip: 0.15 kB
54
58
  ℹ [ESM] dist/from-node-readable.d.mts.map 0.18 kB │ gzip: 0.15 kB
59
+ ℹ [ESM] dist/simple-http-upload.d.mts 1.41 kB │ gzip: 0.74 kB
55
60
  ℹ [ESM] dist/s3-multipart-upload.d.mts 1.20 kB │ gzip: 0.49 kB
56
61
  ℹ [ESM] dist/network-multiplier.d.mts 0.97 kB │ gzip: 0.49 kB
57
62
  ℹ [ESM] dist/optimal-part-size.d.mts 0.87 kB │ gzip: 0.46 kB
58
- ℹ [ESM] dist/simple-http-upload.d.mts 0.44 kB │ gzip: 0.28 kB
59
63
  ℹ [ESM] dist/from-node-readable.d.mts 0.26 kB │ gzip: 0.18 kB
60
64
  ℹ [ESM] dist/from-file.d.mts 0.21 kB │ gzip: 0.18 kB
61
- ℹ [ESM] 24 files, total: 20.66 kB
62
- ✔ Build complete in 2826ms
65
+ ℹ [ESM] 24 files, total: 24.74 kB
66
+ [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
67
+
68
+ ✔ Build complete in 3514ms
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
- response = await fetch(url, {
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 method,\n headers,\n body: stream as unknown as BodyInit,\n signal,\n })\n } catch (cause) {\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":";;;AASA,SAAgB,iBAAiB,SAE/B;CACA,MAAM,EAAE,KAAK,SAAS,OAAO,SAAS,WAAW;CAEjD,MAAM,SAAS,OAAO,WAAsD;EAC1E,IAAI;AACJ,MAAI;AACF,cAAW,MAAM,MAAM,KAAK;IAC1B;IACA;IACA,MAAM;IACN;IACD,CAAC;WACK,OAAO;AACd,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
+ {"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":";UAEiB,uBAAA;EACf,GAAA;EACA,MAAA;EACA,OAAA,GAAU,MAAA;EACV,MAAA,GAAS,WAAA;AAAA;AAAA,iBAGK,gBAAA,CAAiB,OAAA,EAAS,uBAAA;EACxC,MAAA,GAAS,MAAA,EAAQ,cAAA,CAAe,UAAA,MAAgB,OAAA;AAAA"}
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":";UAEiB,uBAAA;EACf,GAAA;EACA,MAAA;EACA,OAAA,GAAU,MAAA;EACV,MAAA,GAAS,WAAA;AAAA;AAAA,iBAGK,gBAAA,CAAiB,OAAA,EAAS,uBAAA;EACxC,MAAA,GAAS,MAAA,EAAQ,cAAA,CAAe,UAAA,MAAgB,OAAA;AAAA"}
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
- response = await fetch(url, {
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 method,\n headers,\n body: stream as unknown as BodyInit,\n signal,\n })\n } catch (cause) {\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":";;AASA,SAAgB,iBAAiB,SAE/B;CACA,MAAM,EAAE,KAAK,SAAS,OAAO,SAAS,WAAW;CAEjD,MAAM,SAAS,OAAO,WAAsD;EAC1E,IAAI;AACJ,MAAI;AACF,cAAW,MAAM,MAAM,KAAK;IAC1B;IACA;IACA,MAAM;IACN;IACD,CAAC;WACK,OAAO;AACd,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"}
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.0",
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.0"
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.0"
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
- response = await fetch(url, {
19
- method,
20
- headers,
21
- body: stream as unknown as BodyInit,
22
- signal,
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
  }