UNPKG

prom-utils

Version:

Promise utilities: rate limiting, queueing/batching, defer, etc.

928 lines (867 loc) 28.6 kB
import { setTimeout } from 'node:timers/promises' import { describe, expect, test } from 'vitest' import { batchQueue, batchQueueParallel, defer, multiplex, OptionsError, pacemaker, pausable, raceTimeout, rateLimit, throughputLimiter, TIMEOUT, TimeoutError, waitUntil, } from './fns' describe('rateLimit', () => { test('should add up to limit - 1 promises without delay', async () => { expect.assertions(1) const limiter = rateLimit(3) const startTime = new Date().getTime() await limiter.add(setTimeout(1000)) await limiter.add(setTimeout(1000)) const endTime = new Date().getTime() const elapsed = endTime - startTime expect(elapsed).toBeLessThan(100) }) test('should wait for one promise to resolve when limit is reached', async () => { expect.assertions(1) const limiter = rateLimit(3) const startTime = new Date().getTime() await limiter.add(setTimeout(1000)) await limiter.add(setTimeout(1000)) await limiter.add(setTimeout(1000)) await limiter.finish() const endTime = new Date().getTime() const elapsed = endTime - startTime expect(elapsed).toBeGreaterThan(900) }) test('should finish awaiting remaining promises', async () => { expect.assertions(4) // Pass type variable const limiter = rateLimit<string>(3) const done: string[] = [] const createProm = () => setTimeout(1000, 'done').then((p) => { done.push(p) return p }) const startTime = new Date().getTime() for (let i = 0; i < 5; i++) { await limiter.add(createProm()) } await limiter.finish() const endTime = new Date().getTime() const elapsed = endTime - startTime expect(elapsed).toBeGreaterThanOrEqual(2000) expect(elapsed).toBeLessThan(3000) expect(done.length).toBe(5) expect(limiter.length).toBe(0) }) // This test is here to ensure that the rejected promise doesn't // throw an UnhandledPromiseRejection exception. test('rejections should not bubble up .add', async () => { expect.assertions(1) const limiter = rateLimit(3) // Shorter timeout to make sure that this rejects before the other promises resolve await limiter.add( setTimeout(5).then(() => { throw new Error('rejectedPromise') }) ) await limiter.add(setTimeout(10)) await limiter.finish() await setTimeout(20) // Check that the rejected promise was removed from the set. expect(limiter.length).toBe(0) }) test('rejections should not bubble up .finish', () => { expect.assertions(1) expect(async () => { const limiter = rateLimit(3) await limiter.add(setTimeout(10)) await limiter.add( setTimeout(10).then(() => { throw new Error('rejectedPromise') }) ) // Call finish before reaching the limit of 3 await limiter.finish() }).not.toThrow('rejectedPromise') }) test('should allow at most maxItemsPerPeriod if option is set', async () => { expect.assertions(1) // 1 unit per sec const limiter = rateLimit(3, { maxItemsPerPeriod: 1 }) const startTime = new Date().getTime() await limiter.add(setTimeout(10)) await limiter.add(setTimeout(10)) await limiter.add(setTimeout(10)) await limiter.finish() const endTime = new Date().getTime() const elapsed = endTime - startTime expect(elapsed).toBeGreaterThanOrEqual(3000) }) test('should handle infinite concurrency while limiting via maxItemsPerPeriod', async () => { expect.assertions(2) // Simulate an API that allows 2 requests per 100 ms const limiter = rateLimit(Infinity, { maxItemsPerPeriod: 2, period: 100, }) const startTime = new Date().getTime() await limiter.add(setTimeout(10)) await limiter.add(setTimeout(10)) await limiter.add(setTimeout(10)) await limiter.finish() const endTime = new Date().getTime() const elapsed = endTime - startTime expect(elapsed).toBeGreaterThan(100) expect(elapsed).toBeLessThan(150) }) test('should not wait to end of period when bypass is set to true', async () => { expect.assertions(1) // 3 units per 10 ms const limiter = rateLimit(3, { maxItemsPerPeriod: 3, period: 1000 }) const startTime = new Date().getTime() await limiter.add(setTimeout(100)) await limiter.add(setTimeout(100)) await limiter.add(setTimeout(100), { bypass: true }) await limiter.finish() const endTime = new Date().getTime() const elapsed = endTime - startTime expect(elapsed).toBeLessThan(150) }) test('should handle multiple rate limiters', async () => { expect.assertions(1) // 2 requests per 100ms AND 5 requests per 500ms const limiter = rateLimit( Infinity, { maxItemsPerPeriod: 2, period: 100 }, { maxItemsPerPeriod: 5, period: 500 } ) const startTime = new Date().getTime() // Add 6 requests - should be limited by the 5 per 500ms limiter for (let i = 0; i < 6; i++) { await limiter.add(setTimeout(10)) } await limiter.finish() const endTime = new Date().getTime() const elapsed = endTime - startTime // Should take at least 500ms due to the second limiter expect(elapsed).toBeGreaterThanOrEqual(500) }) test('should return stats for all rate limiters', async () => { expect.assertions(2) const limiter = rateLimit( Infinity, { maxItemsPerPeriod: 10, period: 1000 }, { maxItemsPerPeriod: 100, period: 60000 } ) await limiter.add(setTimeout(10)) await limiter.add(setTimeout(10)) await limiter.add(setTimeout(10)) await limiter.finish() const stats = limiter.getStats() // Should return an array with rates for each limiter expect(stats.itemsPerPeriod).toBeInstanceOf(Array) expect(stats.itemsPerPeriod.length).toBe(2) }) test('should return empty array for getStats when no rate limiters', async () => { expect.assertions(1) const limiter = rateLimit(3) await limiter.add(setTimeout(10)) await limiter.finish() const stats = limiter.getStats() expect(stats.itemsPerPeriod).toEqual([]) }) test('should enforce the most restrictive rate limiter', async () => { expect.assertions(1) // First limiter: 10 per 100ms (very permissive) // Second limiter: 2 per 1000ms (very restrictive) const limiter = rateLimit( Infinity, { maxItemsPerPeriod: 10, period: 100 }, { maxItemsPerPeriod: 2, period: 1000 } ) const startTime = new Date().getTime() // Add 3 requests - should be limited by the 2 per 1000ms limiter await limiter.add(setTimeout(10)) await limiter.add(setTimeout(10)) await limiter.add(setTimeout(10)) await limiter.finish() const endTime = new Date().getTime() const elapsed = endTime - startTime // Should take at least 1000ms due to the restrictive limiter expect(elapsed).toBeGreaterThanOrEqual(1000) }) }) describe('batchQueue', () => { test('should batch items up to batchSize', async () => { expect.assertions(4) const calls: any[] = [] const fn = async (records: string[]) => { calls.push(records) return records.length } const batchSize = 2 const queue = batchQueue(fn, { batchSize }) const records = ['Joe', 'Frank', 'Bob'] for (const record of records) { await queue.enqueue(record) } await queue.flush() expect(calls).toEqual([['Joe', 'Frank'], ['Bob']]) expect(queue.lastResult).toBe(1) expect(queue.lastFlush).toEqual({ batchSize }) expect(queue.length).toBe(0) }) test('should flush queue if timeout is reached before batchSize', async () => { expect.assertions(2) const calls: any[] = [] const fn = async (records: string[]) => { calls.push(records) } const batchSize = 2 const timeout = 50 const queue = batchQueue(fn, { batchSize, timeout }) const records = ['Joe', 'Frank', 'Bob'] for (const record of records) { await queue.enqueue(record) await setTimeout(75) } await queue.flush() expect(calls).toEqual([['Joe'], ['Frank'], ['Bob']]) expect(queue.lastFlush).toEqual({ timeout }) }) test('should reset timer if batchSize is reached before timeout', async () => { expect.assertions(1) const calls: any[] = [] const fn = async (records: string[]) => { calls.push(records) } const batchSize = 2 const timeout = 100 const queue = batchQueue(fn, { batchSize, timeout }) const records = ['Joe', 'Frank', 'Bob'] for (const record of records) { await queue.enqueue(record) await setTimeout(20) } await queue.flush() expect(calls).toEqual([['Joe', 'Frank'], ['Bob']]) }) test('fn should only be called once if timeout flush was triggered before queue.flush()', async () => { expect.assertions(1) const calls: any[] = [] const fn = async (records: string[]) => { calls.push(records) await setTimeout(100) } const batchSize = 5 const timeout = 20 const queue = batchQueue(fn, { batchSize, timeout }) const records = ['Joe', 'Frank', 'Bob'] for (const record of records) { await queue.enqueue(record) } await setTimeout(50) await queue.flush() expect(calls).toEqual([['Joe', 'Frank', 'Bob']]) }) test('should flush queue if batchBytes is reached before batchSize', async () => { expect.assertions(2) const calls: any[] = [] const fn = async (records: string[]) => { calls.push(records) } const batchSize = 3 const batchBytes = 8 const queue = batchQueue(fn, { batchSize, batchBytes }) const records = ['Joe', 'Frank', 'Bob', 'Tim'] for (const record of records) { await queue.enqueue(record) } await queue.flush() expect(calls).toEqual([ ['Joe', 'Frank'], ['Bob', 'Tim'], ]) expect(queue.lastFlush).toEqual({ batchBytes }) }) test('should flush queue if batchSize is reached before batchBytes', async () => { expect.assertions(1) const calls: any[] = [] const fn = async (records: string[]) => { calls.push(records) } const batchSize = 3 const batchBytes = 100 const queue = batchQueue(fn, { batchSize, batchBytes }) const records = ['Joe', 'Frank', 'Bob', 'Tim'] for (const record of records) { await queue.enqueue(record) } await queue.flush() expect(calls).toEqual([['Joe', 'Frank', 'Bob'], ['Tim']]) }) test('flush should govern throughput - maxItemsPerSec', async () => { expect.assertions(1) const calls: any[] = [] const fn = async (records: string[]) => { calls.push(records) await setTimeout(100) } const batchSize = 3 const maxItemsPerSec = 5 const queue = batchQueue(fn, { batchSize, maxItemsPerSec }) const records = ['Joe', 'Frank', 'Bob', 'Tim'] const startTime = new Date().getTime() for (const record of records) { await queue.enqueue(record) } await queue.flush() const endTime = new Date().getTime() const elapsed = endTime - startTime // Total time would be around 200 ms without throughput governing expect(elapsed).toBeGreaterThan(500) }) test('flush should govern throughput - maxBytesPerSec', async () => { expect.assertions(1) const calls: any[] = [] const fn = async (records: string[]) => { calls.push(records) await setTimeout(100) } const batchSize = 3 const maxBytesPerSec = 50 const queue = batchQueue(fn, { batchSize, maxBytesPerSec }) const records = ['Joe', 'Frank', 'Bob', 'Tim'] const startTime = new Date().getTime() for (const record of records) { await queue.enqueue(record) } await queue.flush() const endTime = new Date().getTime() const elapsed = endTime - startTime // Total time would be around 200 ms without throughput governing expect(elapsed).toBeGreaterThan(500) }) test('flush should govern throughput - maxItemsPerSec & maxBtyesPerSec', async () => { expect.assertions(3) const calls: any[] = [] const fn = async (records: string[]) => { calls.push(records) await setTimeout(100) } const batchSize = 3 const maxItemsPerSec = 5 const maxBytesPerSec = 50 const queue = batchQueue(fn, { batchSize, maxItemsPerSec, maxBytesPerSec }) const records = ['Joe', 'Frank', 'Bob', 'Tim'] const startTime = new Date().getTime() for (const record of records) { await queue.enqueue(record) } await queue.flush() const endTime = new Date().getTime() const elapsed = endTime - startTime // Total time would be around 200 ms without throughput governing expect(elapsed).toBeGreaterThan(500) // Stats const stats = queue.getStats() expect(stats.bytesPerSec).toBeLessThan(100) expect(stats.itemsPerSec).toBeLessThan(10) }) }) describe('batchQueueParallel', () => { test('should batch items up to batchSize', () => { expect.assertions(3) const calls: any[] = [] const fn = (records: string[]) => { calls.push([...records]) // Copy array since it gets reset } const batchSize = 2 const queue = batchQueueParallel(fn, { batchSize }) const records = ['Joe', 'Frank', 'Bob'] for (const record of records) { queue.enqueue(record) } queue.flush() expect(calls).toEqual([['Joe', 'Frank'], ['Bob']]) expect(queue.length).toBe(0) expect(calls.length).toBe(2) }) test('should flush automatically when batchSize is reached', () => { expect.assertions(3) const calls: any[] = [] const fn = (records: string[]) => { calls.push([...records]) } const batchSize = 3 const queue = batchQueueParallel(fn, { batchSize }) const records = ['Joe', 'Frank', 'Bob', 'Tim', 'Alice'] for (const record of records) { queue.enqueue(record) } // Should have auto-flushed once when batchSize was reached expect(calls).toEqual([['Joe', 'Frank', 'Bob']]) expect(queue.length).toBe(2) // Tim and Alice still in queue expect(queue.lastFlush).toEqual({ batchSize }) }) test('should handle batchBytes option', () => { expect.assertions(4) const calls: any[] = [] const fn = (records: string[]) => { calls.push([...records]) } // Each string is roughly 3-5 bytes, so batchBytes of 10 should trigger with 3-4 items const batchBytes = 10 const queue = batchQueueParallel(fn, { batchBytes }) const records = ['Joe', 'Frank', 'Bob', 'Tim'] for (const record of records) { queue.enqueue(record) } // Should have auto-flushed when batchBytes was reached expect(calls.length).toBeGreaterThan(0) expect(calls[0].length).toBeGreaterThan(0) expect(queue.length).toBeGreaterThanOrEqual(0) expect(queue.lastFlush).toEqual({ batchBytes }) }) test('should flush manually with remaining items', () => { expect.assertions(3) const calls: any[] = [] const fn = (records: string[]) => { calls.push([...records]) } const batchSize = 3 const queue = batchQueueParallel(fn, { batchSize }) const records = ['Joe', 'Frank'] for (const record of records) { queue.enqueue(record) } expect(queue.length).toBe(2) queue.flush() expect(calls).toEqual([['Joe', 'Frank']]) expect(queue.length).toBe(0) }) test('calling flush on an empty queue should not call fn', () => { expect.assertions(2) const calls: any[] = [] const fn = (records: string[]) => { calls.push([...records]) } const queue = batchQueueParallel(fn) queue.flush() expect(calls.length).toBe(0) expect(queue.length).toBe(0) }) test('should be safe for concurrent access', async () => { expect.assertions(4) const calls: any[] = [] const fn = async (records: string[]) => { // Simulate async processing await setTimeout(10) calls.push([...records]) console.log('fn finished: %o', records) } const batchSize = 2 const queue = batchQueueParallel(fn, { batchSize }) // Simulate concurrent enqueuing with async delays queue.enqueue('A') await setTimeout(5) queue.enqueue('B') // Should trigger flush queue.enqueue('C') await setTimeout(5) queue.enqueue('D') // Should trigger another flush queue.enqueue('E') queue.flush() // Wait for async fn calls to complete await Promise.all(queue.results) expect(calls.length).toBe(3) expect(calls[0]).toEqual(['A', 'B']) expect(calls[1]).toEqual(['C', 'D']) expect(calls[2]).toEqual(['E']) }) }) describe('pausable', () => { test('should pause and resume', async () => { expect.assertions(2) const shouldProcess = pausable() const processed: string[] = [] const processRecord = async (record: string) => { processed.push(record) await setTimeout(50) } const records = ['Joe', 'Frank', 'Bob'] global.setTimeout(shouldProcess.pause, 100) global.setTimeout(shouldProcess.resume, 150) const startTime = new Date().getTime() for (const record of records) { await shouldProcess.maybeBlock() await processRecord(record) } const endTime = new Date().getTime() const elapsed = endTime - startTime expect(elapsed).toBeGreaterThanOrEqual(200) expect(records).toEqual(['Joe', 'Frank', 'Bob']) }) test('should pause and resume after timeout', async () => { expect.assertions(2) const shouldProcess = pausable(50) const processed: string[] = [] const processRecord = async (record: string) => { processed.push(record) await setTimeout(50) } const records = ['Joe', 'Frank', 'Bob'] global.setTimeout(shouldProcess.pause, 100) const startTime = new Date().getTime() for (const record of records) { await shouldProcess.maybeBlock() await processRecord(record) } const endTime = new Date().getTime() const elapsed = endTime - startTime expect(elapsed).toBeGreaterThanOrEqual(200) expect(records).toEqual(['Joe', 'Frank', 'Bob']) }) test('isPaused works', () => { const shouldProcess = pausable() expect(shouldProcess.isPaused).toBe(false) shouldProcess.pause() expect(shouldProcess.isPaused).toBe(true) shouldProcess.resume() expect(shouldProcess.isPaused).toBe(false) }) }) describe('defer', () => { test('should defer', async () => { expect.assertions(1) const delay = (milliseconds: number) => { const deferred = defer() global.setTimeout(deferred.done, milliseconds) return deferred.promise } const startTime = new Date().getTime() await delay(100) const endTime = new Date().getTime() const elapsed = endTime - startTime expect(elapsed).toBeGreaterThanOrEqual(100) }) }) describe('pacemaker', () => { test('should call heartbeatFn 2 times', async () => { expect.assertions(2) let count = 0 const heartbeatFn = () => { console.log('heartbeat') count++ } const result = await pacemaker(heartbeatFn, setTimeout(250, 'done'), 100) expect(count).toBe(2) expect(result).toBe('done') }) test('should throw', async () => { expect.assertions(1) const heartbeatFn = () => { console.log('heartbeat') } const promFn = async () => { await setTimeout(150) throw 'fail' } await expect(pacemaker(heartbeatFn, promFn(), 100)).rejects.toMatch('fail') }) }) describe('waitUntil', () => { test('should wait until async pred returns truthy and return the result', async () => { expect.assertions(1) let counter = 0 const check = async () => { await setTimeout(100) return counter++ === 1 } await expect(waitUntil(check)).resolves.toBe(true) }) test('should wait until pred returns truthy and return the result', async () => { expect.assertions(1) let isTruthy = false global.setTimeout(() => { isTruthy = true }, 250) await expect(waitUntil(() => isTruthy)).resolves.toBe(true) }) test('should handle infinite timeout and return the result', async () => { expect.assertions(1) let isTruthy = false global.setTimeout(() => { isTruthy = true }, 250) await expect( waitUntil(async () => isTruthy, { timeout: Infinity }) ).resolves.toBe(true) }) test('should return string value when predicate returns string', async () => { expect.assertions(1) let value: string | null = null global.setTimeout(() => { value = 'ready' }, 250) await expect(waitUntil(() => value)).resolves.toBe('ready') }) test('should throw TimeoutError if the timeout expires', async () => { expect.assertions(1) let isTruthy = false global.setTimeout(() => { isTruthy = true }, 250) await expect( waitUntil(async () => isTruthy, { timeout: 100 }) ).rejects.toThrow(new TimeoutError('Did not complete in 100 ms')) }) test('rejects when pred throws', async () => { expect.assertions(1) await expect( waitUntil( async () => { throw 'fail' }, { timeout: 100 } ) ).rejects.toMatch('fail') }) }) describe('throughputLimiter', () => { test('call to throttle with empty sliding window completes without delay', async () => { expect.assertions(2) const limiter = throughputLimiter(100) const startTime = new Date().getTime() expect(limiter.getCurrentRate()).toBe(0) await limiter.throttle() const endTime = new Date().getTime() expect(endTime - startTime).toBeLessThan(5) }) test('second call to throttle completes without delay if maxUnitsPerTime is Infinity', async () => { expect.assertions(1) const limiter = throughputLimiter(Infinity) const startTime = new Date().getTime() await limiter.throttle() limiter.append(1000) await limiter.throttle() const endTime = new Date().getTime() expect(endTime - startTime).toBeLessThan(5) }) test('throughput should be throttled', async () => { expect.assertions(2) const limiter = throughputLimiter(500) const startTime = new Date().getTime() await limiter.throttleAndAppend(250) await setTimeout(100) await limiter.throttleAndAppend(250) await setTimeout(100) await limiter.throttleAndAppend(250) await setTimeout(100) await limiter.throttleAndAppend(250) const endTime = new Date().getTime() const elapsed = endTime - startTime expect(elapsed).toBeGreaterThanOrEqual(1000) expect(limiter.getCurrentRate()).toBeLessThan(1000) }) test('minWindowLength option should work as expected', async () => { expect.assertions(1) const limiter = throughputLimiter(4, { minWindowLength: 4 }) const startTime = new Date().getTime() await limiter.throttleAndAppend(1) await limiter.throttleAndAppend(1) await limiter.throttleAndAppend(1) await limiter.throttleAndAppend(1) const endTime = new Date().getTime() const elapsed = endTime - startTime expect(elapsed).toBeLessThan(5) }) test('maxWindowLength option should work as expected', async () => { expect.assertions(1) const limiter = throughputLimiter(500, { maxWindowLength: 2, }) const startTime = new Date().getTime() await limiter.throttleAndAppend(250) await setTimeout(100) await limiter.throttleAndAppend(250) await setTimeout(100) await limiter.throttleAndAppend(250) await setTimeout(100) await limiter.throttleAndAppend(250) const endTime = new Date().getTime() const elapsed = endTime - startTime expect(elapsed).toBeGreaterThanOrEqual(1500) }) test('period option should work as expected', async () => { expect.assertions(2) // 5 units per 100 ms const limiter = throughputLimiter(5, { period: 100 }) const startTime = new Date().getTime() await limiter.throttleAndAppend(5) await setTimeout(100) await limiter.throttleAndAppend(5) await setTimeout(100) await limiter.throttleAndAppend(5) const endTime = new Date().getTime() const elapsed = endTime - startTime expect(elapsed).toBeGreaterThanOrEqual(200) expect(elapsed).toBeLessThan(250) }) test('expireAfter option should work as expected', async () => { expect.assertions(2) // 5 units per 100 ms, with sliding window entries resetting after 50 ms const limiter = throughputLimiter(5, { period: 100, expireAfter: 50, }) const startTime = new Date().getTime() await limiter.throttleAndAppend(5) await setTimeout(50) await limiter.throttleAndAppend(5) await setTimeout(50) await limiter.throttleAndAppend(5) const endTime = new Date().getTime() const elapsed = endTime - startTime expect(elapsed).toBeGreaterThanOrEqual(100) expect(elapsed).toBeLessThan(150) }) test('should throw if both maxWindowLength and expireAfter are Inifinity', async () => { expect(() => { throughputLimiter(5, { maxWindowLength: Infinity, expireAfter: Infinity, }) }).toThrow(OptionsError) }) }) describe('raceTimeout', () => { test('It should return promise if resolved before timeout', async () => { const winner = await raceTimeout(setTimeout(5, 'done'), 10) expect(winner).toBe('done') }) test('It should return the TIMEOUT symbol', async () => { const winner = await raceTimeout(setTimeout(10, 'done'), 5) expect(winner).toBe(TIMEOUT) }) }) describe('multiplex', () => { test('should handle async iterables', async () => { expect.assertions(1) const iterable1 = { async *[Symbol.asyncIterator]() { yield 'a' yield 'b' }, } const iterable2 = { async *[Symbol.asyncIterator]() { yield 'c' yield 'd' }, } const results: string[] = [] for await (const value of multiplex(iterable1, iterable2)) { results.push(value) } expect(results.sort()).toEqual(['a', 'b', 'c', 'd']) }) test('should yield values as they become available', async () => { expect.assertions(1) async function* slowGen() { await setTimeout(100) yield 'slow' } async function* fastGen() { yield 'fast1' yield 'fast2' } const results: string[] = [] for await (const value of multiplex(slowGen(), fastGen())) { results.push(value) } // Should get fast values before slow value expect(results).toEqual(['fast1', 'fast2', 'slow']) }) test('should handle empty iterators', async () => { expect.assertions(1) async function* emptyGen() { // yields nothing } async function* gen() { yield 1 yield 2 } const results: number[] = [] for await (const value of multiplex(emptyGen(), gen())) { results.push(value) } expect(results).toEqual([1, 2]) }) test('should throw error if any iterator throws', async () => { expect.assertions(1) async function* errorGen() { yield 1 throw new Error('Iterator error') } async function* normalGen() { yield 2 yield 3 } await expect(async () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars for await (const _val of multiplex(errorGen(), normalGen())) { // consume values } }).rejects.toThrow('Iterator error') }) test('should clean up iterators on early exit', async () => { expect.assertions(1) let cleaned = false async function* gen() { try { yield 1 yield 2 yield 3 } finally { cleaned = true } } for await (const value of multiplex(gen())) { if (value === 1) break } // Give cleanup time to run await setTimeout(10) expect(cleaned).toBe(true) }) test('should handle iterators with different speeds', async () => { expect.assertions(1) async function* gen1() { yield 1 await setTimeout(50) yield 2 await setTimeout(50) yield 3 } async function* gen2() { await setTimeout(25) yield 4 await setTimeout(25) yield 5 await setTimeout(25) yield 6 } const results: number[] = [] for await (const value of multiplex(gen1(), gen2())) { results.push(value) } // All values should be present expect(results.sort()).toEqual([1, 2, 3, 4, 5, 6]) }) })