export type FetchFunction<K, V> = (key: K) => Promise<V>; type ResolveReject<V> = Parameters<ConstructorParameters<typeof Promise<V>>[0]>; type ResolverPair<V> = { resolve: ResolveReject<V>[0]; reject: ResolveReject<V>[1]; }; export class DebounceLoader<K, V> { private resolverMap = new Map<K, ResolverPair<V>>(); private promiseMap = new Map<K, Promise<V>>(); private resolvedPromise = Promise.resolve(); constructor(private loadFn: FetchFunction<K, V>) {} public load(key: K): Promise<V> { const promise = this.promiseMap.get(key); if (typeof promise !== 'undefined') { return promise; } const isFirst = this.promiseMap.size === 0; const newPromise = new Promise<V>((resolve, reject) => { this.resolverMap.set(key, { resolve, reject }); }); this.promiseMap.set(key, newPromise); if (isFirst) { this.enqueueDebouncedLoadJob(); } return newPromise; } private runDebouncedLoad(): void { const resolvers = [...this.resolverMap]; this.resolverMap.clear(); this.promiseMap.clear(); for (const [key, { resolve, reject }] of resolvers) { this.loadFn(key).then(resolve, reject); } } private enqueueDebouncedLoadJob(): void { this.resolvedPromise.then(() => { process.nextTick(() => { this.runDebouncedLoad(); }); }); } }