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.
349 lines (348 loc) • 10.1 kB
JavaScript
/*! idmp v4.1.0 | (c) github/haozi | MIT */
//#region ../../dist/index.js
/*! idmp v4.1.0 | (c) github/haozi | MIT */
/**
* 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$1 = 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$1;
};
/**
* 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$1;
};
/**
* 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$1;
//#endregion
//#region \0@oxc-project+runtime@0.121.0/helpers/asyncToGenerator.js
function asyncGeneratorStep(n, t, e, r, o, a, c) {
try {
var i = n[a](c), u = i.value;
} catch (n) {
e(n);
return;
}
i.done ? t(u) : Promise.resolve(u).then(r, o);
}
function _asyncToGenerator(n) {
return function() {
var t = this, e = arguments;
return new Promise(function(r, o) {
var a = n.apply(t, e);
function _next(n) {
asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
}
function _throw(n) {
asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
}
_next(void 0);
});
};
}
//#endregion
//#region src/index.ts
var UNDEFINED = void 0;
var PREFIX = "@idmp/v4/";
var getCacheKey = (globalKey) => `${PREFIX}${globalKey}`;
/**
* Initialize a safe storage utility (localStorage or sessionStorage) with get/set/remove/clear methods
* @param storageType - Either 'localStorage' or 'sessionStorage'
*/
var initStorage = (storageType) => {
let storage;
try {
storage = window[storageType];
} catch (_unused) {}
/**
* Remove a cached item by key
* @param key - Global cache key
*/
const remove = (key) => {
if (!key) return;
try {
const cacheKey = getCacheKey(key);
storage.removeItem(cacheKey);
} catch (_unused2) {}
};
/**
* Retrieve cached data if available and not expired
* @param key - Global cache key
* @returns Cached data or undefined if not found or expired
*/
const get = (key) => {
if (!key) return;
const cacheKey = getCacheKey(key);
let localData;
try {
localData = JSON.parse(storage.getItem(cacheKey) || "");
if (localData === UNDEFINED) return;
const { t, a: maxAge, d: data } = localData;
if (Date.now() - t > maxAge) {
remove(cacheKey);
return;
}
return data;
} catch (_unused3) {}
};
/**
* Set data into storage with expiration
* @param key - Global cache key
* @param data - Data to cache
* @param maxAge - Time in milliseconds before expiration
*/
const set = (key, data, maxAge) => {
if (!key) return;
const cacheKey = getCacheKey(key);
try {
storage.setItem(cacheKey, JSON.stringify({
t: Date.now(),
a: maxAge,
d: data
}));
} catch (_unused4) {}
};
/**
* Clear all cached entries in the current storage matching the specific prefix
*/
const clear = () => {
try {
for (let i = storage.length - 1; i >= 0; i--) {
const key = storage.key(i);
if (key && key.startsWith(PREFIX)) remove(key);
}
} catch (_unused5) {}
};
return {
get,
set,
remove,
clear
};
};
/**
* Wrap an idmp instance with browser storage (localStorage or sessionStorage) for persistent caching
* @param _idmp - Original idmp instance
* @param storageType - Storage type to use, default is 'sessionStorage'
* @returns Wrapped idmp instance with persistent caching
*/
var storageIdmpWrap = (_idmp, storageType = "sessionStorage") => {
const storage = initStorage(storageType);
const newIdmp = (globalKey, promiseFunc, options) => {
const finalOptions = getOptions(options);
return _idmp(globalKey, _asyncToGenerator(function* () {
const localData = storage.get(globalKey);
if (localData !== UNDEFINED) return localData;
const memoryData = yield promiseFunc();
if (memoryData !== UNDEFINED) storage.set(globalKey, memoryData, finalOptions.maxAge);
return memoryData;
}), options);
};
/**
* Flush both idmp memory and browser storage cache for a specific key
* @param globalKey - Global cache key
*/
newIdmp.flush = (globalKey) => {
_idmp.flush(globalKey);
storage.remove(globalKey);
};
/**
* Flush all idmp memory and browser storage cache
*/
newIdmp.flushAll = () => {
_idmp.flushAll();
storage.clear();
};
return newIdmp;
};
//#endregion
export { storageIdmpWrap as default, getCacheKey };