import { isOnline } from "@utils/helpers"; import { logger } from "@utils/logger"; import { ApiClientConfig, ApiRequest } from "@utils/types"; const isBrowser = typeof window !== "undefined" && window.localStorage; class SimpleStorage { private inMemoryStore: Record = {}; getItem(key: string): string | null { if (isBrowser) { return localStorage.getItem(key); } else { return this.inMemoryStore[key] || null; } } setItem(key: string, value: string): void { if (isBrowser) { localStorage.setItem(key, value); } else { this.inMemoryStore[key] = value; } } removeItem(key: string): void { if (isBrowser) { localStorage.removeItem(key); } else { delete this.inMemoryStore[key]; } } } export class OfflineStore { private config: ApiClientConfig["offline"]; private storage: SimpleStorage; private requestQueueSync: (request: ApiRequest) => void; private isSyncing: boolean = false; constructor( config: ApiClientConfig, requestQueueSync: (request: ApiRequest) => void ) { this.config = config.offline; this.storage = new SimpleStorage(); this.requestQueueSync = requestQueueSync; if (this.config.enabled) { logger.log( `OfflineStore enabled. Storage Key: ${this.config.storageKey}` ); if (isBrowser) { window.addEventListener("online", this.attemptSync); } } } /** * Retrieves all stored requests. * @returns {ApiRequest[]} Array of stored requests. */ private getStoredRequests(): ApiRequest[] { const storedJson = this.storage.getItem(this.config.storageKey); if (!storedJson) { return []; } try { const serializableRequests = JSON.parse(storedJson); return serializableRequests.map((req: any) => ({ ...req, resolve: () => {}, reject: () => {}, status: "QUEUED", })); } catch (e) { logger.error("Failed to parse stored requests from storage.", e); this.storage.removeItem(this.config.storageKey); return []; } } /** * Saves the current list of requests back to storage. * @param requests The list of requests to save. */ private saveRequests(requests: ApiRequest[]): void { const serializableRequests = requests.map((req) => { const { resolve, reject, ...rest } = req; return rest; }); const limitedRequests = serializableRequests.slice(0, this.config.limit); try { this.storage.setItem( this.config.storageKey, JSON.stringify(limitedRequests) ); logger.log(`Saved ${limitedRequests.length} requests to offline store.`); } catch (e) { logger.error("Failed to save requests to offline store.", e); } } /** * Stores a single request after it fails due to being offline. * @param request The failed request object. */ public storeRequest(request: ApiRequest): void { if (!this.config.enabled || request.options.offlineable === false) { logger.warn( `Request ${request.id} not stored: Offline mode disabled or request non-offlineable.` ); return; } const storedRequests = this.getStoredRequests(); storedRequests.push(request); this.saveRequests(storedRequests); request.reject({ name: "OfflineStoreError", message: `Request ${request.id} failed due to network and was stored for later sync.`, classification: "NETWORK", } as any); } public attemptSync = async (): Promise => { if (!this.config.enabled || this.isSyncing || !isOnline()) { return; } const requestsToSync = this.getStoredRequests(); if (requestsToSync.length === 0) { logger.log("Offline store is empty. No sync needed."); return; } this.isSyncing = true; logger.warn( `Starting synchronization of ${requestsToSync.length} offline requests...` ); this.storage.removeItem(this.config.storageKey); requestsToSync.forEach((req) => { req.priority = req.priority !== undefined ? req.priority + 10 : 15; this.requestQueueSync(req); }); logger.warn( "All offline requests have been moved to the RequestQueue for processing." ); this.isSyncing = false; }; public clearStore(): void { this.storage.removeItem(this.config.storageKey); logger.log("Offline store cleared."); } }