Skip to content

Instantly share code, notes, and snippets.

@gabemeola
Last active March 10, 2022 05:53
Show Gist options
  • Save gabemeola/9a82f46b37dd9fdc64121ef39dc5b873 to your computer and use it in GitHub Desktop.
Save gabemeola/9a82f46b37dd9fdc64121ef39dc5b873 to your computer and use it in GitHub Desktop.
Memoize an Async function
/**
* Memoize a async function (any function which returns a promise)
*
* @param {Function} func - Function which returns a promise
* @param {boolean} [cacheError=false] - Should cache promise rejections
* @param {Function} [resolver] - Resolves cache key. Uses first arg as default
* @returns {Function.<Promise>} - Returns memoized function
*/
export default function memoizeAsync(func, cacheError = false, resolver) {
const cache = new Map();
return (...args) => {
const key = resolver ? resolver(...args) : args[0];
if (cache.has(key)) {
return cache.get(key);
}
const promise = func(...args)
.catch((err) => {
if (cacheError === false) {
// Remove the cached promise so we can re-call passed function
cache.delete(key);
}
// Re-throw error
throw err;
});
// Cache whole promise
// This will also prevent race conditions
// from multiple concurrently running promises
cache.set(key, promise);
return promise;
};
}
import memoizeAsync from './memoizeAsync';
let genSymbol = async (name) => {
return Symbol(name);
};
let genSymbolThrow = async (name) => {
throw Symbol(name);
};
beforeEach(() => {
// Reset mock
genSymbol = jest.fn(genSymbol);
genSymbolThrow = jest.fn(genSymbolThrow);
});
it('should resolve value from cache', async (done) => {
const func = memoizeAsync(genSymbol);
// Should be the same symbol
expect(await func('harry')).toBe(await func('harry'));
// Should be the same promise
expect(func('harry')).toBe(func('harry'));
expect(await func('jamason')).not.toBe(await func('harry'));
expect(func('jamason')).not.toBe(func('harry'));
done()
});
describe('should not run the same function twice (race-condition)', () => {
it('cacheError = false', async (done) => {
const func = memoizeAsync(genSymbol, false);
// Firing off a few concurrent calls
const harry = func('harry');
func('harry');
func('harry');
expect(genSymbol).toBeCalledTimes(1);
// Should be the same symbol
expect(await func('harry')).toBe(await harry);
done()
});
it('cacheError = true', async (done) => {
const func = memoizeAsync(genSymbol, true);
// Firing off a few concurrent calls
const harry = func('harry');
func('harry');
func('harry');
expect(genSymbol).toBeCalledTimes(1);
// Should be the same symbol
expect(await func('harry')).toBe(await harry);
done()
});
});
it('should not cache rejected promise', (done) => {
const func = memoizeAsync(genSymbolThrow, false);
const call = func('harry');
// console.log('call', call);
call.catch((err) => {
const secondCall = func('harry');
secondCall.catch((err2) => {
// console.log('error', err, err2, err === err2);
expect(err).not.toBe(err2);
done();
});
})
});
it('should cache rejected promise', (done) => {
const func = memoizeAsync(genSymbolThrow, true);
const call = func('harry');
call.catch((err) => {
const secondCall = func('harry');
secondCall.catch((err2) => {
expect(err).toBe(err2);
done();
});
})
});
describe('should use key resolver function', () => {
it('always resolve same cache', async (done) => {
const func = memoizeAsync(genSymbol, false, () => 1);
expect(await func('harry')).toBe(await func('dave'));
done();
});
it('always resolve different cache', async (done) => {
const func = memoizeAsync(genSymbol, false, () => Math.random());
expect(await func('harry')).not.toBe(await func('harry'));
expect(await func('harry')).not.toBe(await func('harry'));
expect(func('harry')).not.toBe(func('harry'));
done();
})
});
@madagascarVanilla
Copy link

@gabemeola Hey Gabe, I think that on line 13 it's better to have all args stringified in some way, otherwise if first argument is the same and others different we will be caching sometihng which should output different results, my example: https://codesandbox.io/s/sharp-ardinghelli-xqygdf?file=/src/index.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment