Skip to content

Commit

Permalink
v0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
fzn0x committed May 14, 2024
1 parent 954ba8c commit cbf34af
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 58 deletions.
36 changes: 36 additions & 0 deletions deno_dist/Hyperfetch.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import hypf from "node:hypf";
import fs from "node:node:fs";

describe("Hyperfetch", () => {
// TODO: create a local mock server for typicode
const hypfRequest = hypf.createRequest(
"https://jsonplaceholder.typicode.com"
);

const hypfRequest2 = hypf.createRequest("http://localhost:3001");

it("GET", async () => {
const [getErr, getData] = await hypfRequest.get<
Array<{
Expand Down Expand Up @@ -44,6 +47,39 @@ describe("Hyperfetch", () => {
}
});

it("POST:upload", async () => {
const formdata = new FormData();
const chunks: BlobPart[] = [];

const stream = fs.createReadStream(
"C:/Users/User/Downloads/Screenshot 2024-05-14 060852.png"
);

// Create a promise to handle the stream end event
const streamEndPromise = new Promise((resolve, reject) => {
stream.on("data", (chunk) => chunks.push(chunk));
stream.once("end", resolve);
stream.once("error", reject);
});

// Wait for the stream to finish
await streamEndPromise;

// Convert chunks to Blob
const blob = new Blob(chunks);
formdata.append("image[]", blob);

const [postErr, postData] = await hypfRequest2.post("/api/upload-file", {
body: formdata,
});

if (postErr) {
console.error("POST Error:", postErr);
} else {
console.log("POST Data:", postData);
}
});

it("PUT", async () => {
const [putErr, putData] = await hypfRequest.put(
"/posts/1",
Expand Down
109 changes: 60 additions & 49 deletions deno_dist/Hyperfetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from "./constant.ts";

import { appendParams } from "./utils/append-params.ts";
import { createHTTPError } from "./utils/create-http-error.ts";

const defaultBackoff = (retryCount: number, factor: number) =>
Math.pow(2, retryCount) * 1000 * factor; // Exponential backoff, starting from 1 second
Expand Down Expand Up @@ -46,34 +47,30 @@ function createRequest(
options = {},
data
): Promise<[Error | null, null]> => {
const {
method = "GET",
retries = 0,
backoff = defaultBackoff,
jitter = false,
jitterFactor = DEFAULT_JITTER_FACTOR,
backoffFactor = DEFAULT_BACKOFF_FACTOR,
timeout = DEFAULT_MAX_TIMEOUT,
retryOnTimeout = false,
params,
headers = {},
signal,
...otherOptions
} = options;

try {
// Execute pre-request hook
if (hooks?.preRequest) {
hooks.preRequest(url, options);
}

const fullUrl = `${baseUrl}${url}`;
const {
method = "GET",
retries = 0,
backoff = defaultBackoff,
jitter = false,
jitterFactor = DEFAULT_JITTER_FACTOR,
backoffFactor = DEFAULT_BACKOFF_FACTOR,
timeout = DEFAULT_MAX_TIMEOUT,
retryOnTimeout = false,
params,
headers = {},
signal,
...otherOptions
} = options;

const reqHeaders = new Headers(options.headers);

// Set default Content-Type to application/json if not provided
if (!reqHeaders.get("Content-Type") && data && typeof data === "object") {
reqHeaders.set("Content-Type", "application/json");
}
const reqHeaders = new Headers(headers);

// Automatically detect and add Content-Length based on payload length
const textEncoder = new TextEncoder();
Expand All @@ -93,6 +90,15 @@ function createRequest(
}
}

// Set default Content-Type to application/json if not provided
if (!reqHeaders.get("Content-Type") && data && typeof data === "object") {
if (
!(((data as { body: FormData })?.body || data) instanceof FormData)
) {
reqHeaders.set("Content-Type", "application/json");
}
}

// Execute pre-timeout hook
if (hooks?.preTimeout) {
hooks.preTimeout(url, options);
Expand Down Expand Up @@ -123,14 +129,7 @@ function createRequest(
// Append params to the URL
const urlWithParams = params ? appendParams(fullUrl, params) : fullUrl;

// Only checks Node.js for duplex compability, as other JS runtimes do full-duplex
// Streams are supported, but they inherently support one-way operations each. Combine them for pseudo full duplex.
if (isReadableStreamSupported && !isWriteableStreamSupported && isNode) {
// The @ts-expect-error directive is used here because we are about to assign a value to a property
// that might not be officially recognized in the TypeScript types definitions for `otherOptions`.
// This tells TypeScript to expect a type error on the next line but to ignore it for compilation.
// This approach is often used when dealing with dynamic properties or when using features that TypeScript
// is not aware of, possibly due to using newer browser APIs or experimental features.
// @ts-expect-error
otherOptions.duplex = "half";
}
Expand All @@ -153,28 +152,42 @@ function createRequest(
otherOptions.duplex = "half";
}

const responsePromise = fetch(urlWithParams, {
const requestBody =
options.body instanceof FormData
? options.body
: data
? JSON.stringify(data)
: undefined;

const requestOptions = {
method,
signal: isAbortControllerSupported ? globalThis.abortSignal : null,
headers,
headers: reqHeaders,
...otherOptions,
body: data ? JSON.stringify(data) : undefined,
});
body: requestBody,
};

if (requestBody instanceof FormData) {
requestOptions.headers.delete("Content-Type");
}

const responsePromise = fetch(urlWithParams, requestOptions);

clearTimeout(timeoutId);

const response = await responsePromise;

if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status}`);
}

const contentType = response.headers.get("content-type");

const responseData =
contentType && contentType.includes("application/json")
? await response.json()
: await response.text();

if (!response.ok) {
throw createHTTPError(response, responseData);
}

// Execute post-request hook
if (hooks?.postRequest) {
hooks.postRequest(url, options, data, [null, responseData]);
Expand All @@ -193,33 +206,31 @@ function createRequest(
if (error.name === "AbortError") {
console.error("Request aborted:", error);
} else if (
options.retryOnTimeout &&
retryOnTimeout &&
error.name === "TimeoutError" &&
options.retries &&
options.retries > 0
retries &&
retries > 0
) {
const delay =
options.jitter && options.jitterFactor
? defaultJitter(options.jitterFactor)
jitter && jitterFactor
? defaultJitter(jitterFactor)
: defaultBackoff(
options.retries,
options.backoffFactor
? options.backoffFactor
: DEFAULT_BACKOFF_FACTOR
retries,
backoffFactor ? backoffFactor : DEFAULT_BACKOFF_FACTOR
);
if (DEBUG) {
console.warn(
`Request timed out. Retrying in ${delay}ms... (Remaining retries: ${options.retries})`
`Request timed out. Retrying in ${delay}ms... (Remaining retries: ${retries})`
);
}
// Execute pre-retry hook
if (hooks?.preRetry) {
hooks.preRetry(url, options, options.retries, options.retries);
hooks.preRetry(url, options, retries, retries);
}
await new Promise((resolve) => setTimeout(resolve, delay));
const [retryErr, retryData] = await request(
url,
{ ...options, retries: options.retries - 1 },
{ ...options, retries: retries - 1 },
data
);
// Execute post-retry hook
Expand All @@ -229,8 +240,8 @@ function createRequest(
options,
data,
[retryErr, retryData],
options.retries,
options.retries - 1
retries,
retries - 1
);
}
return [retryErr, retryData];
Expand Down
37 changes: 34 additions & 3 deletions deno_dist/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img width="55%" src="./assets/hyperfetch.png">
</p>

Creates supertiny and stunning HTTP client for frontend apps. Best frontend wrapper for Fetch API.
Supertiny (4kB minified & 0 dependencies) and strong-typed HTTP client for Deno, Bun, Node.js, Cloudflare Workers and Browsers.

```sh
# Node.js
Expand All @@ -11,7 +11,7 @@ npm install hypf
bun install hypf
```

The idea of this tool is to provide frontend-only lightweight solution for fetching APIs with an easy wrapper:
The idea of this tool is to provide lightweight `fetch` wrapper for Node.js, Bun:

```js
import hypf from "hypf";
Expand All @@ -36,7 +36,38 @@ if (postErr) {
}
```

or browsers
Cloudflare Workers:

```ts
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
const hypfInstance = await hypf.createRequest(
"https://jsonplaceholder.typicode.com"
);

const [getErr, getData] = await hypfInstance.get<
Array<{
userId: number;
id: number;
title: string;
body: string;
}>
>("/posts");

if (getErr) {
console.error("GET Error:", getErr);
}

return Response.json(getData);
},
};
```

and Browsers:

```html
<script src="https://unpkg.com/hypf/dist/hyperfetch-browser.min.js"></script>
Expand Down
14 changes: 14 additions & 0 deletions deno_dist/utils/create-http-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function createHTTPError(response: Response, responseData: Response) {
const code = response.status || response.status === 0 ? response.status : "";
const title = response.statusText || "";
const status = `${code} ${title}`.trim();
const reason = status ? `status code ${status}` : "an unknown error";
const error = new Error(reason);

error.name = "HTTPError";

(error as any).response = response;
(error as any).data = responseData;

return error;
}
2 changes: 1 addition & 1 deletion dist/Hyperfetch.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion dist/Hyperfetch.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/Hyperfetch.js.map

Large diffs are not rendered by default.

Loading

0 comments on commit cbf34af

Please sign in to comment.