Skip to content

Commit a64f95b

Browse files
committed
Final: Library/package working
1 parent 1da797d commit a64f95b

File tree

11 files changed

+183
-109
lines changed

11 files changed

+183
-109
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ fastFetch("https://pokeapi.co/api/v2/pokemon/ditto", {
9393
}
9494
// For network errors or other errors, retry
9595
return true;
96-
}
96+
},
9797
})
9898
.then((res) => res.json())
9999
.then((data) => console.log("FastFetch data:", data))
@@ -114,7 +114,8 @@ const api = axios.create({ baseURL: "https://pokeapi.co/api/v2" });
114114
// Register the Axios instance with FastFetch
115115
registerAxios(api);
116116

117-
api.get("/pokemon/ditto")
117+
api
118+
.get("/pokemon/ditto")
118119
.then((response) => {
119120
console.log("Axios fetched data:", response.data);
120121
})
@@ -128,6 +129,7 @@ api.get("/pokemon/ditto")
128129
### `fastFetch(input: RequestInfo, init?: RequestInit & FastFetchOptions): Promise<Response>`
129130

130131
- **Parameters:**
132+
131133
- `input`: URL or Request object.
132134
- `init`: An object that extends standard `RequestInit` with additional options:
133135
- `retries`: _number_ — Number of retry attempts (default: `0`).
@@ -171,7 +173,7 @@ fastFetch("https://example.com/api/data", {
171173
return errorOrResponse.status >= 500;
172174
}
173175
return true;
174-
}
176+
},
175177
})
176178
.then((res) => res.json())
177179
.then((data) => console.log("Custom retry data:", data))
@@ -203,6 +205,7 @@ Promise.all([fastFetch(url), fastFetch(url)])
203205
FastFetch comes with a Jest test suite. To run tests:
204206

205207
1. **Install dependencies:**
208+
206209
```bash
207210
npm install
208211
```

