Skip to content

Commit

Permalink
Merge pull request #1 from Samagra-Development/sequential-api-requests
Browse files Browse the repository at this point in the history
[Feat] Sequential API Requests
  • Loading branch information
amit-s19 authored Aug 7, 2024
2 parents bad65a5 + 2e2c2b3 commit 9f7f461
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 32 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

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

194 changes: 166 additions & 28 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@ import React, {
useContext,
useEffect,
useState,
useRef
useRef,
} from 'react';
import { DataSyncContext } from './data-sync-context';
import axios from 'axios';
import localForage from 'localforage';

import { omit } from 'underscore';

const api = axios.create();
const RETRY_LIMIT = 0;
const RETRY_DELAY_MS = 1000;
const API_REQUESTS_STORAGE_KEY = 'apiRequests';
const OFFLINE_RESPONSES_STORAGE_KEY = 'offlineResponses';

import {
generateUuid,
Expand All @@ -32,21 +33,35 @@ import useNetworkStatus from './hooks/useNetworkStatus';
// const hasWindow = () => {
// return window && typeof window !== 'undefined';
// };
interface ApiRequest {
id: string;
type?: string;
url: string;
method: string;
data?: any | ((previousResponses: any[]) => Promise<any>);
isFormdata?: boolean;
retryCount?: number;
dependsOn?: string;
}

type ConfigType = {
isFormdata?: boolean;
maxRetry?: number;
executionOrder?: string[];
sequentialProcessing?: boolean;
};

export const OfflineSyncProvider: FC<{
children: ReactElement;
render?: (status: { isOffline?: boolean; isOnline: boolean }) => ReactNode;
onStatusChange?: (status: { isOnline: boolean }) => void;
onCallback?: (data: any) => void;
toastConfig?: any;
config?: ConfigType;
}> = ({ children, render, onStatusChange, onCallback }) => {
}> = ({ children, render, onStatusChange, onCallback, config }) => {
// Manage state for data, offline status, and online status
const [data, setData] = useState<Record<string, any>>({});
const isSyncing = useRef<boolean>();
const isSyncing = useRef<boolean>(false);
const [isOnline, setIsOnline] = useState<boolean>(
window?.navigator?.onLine ?? true
);
Expand All @@ -61,7 +76,6 @@ export const OfflineSyncProvider: FC<{
handleOnline();
} else {
handleOffline();

}
}
}, [isConnected]);
Expand Down Expand Up @@ -101,7 +115,7 @@ export const OfflineSyncProvider: FC<{

const saveRequestToOfflineStorage = async (apiConfig: any) => {
try {
const storedRequests: Array<any> =
const storedRequests: any =
(await localForage.getItem(API_REQUESTS_STORAGE_KEY)) || [];
console.log('perform stored', {
req: storedRequests,
Expand All @@ -110,10 +124,17 @@ export const OfflineSyncProvider: FC<{
if (apiConfig?.isFormdata && apiConfig?.data instanceof FormData) {
// console.log({ apiConfig })
const newData = await _formDataToObject(apiConfig.data);
storedRequests.push(omit({ ...apiConfig, data: newData }, 'onSuccess'));
storedRequests.push(
omit(
{ ...apiConfig, data: newData, type: apiConfig.type },
'onSuccess'
)
);
} else {
console.log('Saving request normally');
storedRequests.push(omit({ ...apiConfig }, 'onSuccess'));
storedRequests.push(
omit({ ...apiConfig, type: apiConfig.type }, 'onSuccess')
);
}
console.log('perform forage after:', { storedRequests });
const result = await localForage.setItem(
Expand All @@ -126,30 +147,74 @@ export const OfflineSyncProvider: FC<{
}
};

const saveResponseToOfflineStorage = async (type: string, response: any) => {
try {
const storedResponses: Record<string, any> =
(await localForage.getItem(OFFLINE_RESPONSES_STORAGE_KEY)) || {};
if (!storedResponses[type]) {
storedResponses[type] = [];
}
storedResponses[type].push(response);
await localForage.setItem(OFFLINE_RESPONSES_STORAGE_KEY, storedResponses);
} catch (error) {
console.error('Error saving response to offline storage:', error);
}
};

const getOfflineResponses = async (type: string) => {
try {
const storedResponses: Record<string, any> =
(await localForage.getItem(OFFLINE_RESPONSES_STORAGE_KEY)) || {};
return storedResponses[type] || [];
} catch (error) {
console.error('Error getting offline responses:', error);
return [];
}
};

// Function to perform the actual API request and handle retries
const performRequest = async (config: any): Promise<any> => {
console.log("Inside performRequest")
console.log('Inside performRequest');
try {
let requestData = config.data;
if (typeof requestData === 'function') {
const dependencyResponses = config.dependsOn
? await getOfflineResponses(config.dependsOn)
: [];
requestData = await requestData(dependencyResponses);
}

let response;
if (config?.isFormdata && !(config?.data instanceof FormData)) {
const updateConfig = { ...config, data: objectToFormData(config.data) };
if (config?.isFormdata && !(requestData instanceof FormData)) {
const updateConfig = { ...config, data: objectToFormData(requestData) };
response = await api.request(updateConfig);
} else {
response = await api.request(config);
response = await api.request({ ...config, data: requestData });
}

if (config.type) {
await saveResponseToOfflineStorage(config.type, response.data);
}

onCallback && onCallback({ config, data: response, sendRequest });
return response.data;
} catch (error) {
console.log('packageError', { error });
console.log("Inside performRequest error: ", { rc: config.retryCount, RETRY_LIMIT })
if (config.retryCount < RETRY_LIMIT) {
console.log('Inside performRequest error: ', {
rc: config.retryCount,
RETRY_LIMIT,
});
if ((config.retryCount ?? 0) < RETRY_LIMIT) {
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
config.retryCount++;
if (config.retryCount === undefined) {
config.retryCount = 1;
} else {
config.retryCount++;
}
return performRequest(config);
} else {
// Retry limit reached, save the request to offline storage
console.log("Saving request to offline storage");
console.log('Saving request to offline storage');
await saveRequestToOfflineStorage(config);
return error;
// throw new Error('Exceeded retry limit, request saved for offline sync.');
Expand All @@ -168,36 +233,109 @@ export const OfflineSyncProvider: FC<{
}
};

const processRequestsSequentially = async (
requests: ApiRequest[],
executionOrder?: string[]
) => {
const results = [];

if (executionOrder && executionOrder.length > 0) {
const requestsByType: Record<string, ApiRequest[]> = {};

for (const request of requests) {
const type = request.type || 'default';
if (!requestsByType[type]) {
requestsByType[type] = [];
}
requestsByType[type].push(request);
}

for (const type of executionOrder) {
const typeRequests = requestsByType[type] || [];
for (const request of typeRequests) {
try {
const result = await performRequest(request);
results.push({ request, result });
} catch (error) {
console.error(`Error processing ${type} request:`, error);
results.push({ request, error });
}
}
}

for (const type in requestsByType) {
if (!executionOrder.includes(type)) {
for (const request of requestsByType[type]) {
try {
const result = await performRequest(request);
results.push({ request, result });
} catch (error) {
console.error(`Error processing ${type} request:`, error);
results.push({ request, error });
}
}
}
}
} else {
for (const request of requests) {
try {
const result = await performRequest(request);
results.push({ request, result });
} catch (error) {
console.error(`Error processing request:`, error);
results.push({ request, error });
}
}
}

return results;
};

const syncOfflineRequests = async () => {
if (isSyncing.current) {
return;
}
isSyncing.current = true;
const storedRequests: any = await getStoredRequests();
if (!storedRequests || storedRequests.length === 0) {
isSyncing.current = false;
return;
}

console.log("Inside syncOfflineRequests", storedRequests)
console.log('Inside syncOfflineRequests', storedRequests);
const requestClone = [...storedRequests];
for (const request of storedRequests) {
console.log("Inside syncOfflineRequests loop, ", storedRequests)
if (request) {
try {
await performRequest(request);
// Remove the request with a matching id from requestClone

try {
let results;
if (config?.executionOrder) {
results = await processRequestsSequentially(
requestClone,
config.executionOrder
);
} else if (config?.sequentialProcessing) {
results = await processRequestsSequentially(requestClone);
} else {
results = await Promise.all(requestClone.map(performRequest));
}

for (const result of results) {
const request = result.request || result;
const error = result.error;
if (!error) {
const updatedRequests = requestClone.filter(
sr => sr.id !== request.id
);
requestClone.splice(0, requestClone.length, ...updatedRequests);
} catch (error) {
console.log({ error });
} finally {
await localForage.setItem(API_REQUESTS_STORAGE_KEY, requestClone);
} else {
console.error('Failed to process request:', request, error);
}
}
} catch (error) {
console.error('Error in syncOfflineRequests:', error);
} finally {
await localForage.setItem(API_REQUESTS_STORAGE_KEY, requestClone);
isSyncing.current = false;
}
isSyncing.current = false;
};

return (
Expand Down

0 comments on commit 9f7f461

Please sign in to comment.