Batch loader (like DataLoader)
In GraphQL resolvers each post.author queries the database separately: 100 posts mean 100 queries (the N+1 problem). Write createLoader(batchFn) with load(key): all load calls made within one turn of the event loop are combined into a single batchFn(keys) call that returns an array of values in the same order.
Repeated keys aren't duplicated in a batch, and an already loaded key comes from the cache without a new request. If an Error is returned in a key's position, only that load rejects. If batchFn throws or returns an array of a different length, every load in the batch rejects, and the keys aren't cached, so the next load tries again.
Ctrl+Enter runs the tests. Tab indents; to leave the editor with the keyboard, press Esc and then Tab. Your code is saved in this browser.
Tests (6)
const batchFn = fn(async (ids) => ids.map((id) => ({ id, name: 'user' + id }))); const loader = createLoader(batchFn); const [a, b, c] = await Promise.all([loader.load(1), loader.load(2), loader.load(3)]); expect([a.name, b.name, c.name]).toEqual(['user1', 'user2', 'user3']); expect(batchFn).toHaveBeenCalledTimes(1); expect(batchFn).toHaveBeenCalledWith([1, 2, 3]);
const batchFn = fn(async (ids) => ids.map((id) => id * 10)); const loader = createLoader(batchFn); const [x, y] = await Promise.all([loader.load(5), loader.load(5)]); expect([x, y]).toEqual([50, 50]); expect(batchFn).toHaveBeenLastCalledWith([5]);
const batchFn = fn(async (ids) => ids.map((id) => id)); const loader = createLoader(batchFn); await loader.load(1); await loader.load(2); await loader.load(1); expect(batchFn).toHaveBeenCalledTimes(2);
const loader = createLoader(async (ids) => ids.map((id) => (id === 2 ? new Error('User 2 not found') : id))); const results = await Promise.allSettled([loader.load(1), loader.load(2), loader.load(3)]); expect(results.map((r) => r.status)).toEqual(['fulfilled', 'rejected', 'fulfilled']); expect(results[1].reason.message).toBe('User 2 not found');
let fail = true; const batchFn = fn(async (ids) => { if (fail) throw new Error('db down'); return ids; }); const loader = createLoader(batchFn); await expect(loader.load(1)).rejects.toThrow('db down'); fail = false; expect(await loader.load(1)).toBe(1);
const loader = createLoader(async (ids) => ids.slice(1)); await expect(loader.load('a')).rejects.toThrow('same length');