__tests__/demo.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,22 @@ fastFetch("https://pokeapi.co/api/v2/pokemon/ditto", {
1414

1515
// If it's a network or other error
1616
return true;
17-
}
17+
},
1818
})
19-
.then(res => res.json())
20-
.then(data => {
19+
.then((res) => res.json())
20+
.then((data) => {
2121
console.log("FastFetch data:", data);
2222
})
23-
.catch(err => {
23+
.catch((err) => {
2424
console.error("FastFetch error:", err);
2525
});
2626

2727
// Should output something like:
28+
// [FastFetch] Starting request for: https://pokeapi.co/api/v2/pokemon/ditto
29+
// [FastFetch] Attempt #1 for: https://pokeapi.co/api/v2/pokemon/ditto
30+
// [FastFetch] Stored in-flight request with signature: {"url":"https://pokeapi.co/api/v2/pokemon/ditto","method":"GET","headers":{},"body":null}
31+
// [FastFetch] Request succeeded on attempt #1
32+
// [FastFetch] Removed in-flight record for signature: {"url":"https://pokeapi.co/api/v2/pokemon/ditto","method":"GET","headers":{},"body":null}
2833
// FastFetch data: {
2934
// abilities: [
3035
// { ability: [Object], is_hidden: false, slot: 1 },

__tests__/demo.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe("FastFetch Basic Tests", () => {
1616

1717
const [res1, res2] = await Promise.all([
1818
fastFetch(POKE_API),
19-
fastFetch(POKE_API)
19+
fastFetch(POKE_API),
2020
]);
2121
const time = Date.now() - start;
2222

dist/fastFetch.d.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
/**
22
* Options for controlling FastFetch behavior:
33
* - `retries` -> number of times to retry on failure
4-
* - `retryDelay` -> wait in ms before next retry
4+
* - `retryDelay` -> base delay in ms before next retry (will be multiplied exponentially)
55
* - `deduplicate` -> whether to merge identical requests in-flight
66
* - `shouldRetry` -> custom logic to decide if we retry
77
*/
88
export interface FastFetchOptions {
9-
retries?: number;
10-
retryDelay?: number;
11-
deduplicate?: boolean;
12-
shouldRetry?: (error: any, attempt: number) => boolean;
9+
retries?: number;
10+
retryDelay?: number;
11+
deduplicate?: boolean;
12+
shouldRetry?: (error: any, attempt: number) => boolean;
1313
}
1414
/**
1515
* FastFetch main function
16-
* - Retries on error
16+
* - Retries on error with exponential backoff
1717
* - Deduplicates in-flight requests if deduplicate = true
1818
*/
19-
export declare function fastFetch(input: RequestInfo, init?: RequestInit & FastFetchOptions): Promise<Response>;
20-
//# sourceMappingURL=fastFetch.d.ts.map
19+
export declare function fastFetch(
20+
input: RequestInfo,
21+
init?: RequestInit & FastFetchOptions,
22+
): Promise<Response>;
23+
//# sourceMappingURL=fastFetch.d.ts.map

dist/fastFetch.d.ts.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/fastFetch.js

Lines changed: 98 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -8,86 +8,117 @@ const inFlightMap = new Map();
88
* Generate a signature for dedup if deduplicate is true
99
*/
1010
function makeSignature(input, init) {
11-
// Basic approach:
12-
const normalized = {
13-
url: typeof input === "string" ? input : input.url,
14-
method: init?.method ?? "GET",
15-
headers: init?.headers ?? {},
16-
body: init?.body ?? null
17-
};
18-
return JSON.stringify(normalized);
11+
// Basic approach:
12+
const normalized = {
13+
url: typeof input === "string" ? input : input.url,
14+
method: init?.method ?? "GET",
15+
headers: init?.headers ?? {},
16+
body: init?.body ?? null,
17+
};
18+
return JSON.stringify(normalized);
1919
}
2020
/**
2121
* Sleep helper for retryDelay
2222
*/
2323
function sleep(ms) {
24-
return new Promise(resolve => setTimeout(resolve, ms));
24+
return new Promise((resolve) => setTimeout(resolve, ms));
2525
}
2626
/**
2727
* FastFetch main function
28-
* - Retries on error
28+
* - Retries on error with exponential backoff
2929
* - Deduplicates in-flight requests if deduplicate = true
3030
*/
3131
export async function fastFetch(input, init) {
32-
const { retries = 0, retryDelay = 1000, deduplicate = true, shouldRetry } = init || {};
33-
// If deduplicating, check if a matching in-flight request exists
34-
let signature = "";
35-
if (deduplicate) {
36-
signature = makeSignature(input, init);
37-
if (inFlightMap.has(signature)) {
38-
return inFlightMap.get(signature);
39-
}
32+
const {
33+
retries = 0,
34+
retryDelay = 1000,
35+
deduplicate = true,
36+
shouldRetry,
37+
} = init || {};
38+
console.log("[FastFetch] Starting request for:", input);
39+
// If deduplicating, check if a matching in-flight request exists
40+
let signature = "";
41+
if (deduplicate) {
42+
signature = makeSignature(input, init);
43+
if (inFlightMap.has(signature)) {
44+
console.log(
45+
"[FastFetch] Found in-flight request for signature:",
46+
signature,
47+
);
48+
return inFlightMap.get(signature);
4049
}
41-
// We'll build a promise chain that tries up to `retries + 1` times
42-
let attempt = 0;
43-
let promise = (async function fetchWithRetry() {
44-
while (true) {
45-
try {
46-
attempt++;
47-
const response = await fetch(input, init);
48-
// If success status or we don't want to parse error, just return it
49-
if (!response.ok && shouldRetry) {
50-
// If user-defined logic says "retry again," we do so
51-
const doRetry = shouldRetry(response, attempt);
52-
if (doRetry && attempt <= retries) {
53-
await sleep(retryDelay);
54-
continue; // try again
55-
}
56-
}
57-
return response;
58-
}
59-
catch (error) {
60-
// If fetch truly fails (like network error), see if we can retry
61-
if (shouldRetry) {
62-
const doRetry = shouldRetry(error, attempt);
63-
if (doRetry && attempt <= retries) {
64-
await sleep(retryDelay);
65-
continue;
66-
}
67-
}
68-
else {
69-
// By default, if not a 2xx response or network error, we only retry up to `retries`
70-
if (attempt <= retries) {
71-
await sleep(retryDelay);
72-
continue;
73-
}
74-
}
75-
throw error;
76-
}
50+
}
51+
// Build a promise chain that tries up to retries + 1 times with exponential backoff
52+
let attempt = 0;
53+
let promise = (async function fetchWithRetry() {
54+
while (true) {
55+
try {
56+
attempt++;
57+
console.log(`[FastFetch] Attempt #${attempt} for:`, input);
58+
const response = await fetch(input, init);
59+
if (!response.ok && shouldRetry) {
60+
const doRetry = shouldRetry(response, attempt);
61+
console.log(
62+
`[FastFetch] Response not ok (status: ${response.status}). Retry decision: ${doRetry}`,
63+
);
64+
if (doRetry && attempt <= retries) {
65+
const delay = retryDelay * Math.pow(2, attempt - 1);
66+
console.log(
67+
`[FastFetch] Waiting ${delay}ms (exponential backoff) before retrying...`,
68+
);
69+
await sleep(delay);
70+
continue; // try again
71+
}
7772
}
78-
})();
79-
// If deduplicating, store in the map so subsequent calls get the same promise
80-
if (deduplicate) {
81-
inFlightMap.set(signature, promise);
82-
}
83-
try {
84-
const result = await promise;
85-
return result;
86-
}
87-
finally {
88-
// Once done (success or fail), remove from inFlightMap
89-
if (deduplicate) {
90-
inFlightMap.delete(signature);
73+
console.log(`[FastFetch] Request succeeded on attempt #${attempt}`);
74+
return response;
75+
} catch (error) {
76+
console.log(`[FastFetch] Caught error on attempt #${attempt}:`, error);
77+
if (shouldRetry) {
78+
const doRetry = shouldRetry(error, attempt);
79+
console.log(`[FastFetch] Retry decision based on error: ${doRetry}`);
80+
if (doRetry && attempt <= retries) {
81+
const delay = retryDelay * Math.pow(2, attempt - 1);
82+
console.log(
83+
`[FastFetch] Waiting ${delay}ms (exponential backoff) before retrying after error...`,
84+
);
85+
await sleep(delay);
86+
continue;
87+
}
88+
} else {
89+
if (attempt <= retries) {
90+
const delay = retryDelay * Math.pow(2, attempt - 1);
91+
console.log(
92+
`[FastFetch] Retrying attempt #${attempt} after error. Waiting ${delay}ms...`,
93+
);
94+
await sleep(delay);
95+
continue;
96+
}
9197
}
98+
console.log("[FastFetch] No more retries. Throwing error.");
99+
throw error;
100+
}
101+
}
102+
})();
103+
// If deduplicating, store in the map so subsequent calls get the same promise
104+
if (deduplicate) {
105+
inFlightMap.set(signature, promise);
106+
console.log(
107+
"[FastFetch] Stored in-flight request with signature:",
108+
signature,
109+
);
110+
}
111+
try {
112+
const result = await promise;
113+
return result;
114+
} finally {
115+
// Once done (success or fail), remove from inFlightMap
116+
if (deduplicate) {
117+
inFlightMap.delete(signature);
118+
console.log(
119+
"[FastFetch] Removed in-flight record for signature:",
120+
signature,
121+
);
92122
}
123+
}
93124
}

dist/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export { fastFetch } from "./fastFetch.js";
22
export type { FastFetchOptions } from "./fastFetch.js";
3-
//# sourceMappingURL=index.d.ts.map
3+
//# sourceMappingURL=index.d.ts.map

index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ export interface FastFetchOptions {
1111

1212
export declare function fastFetch(
1313
input: RequestInfo,
14-
init?: RequestInit & FastFetchOptions
14+
init?: RequestInit & FastFetchOptions,
1515
): Promise<Response>;

package-lock.json

Lines changed: 9 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@hoangsonw/fast-fetch",
33
"displayName": "Fast Fetch Smart API Fetcher",
4-
"version": "1.0.0",
4+
"version": "1.1.1",
55
"description": "A smarter fetch() wrapper with auto-retry, deduplication, and minimal boilerplate.",
66
"main": "dist/index.js",
77
"types": "index.d.ts",
@@ -40,7 +40,7 @@
4040
"typescript": "^5.0.0"
4141
},
4242
"dependencies": {
43-
"@hoangsonw/fast-fetch": "^1.0.0",
43+
"@hoangsonw/fast-fetch": "^1.1.1",
4444
"cross-fetch": "^4.1.0",
4545
"prettier": "^3.5.3"
4646
}

0 commit comments

Comments
 (0)