Skip to the code
cf.

Merge async iterables

Write merge(...sources). Each source is an async iterable, such as an async generator. merge returns an async iterable that yields the values of all the sources as they arrive, whichever source they come from: if the second source produces a value before the first, that value comes out first.

Every value comes out exactly once, and the merged iterable ends once every source is done. With no sources, it ends at once.

If a source throws, the merged iteration throws that error.

When the merged iteration ends early, because the caller stopped (with break, or by calling return()) or because a source threw, it closes every source that's still running: it calls their return() method, if they have one.

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function* ticks(name, every, count) {
  for (let i = 1; i <= count; i++) {
    await wait(every);
    yield name + i;
  }
}
 
for await (const value of merge(ticks("a", 30, 3), ticks("b", 50, 2))) {
  console.log(value);
}
// logs a1 (at 30 ms), b1 (50 ms), a2 (60 ms), a3 (90 ms), b2 (100 ms)

What the tests check

  1. yields values in the order they arrive
    const clock = useFakeTimers();
    const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    async function* ticks(name, every, count) {
      for (let i = 1; i <= count; i++) {
        await wait(every);
        yield name + i;
      }
    }
    const seen = [];
    let failure;
    (async () => {
      for await (const value of merge(ticks("a", 30, 3), ticks("b", 50, 2))) seen.push(value);
    })().catch((error) => {
      failure = error;
    });
    await clock.tickAsync(100);
    expect(failure).toBeUndefined();
    expect(seen).toEqual(["a1", "b1", "a2", "a3", "b2"]);
    
  2. yields each value as soon as it arrives
    const clock = useFakeTimers();
    const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    async function* ticks(name, every, count) {
      for (let i = 1; i <= count; i++) {
        await wait(every);
        yield name + i;
      }
    }
    const seen = [];
    let failure;
    (async () => {
      for await (const value of merge(ticks("a", 30, 2), ticks("b", 50, 1))) seen.push(value);
    })().catch((error) => {
      failure = error;
    });
    await clock.tickAsync(30);
    expect(seen).toEqual(["a1"]);
    await clock.tickAsync(20);
    expect(seen).toEqual(["a1", "b1"]);
    await clock.tickAsync(10);
    expect(seen).toEqual(["a1", "b1", "a2"]);
    expect(failure).toBeUndefined();
    
  3. ends once every source is done, or at once with no sources
    const clock = useFakeTimers();
    const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    async function* ticks(name, every, count) {
      for (let i = 1; i <= count; i++) {
        await wait(every);
        yield name + i;
      }
    }
    const seen = [];
    let finished = false;
    let failure;
    (async () => {
      for await (const value of merge(ticks("a", 10, 1), ticks("b", 30, 1))) seen.push(value);
      finished = true;
    })().catch((error) => {
      failure = error;
    });
    await clock.tickAsync(10);
    expect(seen).toEqual(["a1"]);
    expect(finished).toBe(false);
    await clock.tickAsync(20);
    expect(seen).toEqual(["a1", "b1"]);
    expect(finished).toBe(true);
    expect(failure).toBeUndefined();
     
    let empty;
    merge()[Symbol.asyncIterator]()
      .next()
      .then(({ value, done }) => {
        empty = { value, done };
      });
    await flush();
    expect(empty).toEqual({ value: undefined, done: true });
    
  4. never loses or repeats a value
    const clock = useFakeTimers();
    const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    async function* ticks(name, every, count) {
      for (let i = 1; i <= count; i++) {
        await wait(every);
        yield name + i;
      }
    }
    const seen = [];
    let failure;
    (async () => {
      for await (const value of merge(ticks("a", 20, 3), ticks("b", 20, 3), ticks("c", 30, 2))) seen.push(value);
    })().catch((error) => {
      failure = error;
    });
    await clock.tickAsync(100);
    expect(failure).toBeUndefined();
    expect(seen).toHaveLength(8);
    expect([...seen].sort()).toEqual(["a1", "a2", "a3", "b1", "b2", "b3", "c1", "c2"]);
    
  5. throws when a source throws
    const clock = useFakeTimers();
    const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    async function* ticks(name, every, count) {
      for (let i = 1; i <= count; i++) {
        await wait(every);
        yield name + i;
      }
    }
    async function* failing() {
      await wait(40);
      throw new Error("source failed");
    }
    const seen = [];
    let failure;
    (async () => {
      for await (const value of merge(ticks("a", 30, 3), failing())) seen.push(value);
    })().catch((error) => {
      failure = error;
    });
    await clock.tickAsync(100);
    expect(seen).toEqual(["a1"]);
    expect(failure).toBeInstanceOf(Error);
    expect(failure.message).toBe("source failed");
    
  6. closes the other sources when one throws
    const clock = useFakeTimers();
    const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    const closed = [];
    async function* endless(name, every) {
      try {
        for (let i = 1; ; i++) {
          await wait(every);
          yield name + i;
        }
      } finally {
        closed.push(name);
      }
    }
    async function* failing() {
      await wait(40);
      throw new Error("source failed");
    }
    const seen = [];
    let failure;
    (async () => {
      for await (const value of merge(endless("a", 30), endless("b", 25), failing())) seen.push(value);
    })().catch((error) => {
      failure = error;
    });
    await clock.tickAsync(200);
    expect(seen).toEqual(["b1", "a1"]);
    expect(failure).toBeInstanceOf(Error);
    expect(closed.sort()).toEqual(["a", "b"]);
    
  7. closes every source when the caller stops early
    const clock = useFakeTimers();
    const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    const closed = [];
    async function* endless(name, every) {
      try {
        for (let i = 1; ; i++) {
          await wait(every);
          yield name + i;
        }
      } finally {
        closed.push(name);
      }
    }
    const seen = [];
    let failure;
    (async () => {
      for await (const value of merge(endless("a", 30), endless("b", 50))) {
        seen.push(value);
        break;
      }
    })().catch((error) => {
      failure = error;
    });
    await clock.tickAsync(200);
    expect(failure).toBeUndefined();
    expect(seen).toEqual(["a1"]);
    expect(closed.sort()).toEqual(["a", "b"]);
    
  8. works with async iterators that have no return()
    const clock = useFakeTimers();
    const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    const counter = (name, every, count) => ({
      [Symbol.asyncIterator]() {
        let i = 0;
        return {
          async next() {
            if (i >= count) return { value: undefined, done: true };
            await wait(every);
            i += 1;
            return { value: name + i, done: false };
          },
        };
      },
    });
    const all = [];
    const firstOnly = [];
    let failure;
    (async () => {
      for await (const value of merge(counter("x", 10, 2), counter("y", 15, 1))) all.push(value);
      for await (const value of merge(counter("x", 10, 2), counter("y", 15, 1))) {
        firstOnly.push(value);
        break;
      }
    })().catch((error) => {
      failure = error;
    });
    await clock.tickAsync(100);
    expect(failure).toBeUndefined();
    expect(all).toEqual(["x1", "y1", "x2"]);
    expect(firstOnly).toEqual(["x1"]);
    

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).