JavaScript helper function for you to use
by EGreg | 2025-07-08 21:03:30+00:00
Got a function that fetches some values asynchronously from somewhere? Consider wrapping it in this and making it a super-function.

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; }