JavaScript helper function for you to use
function createGetter(fn, {
cache = new Map(),
cacheSize = 100, // Used only if cache is a Map
throttleSize = Infinity,
prepare,
callbackIndex,
resolveWithFirstArgument = false
} = {}) {
const inFlight = new Map();
let activeCount = 0;
const queue = [];
// Wrap Map in a simple LRU if needed
if (cache instanceof Map) {
const rawMap = cache;
const lru = new Map();
cache = {
get(key) {
if (!rawMap.has(key)) return undefined;
const value = rawMap.get(key);
lru.delete(key);
lru.set(key, true); // Mark as most recently used
return value;
},
set(key, value) {
rawMap.set(key, value);
lru.set(key, true);
if (rawMap.size > cacheSize) {
const oldest = lru.keys().next().value;
rawMap.delete(oldest);
lru.delete(oldest);
}
},
delete(key) {
rawMap.delete(key);
lru.delete(key);
},
has(key) {
return rawMap.has(key);
}
};
}
function makeKey(args) {
return JSON.stringify(args.map(arg => (typeof arg === 'function' ? 'ƒ' : arg)));
}
function execute(context, args, key, resolve, reject) {
const callback = (err, result) => {
if (err) return reject(err);
cache.set(key, [context, arguments]);
if (prepare) prepare.call(null, context, arguments);
resolve(resolveWithFirstArgument && context !== undefined ? context : result);
processNext();
};
if (callbackIndex != null) args.splice(callbackIndex, 0, callback);
else args.push(callback);
if (fn.apply(context, args) === false) {
cache.delete(key); // opt-out of cache
}
}
function processNext() {
activeCount--;
if (queue.length && activeCount < throttleSize) {
const next = queue.shift();
activeCount++;
execute(...next);
}
}
const getter = function (...args) {
return new Promise((resolve, reject) => {
const context = this;
const key = makeKey(args);
if (cache.has(key)) {
const [cachedContext, cachedArgs] = cache.get(key);
if (prepare) prepare.call(null, cachedContext, cachedArgs);
return resolve(resolveWithFirstArgument && cachedContext !== undefined ? cachedContext : cachedArgs[1]);
}
if (inFlight.has(key)) {
return inFlight.get(key).then(resolve, reject);
}
const promise = new Promise((res, rej) => {
if (activeCount < throttleSize) {
activeCount++;
execute(context, args.slice(), key, res, rej);
} else {
queue.push([context, args.slice(), key, res, rej]);
}
});
inFlight.set(key, promise);
promise.finally(() => {
inFlight.delete(key);
});
promise.then(resolve, reject);
});
};
getter.forget = (...args) => {
const key = makeKey(args);
inFlight.delete(key);
return cache.delete(key);
};
getter.force = function (...args) {
getter.forget(...args);
return getter.apply(this, args);
};
return getter;
}
Here is what it does for all the functions you wrap with it:Memoizes async getters: Call a function with the same arguments and it returns the cached result instead of recomputing.
Handles in-flight deduping: If multiple parts of your app call the same getter while it's still working, only one request is sent. The rest wait on the same promise.
Throttles concurrency: You can limit how many calls to your getter run in parallel. Useful for APIs, disk I/O, or anything rate-sensitive.
Supports custom caching backends: Pass any object with get, set, delete, and has. Works with Map, LRU, or your own cache logic.
Optional LRU eviction: If you pass a plain Map, it upgrades it to an LRU with a max size. Least recently used items are evicted when full.
Handles callbacks and Promises: Wraps traditional callback-style async functions, but gives you a modern Promise-based interface.
Smart-ish keying: Builds a cache key by stringifying non-function arguments. Works well for most everyday use cases.
Supports manual eviction: Call getter.forget(...args) to remove specific entries or getter.force(...args) to bypass the cache for one call.
Allows custom preparation logic: You can pass a prepare() function to clone or process cached results before using them.
No comments yet