← Усі задачіNode.js · Senior

Batch loader (як DataLoader)

У GraphQL-резолверах кожен post.author окремо ходить у базу: 100 постів - 100 запитів (проблема N+1). Напишіть createLoader(batchFn) з методом load(key): усі load, викликані в межах одного проходу event loop, збираються в один виклик batchFn(keys), який повертає масив значень у тому самому порядку.

Однакові ключі в пакеті не дублюються, а вже завантажений ключ береться з кешу без нового запиту. Якщо на місці ключа повернуто Error, відхиляється лише цей load. Якщо batchFn упав чи повернув масив іншої довжини, відхиляються всі load пакета, а ключі не кешуються - наступний load спробує знову.

Тести (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');