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.
483 lines (482 loc) • 15.8 kB
JavaScript
/*! idmp v4.1.0 | (c) github/haozi | MIT */
Object.defineProperties(exports, {
__esModule: { value: true },
[Symbol.toStringTag]: { value: "Module" }
});
//#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;
/**
* Makes an object's properties read-only to prevent mutation
* @param obj - Object to make read-only
* @param key - Property key to make read-only
* @param value - Value to assign to the property
* @param visited - WeakSet to track visited objects and prevent circular references
* @returns true if successful, false otherwise
*/
var defineReactive = (obj, key, value, visited) => {
try {
var _Object$getOwnPropert, _Object$getOwnPropert2;
Object.prototype.toString.call(value);
readonly(value, visited);
Object.defineProperty(obj, key, {
configurable: false,
enumerable: (_Object$getOwnPropert = (_Object$getOwnPropert2 = Object.getOwnPropertyDescriptor(obj, key)) === null || _Object$getOwnPropert2 === void 0 ? void 0 : _Object$getOwnPropert2.enumerable) !== null && _Object$getOwnPropert !== void 0 ? _Object$getOwnPropert : true,
get: () => value,
set: (newValue) => {
const msg = `[idmp error] The data is read-only, set ${key.toString()}=${JSON.stringify(newValue)} is not allow`;
console.error(`%c ${msg}`, "font-weight: lighter; color: red", newValue);
throw new Error(msg);
}
});
return true;
} catch (_unused) {
/* istanbul ignore next */
return false;
}
};
/**
* Recursively makes an object and its properties read-only
* @param obj - Object to make read-only
* @param visited - WeakSet to track visited objects and prevent circular references
* @returns The read-only object, or original object if readonly fails
*/
var readonly = (obj, visited) => {
try {
if (obj == null || typeof obj !== "object") return obj;
const protoType = Object.prototype.toString.call(obj);
if (!["[object Object]", "[object Array]"].includes(protoType)) return obj;
const isImmerDraft = (obj) => !!obj[Symbol.for("immer-state")];
if (isImmerDraft(obj)) return obj;
const proto = Object.getPrototypeOf(obj);
if (proto !== Object.prototype && proto !== Array.prototype && proto !== null)
/* istanbul ignore next */
return obj;
if (!visited) visited = /* @__PURE__ */ new WeakSet();
if (visited.has(obj)) return obj;
visited.add(obj);
Object.keys(obj).forEach((key) => {
try {
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
if (!descriptor || descriptor.configurable === false) return;
if (descriptor.get || descriptor.set) return;
defineReactive(obj, key, obj[key], visited);
} catch (_unused2) {}
});
return obj;
} catch (_unused3) {
/* istanbul ignore next */
return obj;
}
};
/**
* 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 (process.env.NODE_ENV !== "production") options = readonly(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];
let callStackLocation = "";
const printLogs = (...msg) => {
if (typeof window === "undefined") return;
try {
if (localStorage.idmp_debug === "false") return;
} catch (_unused4) {}
/* istanbul ignore else -- Fallback for envs without console.groupCollapsed */
if (console.groupCollapsed) {
console.groupCollapsed(...msg);
console.log("globalKey:", globalKey);
console.log("callStackLocation:", callStackLocation);
console.log("data:", cache[K.resolvedData]);
console.groupEnd();
} else console.log(...msg);
};
/**
* 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]);
if (process.env.NODE_ENV !== "production") if (i === 0) printLogs(`%c[idmp debug] ${globalKey === null || globalKey === void 0 ? void 0 : globalKey.toString()} from origin`, "font-weight: lighter");
else printLogs(`%c[idmp debug] ${globalKey === null || globalKey === void 0 ? void 0 : globalKey.toString()} from cache`, "color: gray; font-weight: lighter");
}
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 (process.env.NODE_ENV !== "production") try {
if (cache[K.retryCount] === 0) throw new Error();
} catch (err) {
const getCodeLine = (stack, offset = 0) => {
if (typeof globalKey === "symbol") return "";
try {
let arr = stack.split("\n").filter((o) => o.includes(":"));
let idx = Infinity;
$0: for (let key of [
"idmp/src/index.ts",
"idmp/",
"idmp\\",
"idmp"
]) {
let _idx = arr.length - 1;
$1: for (; _idx >= 0; --_idx) if (arr[_idx].indexOf(key) > -1) {
idx = _idx;
break $0;
}
}
const line = arr[idx + offset + 1] || "";
if (line.includes("idmp")) return line;
/* istanbul ignore next */
return "";
} catch (_unused5) {
/* istanbul ignore next */
return "";
}
};
callStackLocation = getCodeLine(err.stack, 1).split(" ").pop() || "";
!cache[K._originalErrorStack] && (cache[K._originalErrorStack] = err.stack);
if (cache[K._originalErrorStack] !== err.stack) {
const line1 = getCodeLine(cache[K._originalErrorStack]);
const line2 = getCodeLine(err.stack);
if (line1 && line2 && line1 !== line2) console.error(`[idmp warn] the same key \`${globalKey.toString()}\` may be used multiple times in different places\n(It may be a misjudgment and can be ignored):\nsee https://github.com/ha0z1/idmp?tab=readme-ov-file#implementation \n${[
`1.${line1} ${cache[K._originalErrorStack]}`,
"------------",
`2.${line2} ${err.stack}`
].join("\n")}`);
}
}
if (cache[K.status] === Status.RESOLVED) {
if (process.env.NODE_ENV !== "production") printLogs(`%c[idmp debug] \`${globalKey === null || globalKey === void 0 ? void 0 : globalKey.toString()}\` from cache`, "color: gray;font-weight: lighter");
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) => {
if (process.env.NODE_ENV !== "production") cache[K.resolvedData] = readonly(data);
else 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 = process.env.NODE_ENV !== "production" ? _globalStore : 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) {
if (process.env.NODE_ENV !== "production") console.log(`[idmp-plugin browser-storage debug] ${globalKey} from ${storageType}["${getCacheKey(globalKey)}"]`);
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
exports.default = storageIdmpWrap;
exports.getCacheKey = getCacheKey;