← Wszystkie zadaniaNode.js · Senior

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.

Testy (6)

  1. 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]);
  2. 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]);
  3. 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);
  4. 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');
  5. 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);
  6. const loader = createLoader(async (ids) => ids.slice(1));
    await expect(loader.load('a')).rejects.toThrow('same length');