@uppy/utils
Version:
Shared utility functions for Uppy Core and plugins maintained by the Uppy team.
255 lines (199 loc) • 7.2 kB
text/typescript
import { describe, expect, it } from 'vitest'
import delay from './delay.js'
import { TaskQueue } from './TaskQueue.js'
describe('TaskQueue', () => {
it('runs queued tasks in FIFO order after resume', async () => {
const queue = new TaskQueue({ concurrency: 1 })
queue.pause()
const order: string[] = []
const makeTask = (label: string) => async () => {
order.push(label)
await delay(5)
return label
}
const first = queue.add(makeTask('first'))
const second = queue.add(makeTask('second'))
const third = queue.add(makeTask('third'))
expect(queue.pending).toBe(3)
queue.resume()
await Promise.all([first, second, third])
expect(order).toEqual(['first', 'second', 'third'])
})
it('aborts a queued task without executing it', async () => {
const queue = new TaskQueue({ concurrency: 1 })
const runningBlocker = Promise.withResolvers<void>()
const started: string[] = []
const running = queue.add(async () => {
started.push('first')
await runningBlocker.promise
})
const queued = queue.add(async () => {
started.push('second')
return 'second'
})
const reason = new Error('nope')
queued.abort(reason)
await expect(queued).rejects.toBe(reason)
runningBlocker.resolve()
await running
expect(started).toEqual(['first'])
expect(queue.pending).toBe(0)
})
it('rejects a running task when aborted before it resolves', async () => {
const queue = new TaskQueue({ concurrency: 1 })
const deferred = Promise.withResolvers<string>()
const promise = queue.add(async () => deferred.promise)
const reason = new Error('stop')
promise.abort(reason)
deferred.resolve('ok')
await expect(promise).rejects.toBe(reason)
})
it('clear rejects pending tasks but leaves running tasks alone', async () => {
const queue = new TaskQueue({ concurrency: 1 })
const runningDeferred = Promise.withResolvers<string>()
const running = queue.add(async () => runningDeferred.promise)
const queued1 = queue.add(async () => 'queued1')
const queued2 = queue.add(async () => 'queued2')
const reason = new Error('cleared')
queue.clear(reason)
await expect(queued1).rejects.toBe(reason)
await expect(queued2).rejects.toBe(reason)
runningDeferred.resolve('ok')
await expect(running).resolves.toBe('ok')
expect(queue.pending).toBe(0)
})
it('wrapPromiseFunction queues work and preserves arguments', async () => {
const queue = new TaskQueue({ concurrency: 1 })
queue.pause()
const order: string[] = []
const makeTask = (label: string) => async () => {
order.push(label)
return label
}
const wrapped = queue.wrapPromiseFunction(async (value: string) => {
order.push(`wrapped:${value}`)
return value.toUpperCase()
})
const first = queue.add(makeTask('first'))
const wrappedPromise = wrapped('hello')
expect(typeof wrappedPromise.abort).toBe('function')
queue.resume()
await expect(first).resolves.toBe('first')
await expect(wrappedPromise).resolves.toBe('HELLO')
expect(order).toEqual(['first', 'wrapped:hello'])
})
it('updates concurrency via setter and starts additional tasks', async () => {
const queue = new TaskQueue({ concurrency: 1 })
const started1 = Promise.withResolvers<void>()
const started2 = Promise.withResolvers<void>()
const blocker1 = Promise.withResolvers<void>()
const blocker2 = Promise.withResolvers<void>()
const first = queue.add(async () => {
started1.resolve()
await blocker1.promise
return 'first'
})
const second = queue.add(async () => {
started2.resolve()
await blocker2.promise
return 'second'
})
await started1.promise
expect(queue.running).toBe(1)
expect(queue.pending).toBe(1)
queue.concurrency = 2
await started2.promise
expect(queue.running).toBe(2)
expect(queue.pending).toBe(0)
blocker1.resolve()
blocker2.resolve()
await expect(first).resolves.toBe('first')
await expect(second).resolves.toBe('second')
})
it('aborts when abortOn signal fires while queued', async () => {
const queue = new TaskQueue({ concurrency: 1 })
queue.pause()
const controller = new AbortController()
const promise = queue.add(async () => 'ok')
const returned = promise.abortOn(controller.signal)
expect(returned).toBe(promise)
const reason = new Error('signal abort')
controller.abort(reason)
await expect(promise).rejects.toBe(reason)
expect(queue.pending).toBe(0)
})
it('runs tasks concurrently up to the concurrency limit', async () => {
const queue = new TaskQueue({ concurrency: 2 })
const started1 = Promise.withResolvers<void>()
const started2 = Promise.withResolvers<void>()
const started3 = Promise.withResolvers<void>()
const blocker1 = Promise.withResolvers<void>()
const blocker2 = Promise.withResolvers<void>()
const blocker3 = Promise.withResolvers<void>()
const first = queue.add(async () => {
started1.resolve()
await blocker1.promise
return 'first'
})
const second = queue.add(async () => {
started2.resolve()
await blocker2.promise
return 'second'
})
const third = queue.add(async () => {
started3.resolve()
await blocker3.promise
return 'third'
})
await Promise.all([started1.promise, started2.promise])
let thirdStarted = false
started3.promise.then(() => {
thirdStarted = true
})
await delay(1)
expect(thirdStarted).toBe(false)
expect(queue.running).toBe(2)
expect(queue.pending).toBe(1)
blocker1.resolve()
await started3.promise
blocker2.resolve()
blocker3.resolve()
await expect(first).resolves.toBe('first')
await expect(second).resolves.toBe('second')
await expect(third).resolves.toBe('third')
})
it('continues processing after aborting a running task', async () => {
const queue = new TaskQueue({ concurrency: 1 })
const started1 = Promise.withResolvers<void>()
const started2 = Promise.withResolvers<void>()
const blocker1 = Promise.withResolvers<void>()
const first = queue.add(async () => {
started1.resolve()
await blocker1.promise
return 'first'
})
const second = queue.add(async () => {
started2.resolve()
return 'second'
})
await started1.promise
const reason = new Error('abort running')
first.abort(reason)
blocker1.resolve()
await expect(first).rejects.toBe(reason)
await started2.promise
await expect(second).resolves.toBe('second')
})
it('continues processing after a task throws synchronously', async () => {
const queue = new TaskQueue({ concurrency: 1 })
const reason = new Error('sync throw')
const first = queue.add(() => {
throw reason
})
const second = queue.add(async () => 'second')
await expect(first).rejects.toBe(reason)
await expect(second).resolves.toBe('second')
expect(queue.running).toBe(0)
expect(queue.pending).toBe(0)
})
})