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
JavaScript
/*! 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 };