Skip to the code
cf.

Batch calls

MediumTiming

Write batchCalls(fn, { wait, maxSize }). It returns a function, add(item), that collects items and passes them to fn in batches, as one array, instead of one at a time. For example, analytics events can reach the server in one request instead of dozens.

The first item added starts a batch and a wait ms timer. Items added later join the batch without restarting the timer. When the timer fires, fn is called once with an array of the batch's items, in the order they were added, and the next item starts a new batch.

When a batch reaches maxSize items, fn gets it right away, during the call that added the last item, and the batch's timer is cancelled. maxSize is optional: without it, only the timer ends a batch.

add also has a flush() method that passes the current batch to fn right away, for example before the page closes. The next item starts a new batch.

fn is never called with an empty array, so flush() with nothing collected does nothing. Each call of fn gets an array of its own, which never changes after fn gets it.

const track = batchCalls((events) => console.log(events), { wait: 1000, maxSize: 3 });
track("view"); // at 0 ms: starts a batch
track("click"); // at 400 ms: joins it
// at 1000 ms: logs ["view", "click"]
track("a");
track("b");
track("c"); // the batch is full: logs ["a", "b", "c"] right away
track("d");
track.flush(); // logs ["d"]
track.flush(); // nothing collected: does nothing

What the tests check

  1. calls fn once with every item, wait ms after the first
    const clock = useFakeTimers();
    const fn = spy();
    const add = batchCalls(fn, { wait: 100, maxSize: 10 });
    add("a");
    clock.tick(60);
    add("b");
    add("c");
    clock.tick(39);
    expect(fn).toHaveBeenCalledTimes(0);
    clock.tick(1);
    expect(fn).toHaveBeenCalledTimes(1);
    expect(fn).toHaveBeenLastCalledWith(["a", "b", "c"]);
    
  2. the next item starts a new batch and a new timer
    const clock = useFakeTimers();
    const fn = spy();
    const add = batchCalls(fn, { wait: 100 });
    add(1);
    clock.tick(650);
    add(2);
    add(3);
    clock.tick(99);
    expect(fn.calls.map((call) => call.args[0])).toEqual([[1]]);
    clock.tick(1);
    expect(fn.calls.map((call) => call.args[0])).toEqual([[1], [2, 3]]);
    
  3. calls fn right away when the batch reaches maxSize
    const clock = useFakeTimers();
    const fn = spy();
    const add = batchCalls(fn, { wait: 100, maxSize: 3 });
    add(1);
    add(2);
    expect(fn).toHaveBeenCalledTimes(0);
    add(3);
    expect(fn).toHaveBeenCalledTimes(1);
    expect(fn).toHaveBeenLastCalledWith([1, 2, 3]);
    add(4);
    clock.tick(100);
    expect(fn.calls.map((call) => call.args[0])).toEqual([[1, 2, 3], [4]]);
    
  4. a full batch cancels its timer
    const clock = useFakeTimers();
    const fn = spy();
    const add = batchCalls(fn, { wait: 100, maxSize: 2 });
    add(1);
    clock.tick(50);
    add(2); // full: fn gets [1, 2] now
    clock.tick(10);
    add(3); // a new batch, due at 160 ms
    clock.tick(99);
    expect(fn.calls.map((call) => call.args[0])).toEqual([[1, 2]]);
    clock.tick(1);
    expect(fn.calls.map((call) => call.args[0])).toEqual([[1, 2], [3]]);
    
  5. flush() passes the current batch to fn right away
    const clock = useFakeTimers();
    const fn = spy();
    const add = batchCalls(fn, { wait: 100, maxSize: 10 });
    add("a");
    add("b");
    clock.tick(50);
    add.flush();
    expect(fn).toHaveBeenCalledTimes(1);
    expect(fn).toHaveBeenLastCalledWith(["a", "b"]);
    add("c"); // a new batch, due at 150 ms
    clock.tick(99);
    expect(fn).toHaveBeenCalledTimes(1);
    clock.tick(1);
    expect(fn).toHaveBeenCalledTimes(2);
    expect(fn).toHaveBeenLastCalledWith(["c"]);
    
  6. never calls fn with an empty array
    const clock = useFakeTimers();
    const fn = spy();
    const add = batchCalls(fn, { wait: 100, maxSize: 2 });
    add.flush();
    add(1);
    add(2);
    clock.tick(1000);
    add.flush();
    expect(fn.calls.map((call) => call.args[0])).toEqual([[1, 2]]);
    
  7. gives fn a new array for every batch
    const clock = useFakeTimers();
    const batches = [];
    const add = batchCalls((items) => batches.push(items), { wait: 100, maxSize: 2 });
    add(1);
    add(2);
    add(3);
    clock.tick(100);
    expect(batches).toEqual([[1, 2], [3]]);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).