Batch Loader (wie DataLoader)
In GraphQL-Resolvern fragt jedes post.author die Datenbank einzeln ab: 100 Posts bedeuten 100 Abfragen (das N+1-Problem). Schreib createLoader(batchFn) mit load(key): alle load-Aufrufe innerhalb eines Durchlaufs der Event Loop werden zu einem einzigen Aufruf batchFn(keys) zusammengefasst, der ein Array von Werten in derselben Reihenfolge liefert.
Wiederholte Schlüssel werden in einem Batch nicht dupliziert, und ein bereits geladener Schlüssel kommt ohne neue Anfrage aus dem Cache. Steht an der Position eines Schlüssels ein Error, wird nur dieses load abgelehnt. Wirft batchFn oder liefert ein Array anderer Länge, wird jedes load im Batch abgelehnt, und die Schlüssel werden nicht gecacht, das nächste load versucht es also erneut.
Strg+Enter führt die Tests aus. Tab rückt ein; um den Editor per Tastatur zu verlassen, drücke Esc und dann Tab. Der Code wird in diesem Browser gespeichert.
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');