modern-async
Version:
A modern tooling library for asynchronous operations using async/await, promises and async generators
674 lines (632 loc) • 17.9 kB
JavaScript
import { expect, test } from '@jest/globals'
import Queue from './Queue.mjs'
import Deferred from './Deferred.mjs'
import CancelledError from './CancelledError.mjs'
import { range } from 'itertools'
import asyncDelay from './asyncDelay.mjs'
test('Queue base 1', async () => {
const queue = new Queue(1)
expect(queue.running).toBe(0)
expect(queue.pending).toBe(0)
const promises = []
const callCount = {}
const ds = [...range(3)].map(() => new Deferred())
const dsi = [...range(3)].map(() => new Deferred())
for (const x of [...range(3)]) {
callCount[x] = 0
const p = queue.exec(async () => {
callCount[x] += 1
dsi[x].resolve()
await ds[x].promise
})
expect(p).toBeInstanceOf(Promise)
promises.push(p)
}
expect(promises.length).toBe(3)
await dsi[0].promise
expect(queue.running).toBe(1)
expect(queue.pending).toBe(2)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(0)
expect(callCount[2]).toBe(0)
ds[0].resolve()
await promises[0]
await dsi[1].promise
expect(queue.running).toBe(1)
expect(queue.pending).toBe(1)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(1)
expect(callCount[2]).toBe(0)
ds[1].resolve()
await promises[1]
await dsi[2].promise
expect(queue.running).toBe(1)
expect(queue.pending).toBe(0)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(1)
expect(callCount[2]).toBe(1)
ds[2].resolve()
await promises[2]
expect(queue.running).toBe(0)
expect(queue.pending).toBe(0)
})
test('Queue base 2', async () => {
const queue = new Queue(2)
expect(queue.running).toBe(0)
expect(queue.pending).toBe(0)
const promises = []
const callCount = {}
const ds = [...range(6)].map(() => new Deferred())
const dsi = [...range(6)].map(() => new Deferred())
for (const x of [...range(6)]) {
callCount[x] = 0
const p = queue.exec(async () => {
callCount[x] += 1
dsi[x].resolve()
await ds[x].promise
})
expect(p).toBeInstanceOf(Promise)
promises.push(p)
}
expect(promises.length).toBe(6)
await dsi[0].promise
await dsi[1].promise
expect(queue.running).toBe(2)
expect(queue.pending).toBe(4)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(1)
expect(callCount[2]).toBe(0)
expect(callCount[3]).toBe(0)
expect(callCount[4]).toBe(0)
expect(callCount[5]).toBe(0)
ds[0].resolve()
ds[1].resolve()
await promises[0]
await promises[1]
await dsi[3].promise
expect(queue.running).toBe(2)
expect(queue.pending).toBe(2)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(1)
expect(callCount[2]).toBe(1)
expect(callCount[3]).toBe(1)
expect(callCount[4]).toBe(0)
expect(callCount[5]).toBe(0)
ds[2].resolve()
ds[3].resolve()
await promises[2]
await promises[3]
await dsi[5].promise
expect(queue.running).toBe(2)
expect(queue.pending).toBe(0)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(1)
expect(callCount[2]).toBe(1)
expect(callCount[3]).toBe(1)
expect(callCount[4]).toBe(1)
expect(callCount[5]).toBe(1)
ds[4].resolve()
ds[5].resolve()
await promises[4]
await promises[5]
expect(queue.running).toBe(0)
expect(queue.pending).toBe(0)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(1)
expect(callCount[2]).toBe(1)
expect(callCount[3]).toBe(1)
expect(callCount[4]).toBe(1)
expect(callCount[5]).toBe(1)
})
test('Queue infinity', async () => {
const queue = new Queue(Number.POSITIVE_INFINITY)
expect(queue.concurrency).toBe(Number.POSITIVE_INFINITY)
expect(queue.running).toBe(0)
expect(queue.pending).toBe(0)
const promises = []
const callCount = {}
const ds = [...range(6)].map(() => new Deferred())
const dsi = [...range(6)].map(() => new Deferred())
for (const x of [...range(6)]) {
callCount[x] = 0
const p = queue.exec(async () => {
callCount[x] += 1
dsi[x].resolve()
await ds[x].promise
})
expect(p).toBeInstanceOf(Promise)
promises.push(p)
}
expect(promises.length).toBe(6)
for (const x of range(6)) {
await dsi[x].promise
}
expect(queue.running).toBe(6)
expect(queue.pending).toBe(0)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(1)
expect(callCount[2]).toBe(1)
expect(callCount[3]).toBe(1)
expect(callCount[4]).toBe(1)
expect(callCount[5]).toBe(1)
ds.forEach((d) => d.resolve())
await Promise.all(promises)
expect(queue.running).toBe(0)
expect(queue.pending).toBe(0)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(1)
expect(callCount[2]).toBe(1)
expect(callCount[3]).toBe(1)
expect(callCount[4]).toBe(1)
expect(callCount[5]).toBe(1)
})
test('Queue infinity race', async () => {
const queue = new Queue(Number.POSITIVE_INFINITY)
expect(queue.concurrency).toBe(Number.POSITIVE_INFINITY)
expect(queue.running).toBe(0)
expect(queue.pending).toBe(0)
const promises = []
const callCount = {}
const completeCount = {}
const ds = [...range(6)].map(() => new Deferred())
const dsi = [...range(6)].map(() => new Deferred())
for (const x of [...range(6)]) {
callCount[x] = 0
completeCount[x] = 0
const p = queue.exec(async () => {
callCount[x] += 1
dsi[x].resolve()
await ds[x].promise
completeCount[x] += 1
})
expect(p).toBeInstanceOf(Promise)
promises.push(p)
}
expect(promises.length).toBe(6)
for (const x of range(6)) {
await dsi[x].promise
}
expect(queue.running).toBe(6)
expect(queue.pending).toBe(0)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(1)
expect(callCount[2]).toBe(1)
expect(callCount[3]).toBe(1)
expect(callCount[4]).toBe(1)
expect(callCount[5]).toBe(1)
ds[2].resolve()
ds[4].resolve()
ds[5].resolve()
await Promise.race(promises)
expect(queue.running).toBe(6 - Object.values(completeCount).reduce((p, c) => p + c, 0))
expect(queue.pending).toBe(0)
ds[0].resolve()
ds[1].resolve()
ds[3].resolve()
await Promise.all(promises)
expect(queue.running).toBe(0)
expect(queue.pending).toBe(0)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(1)
expect(callCount[2]).toBe(1)
expect(callCount[3]).toBe(1)
expect(callCount[4]).toBe(1)
expect(callCount[5]).toBe(1)
})
test('Queue throws', async () => {
const queue = new Queue(2)
expect(queue.running).toBe(0)
expect(queue.pending).toBe(0)
const promises = []
const callCount = {}
const ds = [...range(6)].map(() => new Deferred())
const dsi = [...range(6)].map(() => new Deferred())
for (const x of [...range(6)]) {
callCount[x] = 0
const p = queue.exec(async () => {
callCount[x] += 1
dsi[x].resolve()
await ds[x].promise
if (x % 2 === 1) {
throw new Error()
}
})
expect(p).toBeInstanceOf(Promise)
promises.push(p.then(() => {
return 'success'
}, () => {
return 'fail'
}))
}
expect(promises.length).toBe(6)
await dsi[0].promise
await dsi[1].promise
expect(queue.running).toBe(2)
expect(queue.pending).toBe(4)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(1)
expect(callCount[2]).toBe(0)
expect(callCount[3]).toBe(0)
expect(callCount[4]).toBe(0)
expect(callCount[5]).toBe(0)
ds.forEach(d => d.resolve())
const results = await Promise.all(promises)
expect(queue.running).toBe(0)
expect(queue.pending).toBe(0)
expect(results[0]).toBe('success')
expect(results[1]).toBe('fail')
expect(results[2]).toBe('success')
expect(results[3]).toBe('fail')
expect(results[4]).toBe('success')
expect(results[5]).toBe('fail')
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(1)
expect(callCount[2]).toBe(1)
expect(callCount[3]).toBe(1)
expect(callCount[4]).toBe(1)
expect(callCount[5]).toBe(1)
})
test('Queue priority', async () => {
const queue = new Queue(1)
expect(queue.concurrency).toBe(1)
expect(queue.running).toBe(0)
expect(queue.pending).toBe(0)
const promises = []
const callCount = {}
callCount[0] = 0
const d0 = new Deferred()
const dsi0 = new Deferred()
promises.push(queue.exec(async () => {
callCount[0] += 1
dsi0.resolve()
await d0.promise
}, 0))
await dsi0.promise
expect(queue.running).toBe(1)
expect(queue.pending).toBe(0)
expect(callCount[0]).toBe(1)
callCount[1] = 0
const d1 = new Deferred()
const dsi1 = new Deferred()
promises.push(queue.exec(async () => {
callCount[1] += 1
dsi1.resolve()
await d1.promise
}, 1))
expect(queue.running).toBe(1)
expect(queue.pending).toBe(1)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(0)
callCount[2] = 0
const d2 = new Deferred()
const dsi2 = new Deferred()
promises.push(queue.exec(async () => {
callCount[2] += 1
dsi2.resolve()
await d2.promise
}, 2))
expect(queue.running).toBe(1)
expect(queue.pending).toBe(2)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(0)
expect(callCount[2]).toBe(0)
d0.resolve()
await promises[0]
await dsi2.promise
expect(queue.running).toBe(1)
expect(queue.pending).toBe(1)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(0)
expect(callCount[2]).toBe(1)
d2.resolve()
await promises[2]
await dsi1.promise
expect(queue.running).toBe(1)
expect(queue.pending).toBe(0)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(1)
expect(callCount[2]).toBe(1)
d1.resolve()
await promises[1]
expect(queue.running).toBe(0)
expect(queue.pending).toBe(0)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(1)
expect(callCount[2]).toBe(1)
})
test('Queue cancel', async () => {
const queue = new Queue(1)
const promises = []
const cancels = []
const callCount = {}
const ds = [...range(3)].map(() => new Deferred())
const dsi = [...range(3)].map(() => new Deferred())
for (const x in [...range(3)]) {
callCount[x] = 0
const [p, cancel] = queue.execCancellable(async () => {
callCount[x] += 1
dsi[x].resolve()
await ds[x].promise
return 'test'
}, 0)
const p2 = p.catch((e) => {
expect(e).toBeInstanceOf(CancelledError)
return e
})
promises.push(p2)
cancels.push(cancel)
}
expect(promises.length).toBe(3)
expect(cancels.length).toBe(3)
await dsi[0].promise
expect(queue.running).toBe(1)
expect(queue.pending).toBe(2)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(0)
expect(callCount[2]).toBe(0)
expect(cancels[0]()).toBe(false)
expect(queue.running).toBe(1)
expect(queue.pending).toBe(2)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(0)
expect(callCount[2]).toBe(0)
expect(cancels[1]()).toBe(true)
expect(queue.running).toBe(1)
expect(queue.pending).toBe(1)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(0)
expect(callCount[2]).toBe(0)
expect(cancels[1]()).toBe(false)
expect(queue.running).toBe(1)
expect(queue.pending).toBe(1)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(0)
expect(callCount[2]).toBe(0)
ds.forEach((d) => d.resolve())
const results = await Promise.all(promises)
expect(queue.running).toBe(0)
expect(queue.pending).toBe(0)
expect(results[0]).toBe('test')
expect(results[1]).toBeInstanceOf(CancelledError)
expect(results[2]).toBe('test')
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(0)
expect(callCount[2]).toBe(1)
expect(cancels[0]()).toBe(false)
expect(cancels[1]()).toBe(false)
expect(cancels[2]()).toBe(false)
})
test('Queue cancelAllPending', async () => {
const queue = new Queue(1)
const promises = []
const callCount = {}
const ds = [...range(3)].map(() => new Deferred())
const dsi = [...range(6)].map(() => new Deferred())
for (const x in [...range(3)]) {
callCount[x] = 0
const p = queue.exec(async () => {
callCount[x] += 1
dsi[x].resolve()
await ds[x].promise
return 'test'
}, 0).catch((e) => {
expect(e).toBeInstanceOf(CancelledError)
return e
})
expect(p).toBeInstanceOf(Promise)
promises.push(p)
}
expect(promises.length).toBe(3)
await dsi[0].promise
expect(queue.running).toBe(1)
expect(queue.pending).toBe(2)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(0)
expect(callCount[2]).toBe(0)
queue.cancelAllPending()
expect(queue.running).toBe(1)
expect(queue.pending).toBe(0)
ds[0].resolve()
ds[1].resolve()
ds[2].resolve()
expect(await promises[0]).toBe('test')
expect(await promises[1]).toBeInstanceOf(CancelledError)
expect(await promises[2]).toBeInstanceOf(CancelledError)
expect(queue.running).toBe(0)
expect(queue.pending).toBe(0)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(0)
expect(callCount[2]).toBe(0)
})
test('Queue concurrency 1', async () => {
const mutex = new Queue(1)
expect(mutex.running).toBe(0)
expect(mutex.pending).toBe(0)
const promises = []
const callCount = {}
const ds = [...range(3)].map(() => new Deferred())
const dsi = [...range(6)].map(() => new Deferred())
for (const x of [...range(3)]) {
callCount[x] = 0
const p = mutex.exec(async () => {
callCount[x] += 1
dsi[x].resolve()
await ds[x].promise
})
expect(p).toBeInstanceOf(Promise)
promises.push(p)
}
expect(promises.length).toBe(3)
await dsi[0].promise
expect(mutex.running).toBe(1)
expect(mutex.pending).toBe(2)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(0)
expect(callCount[2]).toBe(0)
ds[0].resolve()
await promises[0]
await dsi[1].promise
expect(mutex.running).toBe(1)
expect(mutex.pending).toBe(1)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(1)
expect(callCount[2]).toBe(0)
ds[1].resolve()
await promises[1]
await dsi[2].promise
expect(mutex.running).toBe(1)
expect(mutex.pending).toBe(0)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(1)
expect(callCount[2]).toBe(1)
ds[2].resolve()
await promises[2]
expect(mutex.running).toBe(0)
expect(mutex.pending).toBe(0)
})
test('Queue concurrency 1 priority', async () => {
const mutex = new Queue(1)
expect(mutex.running).toBe(0)
expect(mutex.pending).toBe(0)
const promises = []
const callCount = {}
const ds = [...range(3)].map(() => new Deferred())
const dsi = [...range(6)].map(() => new Deferred())
for (const x of [...range(3)]) {
callCount[x] = 0
const p = mutex.exec(async () => {
callCount[x] += 1
dsi[x].resolve()
await ds[x].promise
}, 10)
expect(p).toBeInstanceOf(Promise)
promises.push(p)
}
expect(promises.length).toBe(3)
await dsi[0].promise
expect(mutex.running).toBe(1)
expect(mutex.pending).toBe(2)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(0)
expect(callCount[2]).toBe(0)
ds[0].resolve()
await promises[0]
await dsi[1].promise
expect(mutex.running).toBe(1)
expect(mutex.pending).toBe(1)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(1)
expect(callCount[2]).toBe(0)
ds[1].resolve()
await promises[1]
await dsi[2].promise
expect(mutex.running).toBe(1)
expect(mutex.pending).toBe(0)
expect(callCount[0]).toBe(1)
expect(callCount[1]).toBe(1)
expect(callCount[2]).toBe(1)
ds[2].resolve()
await promises[2]
expect(mutex.running).toBe(0)
expect(mutex.pending).toBe(0)
})
test('Queue all resolve in micro tasks', async () => {
const del = asyncDelay()
const queue = new Queue(1)
const ps = []
ps.push(queue.exec(async () => {}))
ps.push(queue.exec(async () => {}))
ps.push(queue.exec(async () => {}))
let finished = false
Promise.all(ps).then(() => {
finished = true
})
await del
expect(finished).toBe(true)
})
test('Queue infinity all resolve in micro tasks', async () => {
const del = asyncDelay()
const queue = new Queue(Number.POSITIVE_INFINITY)
const ps = []
ps.push(queue.exec(async () => {}))
ps.push(queue.exec(async () => {}))
ps.push(queue.exec(async () => {}))
let finished = false
Promise.all(ps).then(() => {
finished = true
})
await del
expect(finished).toBe(true)
})
test('Queue always call in micro task sync', async () => {
const calls = []
const queue = new Queue(1)
const p = queue.exec(() => {
calls.push('exec')
})
calls.push('after exec')
await p
expect(calls).toStrictEqual(['after exec', 'exec'])
})
test('Queue infinity always call in micro task sync', async () => {
const calls = []
const queue = new Queue(Number.POSITIVE_INFINITY)
const p = queue.exec(() => {
calls.push('exec')
})
calls.push('after exec')
await p
expect(calls).toStrictEqual(['after exec', 'exec'])
})
test('Queue always call in micro task async', async () => {
const calls = []
const queue = new Queue(1)
const p = queue.exec(async () => {
calls.push('exec')
})
calls.push('after exec')
await p
expect(calls).toStrictEqual(['after exec', 'exec'])
})
test('Queue infinity always call in micro task async', async () => {
const calls = []
const queue = new Queue(Number.POSITIVE_INFINITY)
const p = queue.exec(async () => {
calls.push('exec')
})
calls.push('after exec')
await p
expect(calls).toStrictEqual(['after exec', 'exec'])
})
test('Queue cancel just scheduled', async () => {
let passed = false
const queue = new Queue(1)
const [p, cancel] = queue.execCancellable(() => {
passed = true
})
expect(cancel()).toBe(true)
try {
await p
expect(true).toBe(false)
} catch (e) {
expect(e instanceof CancelledError).toBe(true)
}
expect(passed).toBe(false)
})
test('Queue multiple priorities sync', async () => {
const calls = []
const queue = new Queue(1)
const p0 = queue.exec(() => {
calls.push(0)
}, 0)
const p1 = queue.exec(() => {
calls.push(1)
}, 1)
const p2 = queue.exec(() => {
calls.push(2)
}, 2)
await p0
await p1
await p2
expect(calls).toStrictEqual([2, 1, 0])
})