Last active
March 10, 2022 05:53
-
-
Save gabemeola/9a82f46b37dd9fdc64121ef39dc5b873 to your computer and use it in GitHub Desktop.
Memoize an Async function
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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; | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
}) | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@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