Batch loader (jak DataLoader)
W resolverach GraphQL każde post.author osobno pyta bazę: 100 postów to 100 zapytań (problem N+1). Napisz createLoader(batchFn) z metodą load(key): wszystkie load wywołane w jednym obiegu event loop są łączone w jedno wywołanie batchFn(keys), które zwraca tablicę wartości w tej samej kolejności.
Powtarzające się klucze nie są dublowane w paczce, a już załadowany klucz pochodzi z cache bez nowego zapytania. Jeśli na miejscu klucza zwrócono Error, odrzucane jest tylko to load. Jeśli batchFn rzuci błąd lub zwróci tablicę innej długości, odrzucane są wszystkie load w paczce, a klucze nie trafiają do cache, więc kolejne load spróbuje ponownie.
Ctrl+Enter uruchamia testy. Tab robi wcięcie; aby wyjść z edytora klawiaturą, naciśnij Esc, a potem Tab. Kod zapisuje się w tej przeglądarce.
Testy (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');