JavaScript helper function for you to use
Here is what it does for all the functions you wrap with it. In my experience, these are very helpful and also gives you a place you can even hook into and add more later (such as handling batching transparencly, etc):
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.
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;
}
No comments yet