UNPKG

idmp

Version:

A lightweight TypeScript library for deduplicating and caching async function calls with automatic retries, designed for idempotent network requests in React and Node.js.

207 lines (206 loc) 6.34 kB
/*! idmp v4.1.0 | (c) github/haozi | MIT */ //#region src/index.ts /** * Internal status enum for promise states */ var Status = /* @__PURE__ */ function(Status) { /** Promise hasn't been initiated yet */ Status[Status["UNSENT"] = 0] = "UNSENT"; /** Promise is in progress */ Status[Status["OPENING"] = 1] = "OPENING"; /** Promise was aborted (reserved for future use) */ Status[Status["ABORTED"] = 2] = "ABORTED"; /** Promise was rejected */ Status[Status["REJECTED"] = 3] = "REJECTED"; /** Promise was resolved successfully */ Status[Status["RESOLVED"] = 4] = "RESOLVED"; return Status; }(Status || {}); /** * Internal keys enum for accessing cache store properties */ var K = /* @__PURE__ */ function(K) { K[K["retryCount"] = 0] = "retryCount"; K[K["status"] = 1] = "status"; K[K["pendingList"] = 2] = "pendingList"; K[K["resolvedData"] = 3] = "resolvedData"; K[K["rejectionError"] = 4] = "rejectionError"; K[K["cachedPromiseFunc"] = 5] = "cachedPromiseFunc"; K[K["timerId"] = 6] = "timerId"; K[K["_originalErrorStack"] = 7] = "_originalErrorStack"; return K; }(K || {}); var DEFAULT_MAX_AGE = 3e3; var _7days = 6048e5; var noop = () => {}; var UNDEFINED = void 0; var $timeout = setTimeout; var $clearTimeout = clearTimeout; var getMax = (a, b) => a > b ? a : b; var getMin = (a, b) => a < b ? a : b; /** * Ensures maxAge is within valid range (0 to 7 days) * @param maxAge - The requested max age in milliseconds * @returns A valid max age value */ var getRange = (maxAge) => { if (maxAge < 0) return 0; if (maxAge > _7days) return _7days; return maxAge; }; /** * Global store for caching promises and their results * Contains tracking information for each promise identified by globalKey */ var _globalStore = {}; /** * Normalizes options, applying defaults as needed * @param options - User-provided options * @returns Normalized options object */ var getOptions = (options) => { const { maxRetry = 30, maxAge: paramMaxAge = DEFAULT_MAX_AGE, minRetryDelay = 50, maxRetryDelay = 5e3, onBeforeRetry = noop, signal } = options || {}; return { maxRetry, maxAge: getRange(paramMaxAge), minRetryDelay, maxRetryDelay, onBeforeRetry, f: paramMaxAge === Infinity, signal }; }; /** * Clears the cached result for a specific key * @param globalKey - The key to clear from cache */ var flush = (globalKey) => { if (!globalKey) return; const cache = _globalStore[globalKey]; if (!cache) return; cache[K.timerId] && $clearTimeout(cache[K.timerId]); _globalStore[globalKey] = UNDEFINED; }; /** * Clears all cached results */ var flushAll = () => { for (let key of Object.keys(_globalStore)) flush(key); _globalStore = {}; }; /** * Main IDMP function - Intelligent Deduplication of Multiple Promises * * Ensures that multiple calls to the same asynchronous operation (identified by globalKey) * will reuse the same promise, avoiding duplicate network requests or operations. * Includes automatic retry logic, caching, and request deduplication. * * @param globalKey - Unique identifier for this promise operation * @param promiseFunc - Function that returns the promise to execute * @param options - Configuration options * @returns Promise resolving to the result of promiseFunc * * @example * ```typescript * // Basic usage * const data = await idmp('user-123', () => fetchUserData(123)); * * // With options * const data = await idmp('user-123', () => fetchUserData(123), { * maxRetry: 5, * maxAge: 60000, // 1 minute cache * onBeforeRetry: (err, {retryCount}) => console.log(`Retry #${retryCount}`) * }); * ``` */ var idmp = (globalKey, promiseFunc, options) => { if (!globalKey) return promiseFunc(); const { maxRetry, minRetryDelay, maxRetryDelay, maxAge, onBeforeRetry, f: isFiniteParamMaxAge, signal } = getOptions(options); _globalStore[globalKey] = _globalStore[globalKey] || [ 0, Status.UNSENT, [] ]; const cache = _globalStore[globalKey]; /** * Resets the promise state for retrying */ const reset = () => { cache[K.status] = Status.UNSENT; cache[K.resolvedData] = cache[K.rejectionError] = UNDEFINED; }; /** * Resolves all pending promises with the cached result */ const doResolves = () => { const len = cache[K.pendingList].length; for (let i = 0; i < len; ++i) cache[K.pendingList][i][0](cache[K.resolvedData]); cache[K.pendingList] = []; if (!isFiniteParamMaxAge) cache[K.timerId] = $timeout(() => { flush(globalKey); }, maxAge); }; /** * Rejects all pending promises with the cached error */ const doRejects = () => { const len = cache[K.pendingList].length; let maxLen; maxLen = len - maxRetry; if (maxLen < 0 || !isFinite(len)) maxLen = getMax(1, cache[K.pendingList].length - 3); for (let i = 0; i < maxLen; ++i) cache[K.pendingList][i][1](cache[K.rejectionError]); flush(globalKey); }; /** * Creates and manages the actual promise execution */ const executePromise = () => new Promise((resolve, reject) => { !cache[K.cachedPromiseFunc] && (cache[K.cachedPromiseFunc] = promiseFunc); if (cache[K.status] === Status.RESOLVED) { resolve(cache[K.resolvedData]); return; } if (signal) { if (signal.aborted) return; signal.addEventListener("abort", () => { reset(); cache[K.rejectionError] = new DOMException(signal.reason, "AbortError"); doRejects(); }); } if (cache[K.status] === Status.UNSENT) { cache[K.status] = Status.OPENING; cache[K.pendingList].push([resolve, reject]); cache[K.cachedPromiseFunc]().then((data) => { cache[K.resolvedData] = data; doResolves(); cache[K.status] = Status.RESOLVED; }).catch((err) => { cache[K.status] = Status.REJECTED; cache[K.rejectionError] = err; ++cache[K.retryCount]; if (cache[K.retryCount] > maxRetry) doRejects(); else { onBeforeRetry(err, { globalKey, retryCount: cache[K.retryCount] }); reset(); $timeout(executePromise, getMin(maxRetryDelay, minRetryDelay * Math.pow(2, cache[K.retryCount] - 1))); } }); } else if (cache[K.status] === Status.OPENING) cache[K.pendingList].push([resolve, reject]); }); return executePromise(); }; /** * Clear the cached result for a specific key */ idmp.flush = flush; /** * Clear all cached results */ idmp.flushAll = flushAll; idmp._s = UNDEFINED; //#endregion export { idmp as default, idmp, getOptions };