From 0b18ce4b3dbb2c8735bdafbd526dcce02691dd7a Mon Sep 17 00:00:00 2001
From: Daniil Bezuglov <xlaystgoku@gmail.com>
Date: Thu, 24 Aug 2023 12:33:42 +0700
Subject: [PATCH 1/2] feat: added progress for request and response with json

---
 src/fetch.ts       | 56 +++++++++++++++++++++++++++++++++++++++++++++-
 test/index.test.ts | 30 +++++++++++++++++++++++++
 2 files changed, 85 insertions(+), 1 deletion(-)

diff --git a/src/fetch.ts b/src/fetch.ts
index 740a9f30..70bacfe9 100644
--- a/src/fetch.ts
+++ b/src/fetch.ts
@@ -66,12 +66,14 @@ export interface FetchOptions<R extends ResponseType = ResponseType>
   onRequestError?(
     context: FetchContext & { error: Error }
   ): Promise<void> | void;
+  onRequestProgress?(progress: number): Promise<void> | void;
   onResponse?(
     context: FetchContext & { response: FetchResponse<R> }
   ): Promise<void> | void;
   onResponseError?(
     context: FetchContext & { response: FetchResponse<R> }
   ): Promise<void> | void;
+  onResponseProgress?(progress: number): Promise<void> | void;
 }
 
 export interface $Fetch {
@@ -196,6 +198,28 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch {
             ? context.options.body
             : JSON.stringify(context.options.body);
 
+        if (context.options.onRequestProgress) {
+          const { readable, writable } = new TransformStream();
+          const _writer = writable.getWriter();
+          const _encoder = new TextEncoder();
+          const contentLength = _encoder.encode(
+            context.options.body
+          ).byteLength;
+          let loaded = 0;
+
+          for (const char of context.options.body) {
+            const chunk = _encoder.encode(char);
+            loaded += chunk.byteLength;
+            _writer.write(chunk);
+            context.options.onRequestProgress(
+              Math.round((loaded / contentLength) * 100)
+            );
+          }
+
+          context.options.body = readable;
+          context.options.duplex = "half";
+        }
+
         // Set Content-Type and Accept headers to application/json by default
         // for JSON serializable request bodies.
         // Pass empty object as older browsers don't support undefined.
@@ -255,7 +279,37 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch {
       // We override the `.json()` method to parse the body more securely with `destr`
       switch (responseType) {
         case "json": {
-          const data = await context.response.text();
+          const data = await (async function () {
+            /* Custom response.text() function to retrieve text from response and get progress values */
+            let loaded = 0;
+            const contentLength =
+              context.response!.headers.get("content-length")!;
+            const _reader = context.response!.body!.getReader();
+            const _decoder = new TextDecoder();
+            const _chunks: string[] = [];
+
+            async function read(): Promise<string> {
+              const { done, value } = await _reader.read();
+
+              if (done) {
+                return _chunks.join("");
+              }
+
+              loaded += value.byteLength;
+
+              if (context.options.onResponseProgress) {
+                context.options.onResponseProgress(
+                  Math.round((loaded / Number.parseInt(contentLength)) * 100)
+                );
+              }
+
+              const chunk = _decoder.decode(value, { stream: true });
+              _chunks.push(chunk);
+              return await read(); // read the next chunk
+            }
+
+            return await read();
+          })();
           const parseFunction = context.options.parseResponse || destr;
           context.response._data = parseFunction(data);
           break;
diff --git a/test/index.test.ts b/test/index.test.ts
index de25338f..9403907d 100644
--- a/test/index.test.ts
+++ b/test/index.test.ts
@@ -323,6 +323,36 @@ describe("ofetch", () => {
     expect(race).to.equal("timeout");
   });
 
+  it("return progress in onResponseProgress", async () => {
+    let loaded = 0;
+    await $fetch(getURL("post"), {
+      method: "post",
+      body: JSON.stringify({
+        key: "test",
+        json: true,
+      }),
+      onResponseProgress: (progress) => {
+        loaded = progress;
+      },
+    });
+    expect(loaded).to.equal(100);
+  });
+
+  it("return progress in onRequestProgress", async () => {
+    let loaded = 0;
+    await $fetch(getURL("post"), {
+      method: "post",
+      body: JSON.stringify({
+        key: "test",
+        json: true,
+      }),
+      onRequestProgress: (progress) => {
+        loaded = progress;
+      },
+    });
+    expect(loaded).to.equal(100);
+  });
+
   it("deep merges defaultOptions", async () => {
     const _customFetch = $fetch.create({
       query: {

From 12c07f1cf9f6241c1af555aad27a006382dc04da Mon Sep 17 00:00:00 2001
From: Pooya Parsa <pooya@pi0.io>
Date: Thu, 26 Oct 2023 20:07:10 +0200
Subject: [PATCH 2/2] fix types

---
 src/fetch.ts | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/fetch.ts b/src/fetch.ts
index 2237c3a8..881c77bd 100644
--- a/src/fetch.ts
+++ b/src/fetch.ts
@@ -209,12 +209,15 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch {
             /* Custom response.text() function to retrieve text from response and get progress values */
             let loaded = 0;
             const contentLength =
-              context.response!.headers.get("content-length")!;
-            const _reader = context.response!.body!.getReader();
+              context.response?.headers.get("content-length") || "0";
+            const _reader = context.response?.body?.getReader();
             const _decoder = new TextDecoder();
             const _chunks: string[] = [];
 
             async function read(): Promise<string> {
+              if (!_reader) {
+                return "";
+              }
               const { done, value } = await _reader.read();
 
               if (done) {