Skip to content

Commit aeef3ba

Browse files
author
dk3tm
committed
Major Alpha - Migrate to Option Store, remove circular dependency inheritence and name change to NetFlux. Ver Bump to 1.2.0-alpha
1 parent f91c663 commit aeef3ba

File tree

4 files changed

+448
-1
lines changed

4 files changed

+448
-1
lines changed

composable/useNetFlux.ts

+319
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
import { ref } from "vue";
2+
3+
// Define a type for the HTTP methods
4+
export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
5+
6+
// Define a type for the API request details
7+
interface ApiRequest {
8+
method: HttpMethod;
9+
endpoint: string;
10+
headers?: Record<string, string>;
11+
queryParams?: Record<string, string | number>;
12+
body?: any;
13+
}
14+
15+
// Define the type for the parameters passed to executeCall
16+
interface ExecuteCallParams {
17+
apiRequest: ApiRequest;
18+
async?: boolean;
19+
override?: boolean;
20+
retries?: number; // Number of retry attempts
21+
retryDelay?: number; // Delay between retries in milliseconds
22+
cancellationToken?: AbortController; // Custom cancellation token
23+
timeout?: number; // Timeout duration in milliseconds
24+
cacheDuration?: number; // Cache duration in milliseconds
25+
skipCache?: boolean; // Flag to skip cache and force a new request
26+
}
27+
28+
// Global configuration for the request manager
29+
export const defaultConfig = ref({
30+
retries: 3, // Default number of retry attempts
31+
retryDelay: 1000, // Default delay between retries in milliseconds
32+
timeout: 5000, // Default timeout in milliseconds (5 seconds)
33+
cacheDuration: 60000, // Default cache duration in milliseconds (1 minute)
34+
async: false, // Default async behavior
35+
override: false, // Default override behavior
36+
skipCache: false, // Default cache skipping
37+
logging: true, // Enable or disable logging globally
38+
});
39+
40+
export function useNetFlux() {
41+
const requestQueue = ref(new Map()); // Store ongoing requests
42+
const cacheStore = ref(new Map()); // Store cached responses
43+
44+
// Helper function for logging
45+
function log(
46+
level: "info" | "warn" | "error",
47+
message: string,
48+
...details: any[]
49+
) {
50+
if (defaultConfig.value.logging) {
51+
const timestamp = new Date().toISOString();
52+
console[level](
53+
`[${timestamp}] ${level.toUpperCase()}: ${message}`,
54+
...details
55+
);
56+
}
57+
}
58+
59+
// Helper function to handle request timeout
60+
function createTimeoutAbortController(timeout: number) {
61+
const controller = new AbortController(); // Create an AbortController
62+
const timeoutId = setTimeout(() => {
63+
controller.abort(); // Abort the request if the timeout occurs
64+
log("warn", `Request timed out after ${timeout}ms`);
65+
}, timeout);
66+
67+
// Return the controller and a function to clear the timeout
68+
return { controller, clearTimeout: () => clearTimeout(timeoutId) };
69+
}
70+
71+
// Helper function to generate cache key based on URL and query params
72+
function generateCacheKey(
73+
url: string,
74+
queryParams: Record<string, string | number>
75+
) {
76+
const queryString = new URLSearchParams(queryParams as any).toString();
77+
return queryString ? `${url}?${queryString}` : url;
78+
}
79+
80+
// Function to check if cached data is valid
81+
function isCacheValid(cacheTimestamp: number, cacheDuration: number) {
82+
const currentTime = Date.now();
83+
return currentTime - cacheTimestamp < cacheDuration;
84+
}
85+
86+
// Helper function to attempt the network call with retry, timeout, and cache support
87+
async function attemptNetworkCall(
88+
{
89+
apiRequest,
90+
retries,
91+
retryDelay,
92+
cancellationToken,
93+
timeout,
94+
cacheDuration,
95+
skipCache,
96+
}: ExecuteCallParams,
97+
attempt: number
98+
): Promise<any> {
99+
const {
100+
method,
101+
endpoint,
102+
headers = {},
103+
queryParams = {},
104+
body,
105+
} = apiRequest;
106+
107+
// Generate a cache key based on the request
108+
const cacheKey = generateCacheKey(endpoint, queryParams);
109+
110+
// If cache is not skipped and cache duration is valid, check for cached response
111+
if (!skipCache && cacheDuration && cacheDuration > 0) {
112+
const cachedResponse = cacheStore.value.get(cacheKey);
113+
if (
114+
cachedResponse &&
115+
isCacheValid(cachedResponse.timestamp, cacheDuration)
116+
) {
117+
log("info", `Returning cached response for: ${cacheKey}`);
118+
return cachedResponse.data;
119+
}
120+
}
121+
122+
// Use either the provided cancellation token or create a new AbortController
123+
let controller = cancellationToken || new AbortController();
124+
let timeoutCleanup: (() => void) | undefined;
125+
126+
if (timeout) {
127+
// Create a timeout-bound controller if timeout is specified
128+
const timeoutController = createTimeoutAbortController(timeout);
129+
controller = timeoutController.controller;
130+
timeoutCleanup = timeoutController.clearTimeout;
131+
}
132+
133+
const signal = controller.signal;
134+
135+
// Define the network request function
136+
const networkCall = async () => {
137+
log("info", `Attempting network call`, { endpoint, method, attempt });
138+
139+
try {
140+
const options: RequestInit = {
141+
method,
142+
headers: {
143+
"Content-Type": "application/json",
144+
...headers,
145+
},
146+
signal,
147+
};
148+
149+
// Add body for methods that require it
150+
if (["POST", "PUT", "PATCH"].includes(method) && body) {
151+
options.body = JSON.stringify(body);
152+
}
153+
154+
const response = await fetch(endpoint, options);
155+
156+
if (!response.ok) {
157+
throw new Error(`Failed to fetch: ${response.statusText}`);
158+
}
159+
160+
const result = await response.json();
161+
162+
log("info", `Network call successful`, {
163+
endpoint,
164+
method,
165+
attempt,
166+
result,
167+
});
168+
169+
// Cache the response if cache duration is specified
170+
if (cacheDuration && cacheDuration > 0) {
171+
cacheStore.value.set(cacheKey, {
172+
data: result,
173+
timestamp: Date.now(), // Save the current timestamp
174+
});
175+
log("info", `Response cached for: ${cacheKey}`, { cacheDuration });
176+
}
177+
178+
return result;
179+
} catch (error: any) {
180+
if (error.name === "AbortError") {
181+
log("warn", "Request aborted:", { endpoint });
182+
} else {
183+
log("error", "Request failed:", { error, endpoint });
184+
return error;
185+
}
186+
187+
// If retries are allowed and there are still retries left, retry the request
188+
if (retries && attempt < retries) {
189+
log("warn", `Retrying... Attempt ${attempt + 1}`, {
190+
endpoint,
191+
method,
192+
retryDelay,
193+
});
194+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
195+
return attemptNetworkCall(
196+
{
197+
apiRequest,
198+
retries,
199+
retryDelay,
200+
cancellationToken,
201+
timeout,
202+
cacheDuration,
203+
skipCache,
204+
},
205+
attempt + 1
206+
);
207+
} else {
208+
// If no retries left, throw the error
209+
throw error;
210+
}
211+
} finally {
212+
if (timeoutCleanup) {
213+
timeoutCleanup(); // Clear the timeout after request completes or fails
214+
}
215+
}
216+
};
217+
218+
// Execute the network call
219+
return networkCall();
220+
}
221+
222+
// Helper function to execute the network call with async/override, retry, timeout, and cache logic
223+
async function executeCall({
224+
apiRequest,
225+
async,
226+
override,
227+
retries,
228+
retryDelay,
229+
cancellationToken,
230+
timeout,
231+
cacheDuration,
232+
skipCache,
233+
}: ExecuteCallParams) {
234+
const {
235+
method,
236+
endpoint,
237+
headers = {},
238+
queryParams = {},
239+
body,
240+
} = apiRequest;
241+
242+
// Merge user-provided options with global config
243+
const mergedOptions = {
244+
async: async !== undefined ? async : defaultConfig.value.async,
245+
override:
246+
override !== undefined ? override : defaultConfig.value.override,
247+
retries: retries !== undefined ? retries : defaultConfig.value.retries,
248+
retryDelay:
249+
retryDelay !== undefined ? retryDelay : defaultConfig.value.retryDelay,
250+
timeout: timeout !== undefined ? timeout : defaultConfig.value.timeout,
251+
cacheDuration:
252+
cacheDuration !== undefined
253+
? cacheDuration
254+
: defaultConfig.value.cacheDuration,
255+
skipCache:
256+
skipCache !== undefined ? skipCache : defaultConfig.value.skipCache,
257+
};
258+
259+
const queryString = new URLSearchParams(queryParams as any).toString();
260+
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
261+
262+
log("info", `Starting API call`, { url, method, mergedOptions });
263+
264+
// Check if an API call is already ongoing for this endpoint
265+
if (requestQueue.value.has(url)) {
266+
if (mergedOptions.override) {
267+
// Abort previous call and remove it from the queue
268+
const ongoingRequest = requestQueue.value.get(url);
269+
ongoingRequest.controller.abort();
270+
requestQueue.value.delete(url);
271+
log("info", `Aborted ongoing request for: ${url}`);
272+
} else if (!mergedOptions.async) {
273+
// If not async and no override, wait for the current one to finish
274+
log("info", `Waiting for ongoing request to complete for: ${url}`);
275+
await requestQueue.value.get(url).promise;
276+
}
277+
}
278+
279+
// Perform the network call with retries, cancellation token, timeout, and cache support
280+
const callPromise = attemptNetworkCall(
281+
{
282+
apiRequest,
283+
retries: mergedOptions.retries,
284+
retryDelay: mergedOptions.retryDelay,
285+
cancellationToken,
286+
timeout: mergedOptions.timeout,
287+
cacheDuration: mergedOptions.cacheDuration,
288+
skipCache: mergedOptions.skipCache,
289+
},
290+
0
291+
);
292+
293+
// Add the request to the queue
294+
requestQueue.value.set(url, {
295+
controller: cancellationToken || new AbortController(),
296+
promise: callPromise,
297+
});
298+
299+
try {
300+
const result = await callPromise;
301+
log("info", `API call completed for: ${url}`, { result });
302+
return result;
303+
} catch (error) {
304+
log("error", `API call failed for: ${url}`, { error });
305+
throw error;
306+
} finally {
307+
// Remove the request from the queue after it completes
308+
requestQueue.value.delete(url);
309+
}
310+
}
311+
312+
// Function to update the global configuration
313+
function updateGlobalConfig(newConfig: Partial<typeof defaultConfig.value>) {
314+
defaultConfig.value = { ...defaultConfig.value, ...newConfig };
315+
log("info", `Global config updated`, { newConfig });
316+
}
317+
318+
return { executeCall, updateGlobalConfig };
319+
}

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "use-netflux-playground",
3-
"version": "1.1.0-alpha",
3+
"version": "1.2.0-alpha",
44
"private": false,
55
"description": "A composable for making API requests with retries, caching, timeouts, cancellation, and logging.",
66
"scripts": {

0 commit comments

Comments
 (0)