├── .gitignore ├── .npmignore ├── example ├── index.html └── index.js ├── index.d.ts ├── index.js ├── license.md ├── package.json ├── readme.md └── test ├── array-methods.js ├── autostart.js ├── concurrent.js ├── end.js ├── error-async.js ├── error-promise.js ├── error-sync.js ├── index.html ├── index.js ├── length.js ├── promises.js ├── results.js ├── resume.js ├── start.js ├── stop-sync.js ├── stop.js ├── synchronous.js └── timeout.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | node_modules 4 | coverage 5 | .vscode 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | example 3 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import Queue from 'queue' 2 | 3 | const q = new Queue({ results: [] }) 4 | 5 | // add jobs using the familiar Array API 6 | q.push(cb => { 7 | const result = 'two' 8 | cb(null, result) 9 | }) 10 | 11 | q.push( 12 | cb => { 13 | const result = 'four' 14 | cb(null, result) 15 | }, 16 | cb => { 17 | const result = 'five' 18 | cb(null, result) 19 | } 20 | ) 21 | 22 | // jobs can accept a callback or return a promise 23 | q.push(() => { 24 | return new Promise((resolve, reject) => { 25 | const result = 'one' 26 | resolve(result) 27 | }) 28 | }) 29 | 30 | q.unshift(cb => { 31 | const result = 'one' 32 | cb(null, result) 33 | }) 34 | 35 | q.splice(2, 0, cb => { 36 | const result = 'three' 37 | cb(null, result) 38 | }) 39 | 40 | // use the timeout feature to deal with jobs that 41 | // take too long or forget to execute a callback 42 | q.timeout = 100 43 | 44 | q.addEventListener('timeout', e => { 45 | console.log('job timed out:', e.detail.job.toString().replace(/\n/g, '')) 46 | e.detail.next() 47 | }) 48 | 49 | q.push(cb => { 50 | setTimeout(() => { 51 | console.log('slow job finished') 52 | cb() 53 | }, 200) 54 | }) 55 | 56 | q.push(cb => { 57 | console.log('forgot to execute callback') 58 | }) 59 | 60 | // jobs can also override the queue's timeout 61 | // on a per-job basis 62 | function extraSlowJob (cb) { 63 | setTimeout(() => { 64 | console.log('extra slow job finished') 65 | cb() 66 | }, 400) 67 | } 68 | 69 | extraSlowJob.timeout = 500 70 | q.push(extraSlowJob) 71 | 72 | // jobs can also opt-out of the timeout altogether 73 | function superSlowJob (cb) { 74 | setTimeout(() => { 75 | console.log('super slow job finished') 76 | cb() 77 | }, 1000) 78 | } 79 | 80 | superSlowJob.timeout = null 81 | q.push(superSlowJob) 82 | 83 | // get notified when jobs complete 84 | q.addEventListener('success', e => { 85 | console.log('job finished processing:', e.detail.toString().replace(/\n/g, '')) 86 | console.log('The result is:', e.detail.result) 87 | }) 88 | 89 | // begin processing, get notified on end / failure 90 | q.start(err => { 91 | if (err) throw err 92 | console.log('all done:', q.results) 93 | }) 94 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for Queue 2 | // Project: https://github.com/jessetane/queue 3 | // Definitions by: Alex Miller 4 | // Additions by Maksim Lavrenyuk 5 | 6 | 7 | 8 | type EventsMap = { 9 | end: { error?: Error } 10 | error: { error: Error, job: QueueWorker } 11 | timeout: { next: (err?: Error, ...result: any[]) => void, job: QueueWorker } 12 | success: { result: any[] } 13 | start: { job?: QueueWorker } 14 | } 15 | 16 | export class QueueEvent extends Event { 17 | readonly detail: Detail 18 | constructor(name: Name, detail: Detail) 19 | } 20 | 21 | 22 | type EventListenerOrEventListenerObject> = (event: Event) => void | { 23 | handleEvent(Event: Event): void; 24 | }; 25 | 26 | 27 | export interface Options { 28 | /** 29 | * Max number of jobs the queue should process concurrently. 30 | * 31 | * @default Infinity 32 | */ 33 | concurrency?: number; 34 | 35 | /** 36 | * Milliseconds to wait for a job to execute its callback. 37 | * 38 | * @default 0 39 | */ 40 | timeout?: number; 41 | 42 | /** 43 | * Ensures the queue is always running if jobs are available. Useful in situations where you are using a queue only for concurrency control. 44 | * 45 | * @default false 46 | */ 47 | autostart?: boolean; 48 | 49 | /** 50 | * An array to set job callback arguments on. 51 | * 52 | * @default null 53 | */ 54 | results?: any[]; 55 | } 56 | 57 | export default class Queue extends EventTarget { 58 | constructor(options?: Options) 59 | 60 | /** 61 | * Max number of jobs the queue should process concurrently. 62 | */ 63 | concurrency: number; 64 | 65 | /** 66 | * Milliseconds to wait for a job to execute its callback. 67 | */ 68 | timeout: number; 69 | 70 | /** 71 | * Ensures the queue is always running if jobs are available. 72 | */ 73 | autostart: boolean; 74 | 75 | /** 76 | * An array to set job callback arguments on. 77 | */ 78 | results: any[] | null; 79 | 80 | /** 81 | * Jobs pending + jobs to process. 82 | */ 83 | readonly length: number; 84 | 85 | /** 86 | * Adds one or more elements to the end of the Queue and returns the new length of the Queue. 87 | * 88 | * @param workers New workers of the Queue. 89 | */ 90 | push(...workers: QueueWorker[]): number; 91 | 92 | /** 93 | * Adds one or more elements to the front of the Queue and returns the new length of the Queue. 94 | * 95 | * @param workers Workers to insert at the start of the Queue. 96 | */ 97 | unshift(...workers: QueueWorker[]): number; 98 | 99 | /** 100 | * Adds and/or removes elements from the queue. 101 | * 102 | * @param start The zero-based location in the Queue from which to start removing elements. 103 | * @param deleteCount The number of elements to remove. 104 | */ 105 | splice(start: number, deleteCount?: number): Queue; 106 | 107 | /** 108 | * Adds and/or removes elements from the queue. 109 | * 110 | * @param start The zero-based location in the Queue from which to start removing elements. 111 | * @param deleteCount The number of elements to remove. 112 | * @param workers Workers to insert into the Queue in place of the deleted elements. 113 | */ 114 | splice(start: number, deleteCount: number, ...workers: QueueWorker[]): Queue; 115 | 116 | /** 117 | * Removes the last element from the Queue and returns that element. 118 | */ 119 | pop(): QueueWorker | undefined; 120 | 121 | /** 122 | * Removes the first element from the Queue and returns that element. 123 | */ 124 | shift(): QueueWorker | undefined; 125 | 126 | /** 127 | * Extracts a section of the Queue and returns Queue. 128 | * 129 | * @param start The beginning of the specified portion of the Queue. 130 | * @param end The end of the specified portion of the Queue. 131 | */ 132 | slice(start?: number, end?: number): Queue; 133 | 134 | /** 135 | * Reverses the order of the elements of the Queue in place. 136 | */ 137 | reverse(): Queue; 138 | 139 | /** 140 | * Returns the first (least) index of an element within the Queue equal to the specified value, or -1 if none is found. 141 | * 142 | * @param searchElement The value to locate in the Queue. 143 | * @param fromIndex The Queue index at which to begin the search. If omitted, the search starts at index 0. 144 | */ 145 | indexOf(searchElement: QueueWorker, fromIndex?: number): number; 146 | 147 | /** 148 | * Returns the last (greatest) index of an element within the Queue equal to the specified value, or -1 if none is found. 149 | * 150 | * @param searchElement The value to locate in the Queue. 151 | * @param fromIndex The Queue index at which to begin the search. If omitted, the search starts at the last index in the Queue. 152 | */ 153 | lastIndexOf(searchElement: QueueWorker, fromIndex?: number): number; 154 | 155 | /** 156 | * Starts the queue. 157 | * 158 | * @param callback Callback to be called when the queue empties or when an error occurs. 159 | */ 160 | start(callback: (error?: Error, results?: any[] | null) => void): void; 161 | 162 | start(): Promise<{ error?: Error, results?: any[] | null }>; 163 | 164 | start(): void; 165 | 166 | /** 167 | * Stops the queue. 168 | */ 169 | stop(): void; 170 | 171 | /** 172 | * Stop and empty the queue immediately. 173 | * 174 | * @param error error of why the stop has occurred, to be passed to start callback if supplied. 175 | */ 176 | end(error?: Error): void; 177 | 178 | addEventListener(name: Event, callback: EventListenerOrEventListenerObject>, options?: AddEventListenerOptions | boolean): void; 179 | 180 | dispatchEvent(event: QueueEvent): boolean; 181 | 182 | removeEventListener(name: Event, callback: EventListenerOrEventListenerObject>, options?: EventListenerOptions | boolean): void; 183 | } 184 | 185 | export interface QueueWorker { 186 | (callback?: QueueWorkerCallback): void | Promise; 187 | 188 | /** 189 | * Override queue timeout. 190 | */ 191 | timeout?: number; 192 | /** 193 | * If the QueueWorker returns a promise, it will be moved to this field. 194 | * This can be useful when tracking timeout events 195 | */ 196 | promise?: Promise 197 | } 198 | 199 | export interface QueueWorkerCallback { 200 | (error?: Error, data?: Object): void; 201 | } 202 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const has = Object.prototype.hasOwnProperty 2 | 3 | /** 4 | * Since CustomEvent is only supported in nodejs since version 19, 5 | * you have to create your own class instead of using CustomEvent 6 | * @see https://github.com/nodejs/node/issues/40678 7 | * */ 8 | export class QueueEvent extends Event { 9 | constructor (name, detail) { 10 | super(name) 11 | this.detail = detail 12 | } 13 | } 14 | 15 | export default class Queue extends EventTarget { 16 | constructor (options = {}) { 17 | super() 18 | const { concurrency = Infinity, timeout = 0, autostart = false, results = null } = options 19 | this.concurrency = concurrency 20 | this.timeout = timeout 21 | this.autostart = autostart 22 | this.results = results 23 | this.pending = 0 24 | this.session = 0 25 | this.running = false 26 | this.jobs = [] 27 | this.timers = [] 28 | this.addEventListener('error', this._errorHandler) 29 | } 30 | 31 | _errorHandler (evt) { 32 | this.end(evt.detail.error) 33 | } 34 | 35 | pop () { 36 | return this.jobs.pop() 37 | } 38 | 39 | shift () { 40 | return this.jobs.shift() 41 | } 42 | 43 | indexOf (searchElement, fromIndex) { 44 | return this.jobs.indexOf(searchElement, fromIndex) 45 | } 46 | 47 | lastIndexOf (searchElement, fromIndex) { 48 | if (fromIndex !== undefined) return this.jobs.lastIndexOf(searchElement, fromIndex) 49 | return this.jobs.lastIndexOf(searchElement) 50 | } 51 | 52 | slice (start, end) { 53 | this.jobs = this.jobs.slice(start, end) 54 | return this 55 | } 56 | 57 | reverse () { 58 | this.jobs.reverse() 59 | return this 60 | } 61 | 62 | push (...workers) { 63 | const methodResult = this.jobs.push(...workers) 64 | if (this.autostart) this._start() 65 | return methodResult 66 | } 67 | 68 | unshift (...workers) { 69 | const methodResult = this.jobs.unshift(...workers) 70 | if (this.autostart) this._start() 71 | return methodResult 72 | } 73 | 74 | splice (start, deleteCount, ...workers) { 75 | this.jobs.splice(start, deleteCount, ...workers) 76 | if (this.autostart) this._start() 77 | return this 78 | } 79 | 80 | get length () { 81 | return this.pending + this.jobs.length 82 | } 83 | 84 | start (callback) { 85 | if (this.running) throw new Error('already started') 86 | let awaiter 87 | if (callback) { 88 | this._addCallbackToEndEvent(callback) 89 | } else { 90 | awaiter = this._createPromiseToEndEvent() 91 | } 92 | this._start() 93 | return awaiter 94 | } 95 | 96 | _start () { 97 | this.running = true 98 | if (this.pending >= this.concurrency) { 99 | return 100 | } 101 | if (this.jobs.length === 0) { 102 | if (this.pending === 0) { 103 | this.done() 104 | } 105 | return 106 | } 107 | const job = this.jobs.shift() 108 | const session = this.session 109 | const timeout = (job !== undefined) && has.call(job, 'timeout') ? job.timeout : this.timeout 110 | let once = true 111 | let timeoutId = null 112 | let didTimeout = false 113 | let resultIndex = null 114 | const next = (error, ...result) => { 115 | if (once && this.session === session) { 116 | once = false 117 | this.pending-- 118 | if (timeoutId !== null) { 119 | this.timers = this.timers.filter(tID => tID !== timeoutId) 120 | clearTimeout(timeoutId) 121 | } 122 | if (error) { 123 | this.dispatchEvent(new QueueEvent('error', { error, job })) 124 | } else if (!didTimeout) { 125 | if (resultIndex !== null && this.results !== null) { 126 | this.results[resultIndex] = [...result] 127 | } 128 | this.dispatchEvent(new QueueEvent('success', { result: [...result], job })) 129 | } 130 | if (this.session === session) { 131 | if (this.pending === 0 && this.jobs.length === 0) { 132 | this.done() 133 | } else if (this.running) { 134 | this._start() 135 | } 136 | } 137 | } 138 | } 139 | if (timeout) { 140 | timeoutId = setTimeout(() => { 141 | didTimeout = true 142 | this.dispatchEvent(new QueueEvent('timeout', { next, job })) 143 | next() 144 | }, timeout) 145 | this.timers.push(timeoutId) 146 | } 147 | if (this.results != null) { 148 | resultIndex = this.results.length 149 | this.results[resultIndex] = null 150 | } 151 | this.pending++ 152 | this.dispatchEvent(new QueueEvent('start', { job })) 153 | job.promise = job(next) 154 | if (job.promise !== undefined && typeof job.promise.then === 'function') { 155 | job.promise.then(function (result) { 156 | return next(undefined, result) 157 | }).catch(function (err) { 158 | return next(err || true) 159 | }) 160 | } 161 | if (this.running && this.jobs.length > 0) { 162 | this._start() 163 | } 164 | } 165 | 166 | stop () { 167 | this.running = false 168 | } 169 | 170 | end (error) { 171 | this.clearTimers() 172 | this.jobs.length = 0 173 | this.pending = 0 174 | this.done(error) 175 | } 176 | 177 | clearTimers () { 178 | this.timers.forEach(timer => { 179 | clearTimeout(timer) 180 | }) 181 | this.timers = [] 182 | } 183 | 184 | _addCallbackToEndEvent (cb) { 185 | const onend = evt => { 186 | this.removeEventListener('end', onend) 187 | cb(evt.detail.error, this.results) 188 | } 189 | this.addEventListener('end', onend) 190 | } 191 | 192 | _createPromiseToEndEvent () { 193 | return new Promise((resolve, reject) => { 194 | this._addCallbackToEndEvent((error, results) => { 195 | if (error) reject(error) 196 | else resolve(results) 197 | }) 198 | }) 199 | } 200 | 201 | done (error) { 202 | this.session++ 203 | this.running = false 204 | this.dispatchEvent(new QueueEvent('end', { error })) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2012-23 [these people](https://github.com/jessetane/queue/graphs/contributors) 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "queue", 3 | "version": "7.0.0", 4 | "description": "asynchronous function queue with adjustable concurrency", 5 | "keywords": [ 6 | "queue", 7 | "async", 8 | "asynchronous", 9 | "synchronous", 10 | "job", 11 | "task", 12 | "concurrency", 13 | "concurrent" 14 | ], 15 | "files": [ 16 | "index.js", 17 | "index.d.ts" 18 | ], 19 | "typings": "index.d.ts", 20 | "main": "index.js", 21 | "exports": { 22 | ".": "./index.js", 23 | "./*": "./*" 24 | }, 25 | "type": "module", 26 | "devDependencies": { 27 | "c8": "^7.12.0", 28 | "http-server": "^14.1.1", 29 | "tap-esm": "^3.0.0" 30 | }, 31 | "scripts": { 32 | "test": "node test", 33 | "coverage": "c8 npm run test", 34 | "dev": "http-server ./ -s -c-1 -p 7357", 35 | "example": "node example" 36 | }, 37 | "repository": "https://github.com/jessetane/queue.git", 38 | "contributors": [ 39 | "Jesse Tane ", 40 | "Maksim Lavrenyuk " 41 | ], 42 | "license": "MIT" 43 | } 44 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ____ __ _____ __ _____ 3 | / __ `/ / / / _ \/ / / / _ \ 4 | / /_/ / /_/ / __/ /_/ / __/ 5 | \__, /\__,_/\___/\__,_/\___/ 6 | /_/ 7 | ``` 8 | Asynchronous function queue with adjustable concurrency. 9 | 10 | [![npm](http://img.shields.io/npm/v/queue.svg?style=flat-square)](http://www.npmjs.org/queue) 11 | 12 | This module exports a class `Queue` that implements most of the `Array` API. Pass async functions (ones that accept a callback or return a promise) to an instance's additive array methods. Processing begins when you call `q.start()`. 13 | 14 | ## Example 15 | Do `npm run example` or `npm run dev` and open the example directory (and your console) to run the following program: 16 | ``` javascript 17 | import Queue from 'queue' 18 | 19 | const q = new Queue({ results: [] }) 20 | 21 | // add jobs using the familiar Array API 22 | q.push(cb => { 23 | const result = 'two' 24 | cb(null, result) 25 | }) 26 | 27 | q.push( 28 | cb => { 29 | const result = 'four' 30 | cb(null, result) 31 | }, 32 | cb => { 33 | const result = 'five' 34 | cb(null, result) 35 | } 36 | ) 37 | 38 | // jobs can accept a callback or return a promise 39 | q.push(() => { 40 | return new Promise((resolve, reject) => { 41 | const result = 'one' 42 | resolve(result) 43 | }) 44 | }) 45 | 46 | q.unshift(cb => { 47 | const result = 'one' 48 | cb(null, result) 49 | }) 50 | 51 | q.splice(2, 0, cb => { 52 | const result = 'three' 53 | cb(null, result) 54 | }) 55 | 56 | // use the timeout feature to deal with jobs that 57 | // take too long or forget to execute a callback 58 | q.timeout = 100 59 | 60 | q.addEventListener('timeout', e => { 61 | console.log('job timed out:', e.detail.job.toString().replace(/\n/g, '')) 62 | e.detail.next() 63 | }) 64 | 65 | q.push(cb => { 66 | setTimeout(() => { 67 | console.log('slow job finished') 68 | cb() 69 | }, 200) 70 | }) 71 | 72 | q.push(cb => { 73 | console.log('forgot to execute callback') 74 | }) 75 | 76 | // jobs can also override the queue's timeout 77 | // on a per-job basis 78 | function extraSlowJob (cb) { 79 | setTimeout(() => { 80 | console.log('extra slow job finished') 81 | cb() 82 | }, 400) 83 | } 84 | 85 | extraSlowJob.timeout = 500 86 | q.push(extraSlowJob) 87 | 88 | // jobs can also opt-out of the timeout altogether 89 | function superSlowJob (cb) { 90 | setTimeout(() => { 91 | console.log('super slow job finished') 92 | cb() 93 | }, 1000) 94 | } 95 | 96 | superSlowJob.timeout = null 97 | q.push(superSlowJob) 98 | 99 | // get notified when jobs complete 100 | q.addEventListener('success', e => { 101 | console.log('job finished processing:', e.detail.toString().replace(/\n/g, '')) 102 | console.log('The result is:', e.detail.result) 103 | }) 104 | 105 | // begin processing, get notified on end / failure 106 | q.start(err => { 107 | if (err) throw err 108 | console.log('all done:', q.results) 109 | }) 110 | ``` 111 | 112 | ## Install 113 | 114 | ``` 115 | npm install queue 116 | 117 | yarn add queue 118 | ``` 119 | 120 | ## Test 121 | 122 | ``` 123 | npm test 124 | 125 | npm run dev // for testing in a browser, open test directory (and your console) 126 | ``` 127 | 128 | ## API 129 | 130 | ### `const q = new Queue([opts])` 131 | Constructor. `opts` may contain initial values for: 132 | * `q.concurrency` 133 | * `q.timeout` 134 | * `q.autostart` 135 | * `q.results` 136 | 137 | ## Instance methods 138 | ### `q.start([cb])` 139 | Explicitly starts processing jobs and provides feedback to the caller when the queue empties or an error occurs. If cb is not passed a promise will be returned. 140 | 141 | ### `q.stop()` 142 | Stops the queue. can be resumed with `q.start()`. 143 | 144 | ### `q.end([err])` 145 | Stop and empty the queue immediately. 146 | 147 | ## Instance methods mixed in from `Array` 148 | Mozilla has docs on how these methods work [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array). Note that `slice` does not copy the queue. 149 | ### `q.push(element1, ..., elementN)` 150 | ### `q.unshift(element1, ..., elementN)` 151 | ### `q.splice(index , howMany[, element1[, ...[, elementN]]])` 152 | ### `q.pop()` 153 | ### `q.shift()` 154 | ### `q.slice(begin[, end])` 155 | ### `q.reverse()` 156 | ### `q.indexOf(searchElement[, fromIndex])` 157 | ### `q.lastIndexOf(searchElement[, fromIndex])` 158 | 159 | ## Properties 160 | ### `q.concurrency` 161 | Max number of jobs the queue should process concurrently, defaults to `Infinity`. 162 | 163 | ### `q.timeout` 164 | Milliseconds to wait for a job to execute its callback. This can be overridden by specifying a `timeout` property on a per-job basis. 165 | 166 | ### `q.autostart` 167 | Ensures the queue is always running if jobs are available. Useful in situations where you are using a queue only for concurrency control. 168 | 169 | ### `q.results` 170 | An array to set job callback arguments on. 171 | 172 | ### `q.length` 173 | Jobs pending + jobs to process (readonly). 174 | 175 | ## Events 176 | 177 | ### `q.dispatchEvent(new QueueEvent('start', { job }))` 178 | Immediately before a job begins to execute. 179 | 180 | ### `q.dispatchEvent(new QueueEvent('success', { result: [...result], job }))` 181 | After a job executes its callback. 182 | 183 | ### `q.dispatchEvent(new QueueEvent('error', { err, job }))` 184 | After a job passes an error to its callback. 185 | 186 | ### `q.dispatchEvent(new QueueEvent('timeout', { next, job }))` 187 | After `q.timeout` milliseconds have elapsed and a job has not executed its callback. 188 | 189 | ### `q.dispatchEvent(new QueueEvent('end', { err }))` 190 | After all jobs have been processed 191 | 192 | ## Releases 193 | The latest stable release is published to [npm](http://npmjs.org/queue). Abbreviated changelog below: 194 | * [7.0](https://github.com/jessetane/queue/archive/7.0.0.tar.gz) 195 | * Modernized codebase, added new maintainer (@MaksimLavrenyuk) 196 | * [6.0](https://github.com/jessetane/queue/archive/6.0.1.tar.gz) 197 | * Add `start` event before job begins (@joelgriffith) 198 | * Add `timeout` property on a job to override the queue's timeout (@joelgriffith) 199 | * [5.0](https://github.com/jessetane/queue/archive/5.0.0.tar.gz) 200 | * Updated TypeScript bindings (@Codex-) 201 | * [4.4](https://github.com/jessetane/queue/archive/4.4.0.tar.gz) 202 | * Add results feature 203 | * [4.3](https://github.com/jessetane/queue/archive/4.3.0.tar.gz) 204 | * Add promise support (@kwolfy) 205 | * [4.2](https://github.com/jessetane/queue/archive/4.2.0.tar.gz) 206 | * Unref timers on end 207 | * [4.1](https://github.com/jessetane/queue/archive/4.1.0.tar.gz) 208 | * Add autostart feature 209 | * [4.0](https://github.com/jessetane/queue/archive/4.0.0.tar.gz) 210 | * Change license to MIT 211 | * [3.1.x](https://github.com/jessetane/queue/archive/3.0.6.tar.gz) 212 | * Add .npmignore 213 | * [3.0.x](https://github.com/jessetane/queue/archive/3.0.6.tar.gz) 214 | * Change the default concurrency to `Infinity` 215 | * Allow `q.start()` to accept an optional callback executed on `q.emit('end')` 216 | * [2.x](https://github.com/jessetane/queue/archive/2.2.0.tar.gz) 217 | * Major api changes / not backwards compatible with 1.x 218 | * [1.x](https://github.com/jessetane/queue/archive/1.0.2.tar.gz) 219 | * Early prototype 220 | 221 | ## License 222 | [MIT](https://github.com/jessetane/queue/blob/master/license.md) 223 | -------------------------------------------------------------------------------- /test/array-methods.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap-esm'; 2 | import Queue from '../index.js' 3 | 4 | tap('pop sync', (t) => { 5 | t.plan(2) 6 | 7 | const q = new Queue() 8 | const results = [] 9 | 10 | q.push((cb) => { 11 | results.push(1) 12 | cb() 13 | }) 14 | 15 | q.push((cb) => { 16 | results.push(2) 17 | cb() 18 | }) 19 | 20 | q.pop() 21 | 22 | q.start((err) => { 23 | t.notOk(err) 24 | t.arrayEqual(results, [ 1 ]) 25 | }) 26 | }) 27 | 28 | tap('pop async', (t) => { 29 | t.plan(2) 30 | const q = new Queue({ concurrency: 1 }) 31 | const results = [] 32 | 33 | q.push((cb) => { 34 | results.push(1) 35 | setTimeout(cb, 10) 36 | }) 37 | 38 | q.push((cb) => { 39 | results.push(2) 40 | cb() 41 | }) 42 | 43 | q.start((err) => { 44 | t.notOk(err) 45 | t.arrayEqual(results, [ 1 ]) 46 | }) 47 | 48 | q.pop() 49 | }) 50 | 51 | tap('shift sync', (t) => { 52 | t.plan(2) 53 | 54 | const q = new Queue() 55 | const results = [] 56 | 57 | q.push((cb) => { 58 | results.push(1) 59 | cb() 60 | }) 61 | 62 | q.push((cb) => { 63 | results.push(2) 64 | cb() 65 | }) 66 | 67 | q.shift() 68 | 69 | q.start((err) => { 70 | t.notOk(err) 71 | t.arrayEqual(results, [ 2 ]) 72 | }) 73 | }) 74 | 75 | tap('unshift with autostart', (t) => { 76 | t.plan(1) 77 | const q = new Queue({ autostart: true }) 78 | let wasRunning = false 79 | 80 | q.unshift((cb) => { 81 | wasRunning = true 82 | cb() 83 | }) 84 | 85 | t.ok(wasRunning) 86 | }) 87 | 88 | tap('shift async', (t) => { 89 | t.plan(2) 90 | const q = new Queue({ concurrency: 1 }) 91 | const results = [] 92 | 93 | q.push((cb) => { 94 | results.push(1) 95 | setTimeout(cb, 10) 96 | }) 97 | 98 | q.push((cb) => { 99 | results.push(2) 100 | cb() 101 | }) 102 | 103 | q.start((err) => { 104 | t.notOk(err) 105 | t.arrayEqual(results, [ 1 ]) 106 | }) 107 | 108 | q.shift() 109 | }) 110 | 111 | tap('slice sync', (t) => { 112 | t.plan(3) 113 | 114 | const q = new Queue() 115 | const results = [] 116 | 117 | q.push((cb) => { 118 | results.push(1) 119 | cb() 120 | }) 121 | 122 | q.push((cb) => { 123 | results.push(2) 124 | cb() 125 | }) 126 | 127 | t.equal(q, q.slice(0, 1)) 128 | 129 | q.start((err) => { 130 | t.notOk(err) 131 | t.arrayEqual(results, [ 1 ]) 132 | }) 133 | }) 134 | 135 | tap('slice async', (t) => { 136 | t.plan(3) 137 | const q = new Queue({ concurrency: 1 }) 138 | const results = [] 139 | 140 | q.push((cb) => { 141 | results.push(1) 142 | setTimeout(cb, 10) 143 | }) 144 | 145 | q.push((cb) => { 146 | results.push(2) 147 | cb() 148 | }) 149 | 150 | q.start((err) => { 151 | t.notOk(err) 152 | t.arrayEqual(results, [ 1 ]) 153 | }) 154 | 155 | t.equal(q, q.slice(0, 0)) 156 | }) 157 | 158 | tap('splice with autostart', (t) => { 159 | t.plan(2) 160 | 161 | const q = new Queue({ autostart: true, concurrency: 1 }) 162 | const results = [] 163 | 164 | q.push((cb) => { 165 | results.push(1) 166 | setTimeout(cb, 5) 167 | }) 168 | 169 | q.push((cb) => { 170 | results.push(2) 171 | cb() 172 | }) 173 | 174 | t.equal(q, q.splice(0, 1)) 175 | 176 | setTimeout(() => { 177 | t.arrayEqual(results, [1]) 178 | }, 10) 179 | }) 180 | 181 | tap('reverse sync', (t) => { 182 | t.plan(3) 183 | 184 | const q = new Queue() 185 | const results = [] 186 | 187 | q.push((cb) => { 188 | results.push(1) 189 | cb() 190 | }) 191 | 192 | q.push((cb) => { 193 | results.push(2) 194 | cb() 195 | }) 196 | 197 | t.equal(q, q.reverse()) 198 | 199 | q.start((err) => { 200 | t.notOk(err) 201 | t.arrayEqual(results, [ 2, 1 ]) 202 | }) 203 | }) 204 | 205 | tap('indexOf', (t) => { 206 | t.plan(3) 207 | 208 | const q = new Queue() 209 | const results = [] 210 | const job = (cb) => { 211 | results.push(results.length + 1) 212 | cb() 213 | } 214 | 215 | q.push(job) 216 | q.push(job) 217 | 218 | t.equal(q.indexOf(job), 0) 219 | 220 | q.start((err) => { 221 | t.notOk(err) 222 | t.arrayEqual(results, [ 1, 2 ]) 223 | }) 224 | }) 225 | 226 | tap('lastIndexOf', (t) => { 227 | t.plan(4) 228 | 229 | const q = new Queue() 230 | const results = [] 231 | const job = (cb) => { 232 | results.push(results.length + 1) 233 | cb() 234 | } 235 | 236 | q.push(job) 237 | q.push(job) 238 | 239 | t.equal(q.lastIndexOf(job), 1) 240 | t.equal(q.lastIndexOf(job, 1), 1) 241 | 242 | q.start((err) => { 243 | t.notOk(err) 244 | t.arrayEqual(results, [ 1, 2 ]) 245 | }) 246 | }) -------------------------------------------------------------------------------- /test/autostart.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap-esm'; 2 | import Queue from '../index.js' 3 | 4 | tap('autostart', (t) => { 5 | t.plan(9) 6 | const expected = ['one', 'two', 'three'] 7 | const actual = [] 8 | const q = new Queue({ autostart: true }) 9 | let numEndHandlers = 0 10 | 11 | q.addEventListener('end', () => { 12 | numEndHandlers++ 13 | t.equal(actual.length, numEndHandlers) 14 | 15 | actual.forEach((a, i) => { 16 | t.equal(actual[i], expected[i]) 17 | }) 18 | }) 19 | 20 | q.push((cb) => { 21 | actual.push('one') 22 | cb() 23 | }) 24 | 25 | q.push((cb) => { 26 | actual.push('two') 27 | cb() 28 | }) 29 | 30 | setTimeout(() => { 31 | q.push((cb) => { 32 | actual.push('three') 33 | cb() 34 | }) 35 | }, 10) 36 | }) 37 | -------------------------------------------------------------------------------- /test/concurrent.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap-esm'; 2 | import Queue from '../index.js' 3 | 4 | tap('concurrent', (t) => { 5 | t.plan(6) 6 | const actual = [] 7 | const q = new Queue() 8 | q.concurrency = 2 9 | 10 | q.push((cb) => { 11 | setTimeout(() => { 12 | actual.push('two') 13 | cb() 14 | }, 20) 15 | }) 16 | 17 | q.push((cb) => { 18 | setTimeout(() => { 19 | actual.push('one') 20 | cb() 21 | }, 0) 22 | }) 23 | 24 | q.push((cb) => { 25 | q.concurrency = 1 26 | setTimeout(() => { 27 | actual.push('three') 28 | cb() 29 | }, 30) 30 | }) 31 | 32 | q.push((cb) => { 33 | setTimeout(() => { 34 | actual.push('four') 35 | cb() 36 | }, 10) 37 | }) 38 | 39 | q.push((cb) => { 40 | setTimeout(() => { 41 | actual.push('five') 42 | cb() 43 | }, 0) 44 | }) 45 | 46 | q.start(() => { 47 | const expected = ['one', 'two', 'three', 'four', 'five'] 48 | t.equal(actual.length, expected.length) 49 | 50 | actual.forEach((a, i) => { 51 | const e = expected[i] 52 | t.equal(a, e) 53 | }) 54 | }) 55 | }) -------------------------------------------------------------------------------- /test/end.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap-esm'; 2 | import Queue from '../index.js' 3 | 4 | tap('end', (t) => { 5 | t.plan(3) 6 | const q = new Queue() 7 | 8 | q.push((cb) => { 9 | setTimeout(cb, 0) 10 | }) 11 | 12 | q.push((cb) => { 13 | setTimeout(() => { 14 | t.equal(q.length, 2) 15 | q.end(new Error('fake error')) 16 | setTimeout(() => { 17 | // session has changed so this should be a nop 18 | cb() 19 | 20 | // and we should still have one job left 21 | t.equal(q.length, 1) 22 | }, 10) 23 | }, 10) 24 | }) 25 | 26 | q.push((cb) => { 27 | setTimeout(cb, 30) 28 | }) 29 | 30 | q.start((err) => { 31 | t.equal(q.length, 0) 32 | 33 | if (err !== undefined) { 34 | q.push(() => {}) 35 | } 36 | }) 37 | }) -------------------------------------------------------------------------------- /test/error-async.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap-esm'; 2 | import Queue from '../index.js' 3 | 4 | tap('error-async', (t) => { 5 | t.plan(2) 6 | const q = new Queue({ autostart: true }) 7 | 8 | q.addEventListener('end', (event) => { 9 | t.equal(event.detail.error.message, 'something broke') // 3 10 | t.equal(q.length, 0) 11 | }) 12 | 13 | q.push((cb) => { 14 | setTimeout(cb, 10) 15 | }) 16 | 17 | q.push((cb) => { 18 | setTimeout(() => { 19 | cb(new Error('something broke')) 20 | }, 20) 21 | }) 22 | 23 | q.push((cb) => { 24 | setTimeout(cb, 30) 25 | }) 26 | }) 27 | 28 | 29 | tap('error-async. start() should return a promise with error data.', async (t) => { 30 | t.plan(2) 31 | const q = new Queue() 32 | 33 | q.push((cb) => { 34 | setTimeout(cb, 10) 35 | }) 36 | 37 | q.push((cb) => { 38 | setTimeout(() => { 39 | cb(new Error('something broke')) 40 | }, 20) 41 | }) 42 | 43 | q.push((cb) => { 44 | setTimeout(cb, 30) 45 | }) 46 | 47 | try { 48 | await q.start() 49 | } catch (err) { 50 | t.equal(err.message, 'something broke') // 3 51 | t.equal(q.length, 0) 52 | } 53 | }) 54 | -------------------------------------------------------------------------------- /test/error-promise.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap-esm'; 2 | import Queue from '../index.js' 3 | 4 | tap('error-promise with error', (t) => { 5 | t.plan(2) 6 | const q = new Queue({ autostart: true }) 7 | 8 | q.addEventListener('end', (event) => { 9 | t.equal(event.detail.error.message, 'something broke') 10 | t.equal(q.length, 0) 11 | }) 12 | 13 | q.push((cb) => { 14 | setTimeout(cb, 10) 15 | }) 16 | 17 | q.push(async () => { 18 | return await new Promise((resolve, reject) => { 19 | setTimeout(() => { 20 | reject(new Error('something broke')) 21 | }, 20) 22 | }) 23 | }) 24 | 25 | q.push(async () => { 26 | return await new Promise((resolve) => { 27 | setTimeout(() => { 28 | resolve() 29 | }, 30) 30 | }) 31 | }) 32 | }) 33 | 34 | tap('error-promise with empty error', (t) => { 35 | t.plan(2) 36 | const q = new Queue({ autostart: true }) 37 | 38 | q.addEventListener('end', (event) => { 39 | t.equal(event.detail.error, true) 40 | t.equal(q.length, 0) 41 | }) 42 | 43 | q.push((cb) => { 44 | setTimeout(cb, 10) 45 | }) 46 | 47 | q.push(async () => { 48 | return await new Promise((resolve, reject) => { 49 | setTimeout(() => { 50 | reject() 51 | }, 20) 52 | }) 53 | }) 54 | 55 | q.push(async () => { 56 | return await new Promise(function (resolve) { 57 | setTimeout(() => { 58 | resolve() 59 | }, 30) 60 | }) 61 | }) 62 | }) 63 | 64 | tap('error-promise with error. start() should return a promise with error data.', async (t) => { 65 | t.plan(2) 66 | const q = new Queue() 67 | 68 | q.push((cb) => { 69 | setTimeout(cb, 10) 70 | }) 71 | 72 | q.push(async () => { 73 | return await new Promise((resolve, reject) => { 74 | setTimeout(() => { 75 | reject(new Error('something broke')) 76 | }, 20) 77 | }) 78 | }) 79 | 80 | q.push(async () => { 81 | return await new Promise((resolve) => { 82 | setTimeout(() => { 83 | resolve() 84 | }, 30) 85 | }) 86 | }) 87 | 88 | try { 89 | await q.start() 90 | } catch (err) { 91 | t.equal(err.message, 'something broke') 92 | t.equal(q.length, 0) 93 | } 94 | }) 95 | 96 | 97 | 98 | tap('error-promise with empty error. start() should return a promise with error data.', async (t) => { 99 | t.plan(2) 100 | const q = new Queue() 101 | 102 | q.push((cb) => { 103 | setTimeout(cb, 10) 104 | }) 105 | 106 | q.push(async () => { 107 | return await new Promise((resolve, reject) => { 108 | setTimeout(() => { 109 | reject('something broke') 110 | }, 20) 111 | }) 112 | }) 113 | 114 | q.push(async () => { 115 | return await new Promise(function (resolve) { 116 | setTimeout(() => { 117 | resolve() 118 | }, 30) 119 | }) 120 | }) 121 | 122 | try { 123 | await q.start() 124 | } catch (err) { 125 | t.equal(err, 'something broke') 126 | t.equal(q.length, 0) 127 | } 128 | }) 129 | -------------------------------------------------------------------------------- /test/error-sync.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap-esm'; 2 | import Queue from '../index.js' 3 | 4 | tap('error-sync', (t) => { 5 | t.plan(2) 6 | 7 | const q = new Queue() 8 | 9 | q.push((cb) => { 10 | cb(new Error('something broke')) 11 | }) 12 | 13 | q.start((err) => { 14 | t.equal(err.message, 'something broke') 15 | t.equal(q.length, 0) 16 | }) 17 | }) 18 | 19 | 20 | tap('error-sync. start() should return a promise with error data.', async (t) => { 21 | t.plan(2) 22 | 23 | const q = new Queue() 24 | 25 | q.push((cb) => { 26 | cb(new Error('something broke')) 27 | }) 28 | 29 | try { 30 | await q.start() 31 | } catch (err) { 32 | t.equal(err.message, 'something broke') 33 | t.equal(q.length, 0) 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import './synchronous.js' 2 | import './concurrent.js' 3 | import './timeout.js' 4 | import './length.js' 5 | import './start.js' 6 | import './stop.js' 7 | import './end.js' 8 | import './error-sync.js' 9 | import './error-async.js' 10 | import './error-promise.js' 11 | import './resume.js' 12 | import './autostart.js' 13 | import './array-methods.js' 14 | import './promises.js' 15 | import './results.js' 16 | -------------------------------------------------------------------------------- /test/length.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap-esm'; 2 | import Queue from '../index.js' 3 | 4 | tap('length', (t) => { 5 | t.plan(12) 6 | const q = new Queue() 7 | 8 | q.push((cb) => { 9 | setTimeout(() => { 10 | t.equal(q.length, 3) 11 | cb() 12 | t.equal(q.length, 2) 13 | }, 0) 14 | }) 15 | 16 | q.push((cb) => { 17 | setTimeout(() => { 18 | t.equal(q.length, 2) 19 | cb() 20 | t.equal(q.length, 1) 21 | }, 10) 22 | }) 23 | 24 | q.push((cb) => { 25 | setTimeout(() => { 26 | t.equal(q.length, 1) 27 | cb() 28 | t.equal(q.length, 0) 29 | }, 20) 30 | }) 31 | 32 | t.equal(q.pending, 0) 33 | t.equal(q.length, 3) 34 | 35 | q.start(() => { 36 | t.equal(q.pending, 0) 37 | t.equal(q.length, 0) 38 | }) 39 | 40 | t.equal(q.pending, 3) 41 | t.equal(q.length, 3) 42 | }) -------------------------------------------------------------------------------- /test/promises.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap-esm'; 2 | import Queue from '../index.js' 3 | 4 | tap('promises', (t) => { 5 | t.plan(4) 6 | const actual = [] 7 | const q = new Queue({ concurrency: 1 }) 8 | 9 | q.addEventListener('end', () => { 10 | const expected = ['one', 'two', 'three'] 11 | t.equal(actual.length, expected.length) 12 | 13 | actual.forEach((a, i) => { 14 | const e = expected[i] 15 | t.equal(a, e) 16 | }) 17 | }) 18 | 19 | q.push(async () => { 20 | actual.push('three') 21 | }) 22 | 23 | q.unshift(async () => { 24 | actual.push('one') 25 | }) 26 | 27 | q.splice(1, 0, async () => { 28 | actual.push('two') 29 | }) 30 | 31 | q.start() 32 | }) 33 | 34 | tap('promise returned from the job must be set to the job.promise property', (t) => { 35 | t.plan(1) 36 | const q = new Queue({ concurrency: 1 }) 37 | let promise; 38 | 39 | q.addEventListener('success',(event) => { 40 | t.equal(event.detail.job.promise, promise) 41 | }) 42 | 43 | q.push(() => { 44 | promise = new Promise((resolve) => resolve()); 45 | 46 | return promise; 47 | }) 48 | 49 | q.start() 50 | }) 51 | 52 | tap('promise returned from an async job should be set to the job.promise property', (t) => { 53 | t.plan(1) 54 | const q = new Queue({ concurrency: 1 }) 55 | 56 | q.addEventListener('success',async (event) => { 57 | const result = await event.detail.job.promise; 58 | 59 | t.equal(result, 'result'); 60 | }) 61 | 62 | q.push(async () => 'result'); 63 | 64 | q.start() 65 | }) -------------------------------------------------------------------------------- /test/results.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap-esm'; 2 | import Queue from '../index.js' 3 | 4 | tap('results', (t) => { 5 | t.plan(5) 6 | const q = new Queue({ results: [] }) 7 | 8 | q.push( 9 | (cb) => cb(undefined, 42), 10 | (cb) => cb(undefined, 3, 2, 1), 11 | (cb) => cb() 12 | ) 13 | 14 | q.unshift((cb) => { 15 | setTimeout(() => { 16 | cb(undefined, 'string') 17 | }, 10) 18 | }) 19 | 20 | q.start((err, results) => { 21 | t.notOk(err); 22 | 23 | [ 24 | ['string'], 25 | [42], 26 | [3, 2, 1], 27 | [] 28 | ].forEach((expected, i) => { 29 | t.arrayEqual(expected, results[i]) 30 | }) 31 | }) 32 | }) 33 | 34 | tap('results from start(). start() should return a promise with error data and results', async (t) => { 35 | t.plan(5) 36 | const q = new Queue({ results: [] }) 37 | 38 | q.push( 39 | (cb) => cb(undefined, 42), 40 | (cb) => cb(undefined, 3, 2, 1), 41 | (cb) => cb() 42 | ) 43 | 44 | q.unshift((cb) => { 45 | setTimeout(() => { 46 | cb(undefined, 'string') 47 | }, 10) 48 | }) 49 | 50 | try { 51 | await q.start() 52 | t.pass() 53 | } catch (err) { 54 | t.notOk(err) 55 | } 56 | 57 | [ 58 | ['string'], 59 | [42], 60 | [3, 2, 1], 61 | [] 62 | ].forEach((expected, i) => { 63 | t.arrayEqual(expected, q.results[i]) 64 | }) 65 | }); 66 | -------------------------------------------------------------------------------- /test/resume.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap-esm'; 2 | import Queue from '../index.js' 3 | 4 | tap('resume', (t) => { 5 | t.plan(16) 6 | const q = new Queue({ concurrency: 2 }) 7 | 8 | let jobsToSet = 16 9 | 10 | while (jobsToSet-- !== 0) { 11 | q.push((cb) => { 12 | setTimeout(() => { 13 | t.ok(q) 14 | cb() 15 | }, 10) 16 | }) 17 | } 18 | 19 | // start 20 | q.start() 21 | 22 | // and stop somewhere in the middle of queue 23 | setTimeout(function () { 24 | q.stop() 25 | q.start() 26 | }, 30) 27 | }) -------------------------------------------------------------------------------- /test/start.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap-esm'; 2 | import Queue from '../index.js' 3 | 4 | tap('start', (t) => { 5 | t.plan(4) 6 | 7 | const q = new Queue() 8 | const work = (cb) => { 9 | t.ok(q) 10 | cb() 11 | } 12 | 13 | q.addEventListener('start', (event) => { 14 | t.equal(event.detail.job, work) 15 | }) 16 | 17 | q.push(work) 18 | 19 | q.start(() => { 20 | t.ok(q) 21 | 22 | q.start(() => { 23 | t.ok(q) 24 | }) 25 | }) 26 | }) 27 | 28 | tap('await start. Push the job, start() returns Promise waiting for all jobs to finish', async (t) => { 29 | t.plan(1) 30 | const queue = new Queue(); 31 | const result = []; 32 | const work = (cb) => { 33 | setTimeout(() => { 34 | result.push(1); 35 | cb(); 36 | }, 30) 37 | } 38 | 39 | queue.push(work); 40 | await queue.start(); 41 | result.push(2); 42 | console.log(result); 43 | 44 | t.arrayEqual(result, [1, 2]); 45 | }); 46 | -------------------------------------------------------------------------------- /test/stop-sync.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap-esm'; 2 | import Queue from '../index.js' 3 | 4 | tap('stop-sync', (t) => { 5 | t.plan(2) 6 | const q = new Queue({ concurrency: 1 }) 7 | let n = 0 8 | 9 | q.push((cb) => { 10 | n++ 11 | q.stop() 12 | cb() 13 | }) 14 | 15 | q.push((cb) => { 16 | n++ 17 | cb() 18 | }) 19 | 20 | q.start() 21 | 22 | setTimeout(() => { 23 | t.equal(q.length, 1) 24 | t.equal(n, 1) 25 | }, 0) 26 | }) -------------------------------------------------------------------------------- /test/stop.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap-esm'; 2 | import Queue from '../index.js' 3 | 4 | tap('stop', (t) => { 5 | t.plan(6) 6 | const q = new Queue({ concurrency: 1 }) 7 | 8 | q.push((cb) => { 9 | setTimeout(() => { 10 | t.equal(q.running, false) 11 | if (cb !== undefined) cb() 12 | 13 | // restart 14 | setTimeout(() => { 15 | q.start(() => { 16 | t.ok(q) 17 | }) 18 | }, 10) 19 | }, 10) 20 | }) 21 | 22 | q.push((cb) => { 23 | t.equal(q.running, true) 24 | cb() 25 | }) 26 | 27 | // start 28 | q.start((err) => { 29 | t.notOk(err) 30 | t.equal(q.running, false) 31 | }) 32 | 33 | // but stop the q before the first job has finished 34 | setTimeout(() => { 35 | t.equal(q.length, 2) 36 | q.stop() 37 | }, 0) 38 | }) -------------------------------------------------------------------------------- /test/synchronous.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap-esm'; 2 | import Queue from '../index.js' 3 | 4 | tap('synchronous', (t) => { 5 | t.plan(4) 6 | 7 | const actual = [] 8 | const q = new Queue({ concurrency: 1 }) 9 | 10 | q.addEventListener('end', () => { 11 | const expected = ['one', 'two', 'three'] 12 | t.equal(actual.length, expected.length) 13 | 14 | actual.forEach((a, i) => { 15 | const e = expected[i] 16 | t.equal(a, e) 17 | }) 18 | }) 19 | 20 | q.push((cb) => { 21 | actual.push('three') 22 | cb() 23 | }) 24 | 25 | q.unshift((cb) => { 26 | actual.push('one') 27 | cb() 28 | }) 29 | 30 | q.splice(1, 0, (cb) => { 31 | actual.push('two') 32 | cb() 33 | }) 34 | 35 | q.start() 36 | }) 37 | -------------------------------------------------------------------------------- /test/timeout.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap-esm'; 2 | import Queue from '../index.js' 3 | 4 | tap('timeout', (t) => { 5 | t.plan(4) 6 | const actual = [] 7 | const q = new Queue({ timeout: 10 }) 8 | 9 | q.addEventListener('timeout', (event) => { 10 | t.ok(q) 11 | event.detail.next() 12 | }) 13 | 14 | q.addEventListener('end', () => { 15 | const expected = ['two', 'three'] 16 | t.equal(actual.length, expected.length) 17 | 18 | actual.forEach((a, i) => { 19 | const e = expected[i] 20 | t.equal(a, e) 21 | }) 22 | }) 23 | 24 | q.push((cb) => { 25 | // forget to call cb 26 | }) 27 | 28 | q.push((cb) => { 29 | actual.push('two') 30 | cb() 31 | }) 32 | 33 | q.push((cb) => { 34 | actual.push('three') 35 | cb() 36 | }) 37 | 38 | q.start() 39 | }) 40 | 41 | tap('job timeout', (t) => { 42 | t.plan(2) 43 | const q = new Queue({ timeout: 5 }) 44 | let timeouts = 0 45 | const willTimeout = (cb) => { 46 | setTimeout(cb, 8) 47 | } 48 | const wontTimeout = (cb) => { 49 | setTimeout(cb, 8) 50 | } 51 | 52 | wontTimeout.timeout = 10 53 | 54 | q.addEventListener('timeout', (event) => { 55 | t.ok(q) 56 | timeouts++ 57 | event.detail.next() 58 | }) 59 | 60 | q.addEventListener('end', () => { 61 | t.equal(timeouts, 1) 62 | }) 63 | 64 | q.push(willTimeout) 65 | q.push(wontTimeout) 66 | 67 | q.start() 68 | }) 69 | 70 | tap('job-based opt-out of timeout', (t) => { 71 | t.plan(1) 72 | const q = new Queue({ timeout: 5 }) 73 | let timeouts = 0 74 | const wontTimeout = (cb) => { 75 | setTimeout(cb, 8) 76 | } 77 | 78 | wontTimeout.timeout = undefined 79 | 80 | q.addEventListener('timeout', (event) => { 81 | t.fail('Job should not have timed-out') 82 | timeouts++ 83 | event.detail.next() 84 | }) 85 | 86 | q.addEventListener('end', () => { 87 | t.equal(timeouts, 0) 88 | }) 89 | 90 | q.push(wontTimeout) 91 | 92 | q.start() 93 | }) 94 | 95 | tap('timeout auto-continue', (t) => { 96 | t.plan(3) 97 | const actual = [] 98 | const q = new Queue({ timeout: 10 }) 99 | 100 | q.addEventListener('end', () => { 101 | const expected = ['two', 'three'] 102 | t.equal(actual.length, expected.length) 103 | 104 | actual.forEach((a, i) => { 105 | const e = expected[i] 106 | t.equal(a, e) 107 | }) 108 | }) 109 | 110 | q.push(function (cb) { 111 | // forget to call cb 112 | }) 113 | 114 | q.push(function (cb) { 115 | actual.push('two') 116 | cb() 117 | }) 118 | 119 | q.push(function (cb) { 120 | actual.push('three') 121 | cb() 122 | }) 123 | 124 | q.start() 125 | }) 126 | 127 | tap('unref timeouts', (t) => { 128 | t.plan(3) 129 | const q = new Queue({ timeout: 99999 }) 130 | 131 | q.push(function (cb) { 132 | t.pass() 133 | // forget to call cb 134 | }) 135 | 136 | q.start() 137 | 138 | q.stop() 139 | 140 | setTimeout(() => { 141 | t.equal(q.pending, 1) 142 | 143 | q.end() 144 | 145 | t.equal(q.pending, 0) 146 | }, 10) 147 | }) 148 | 149 | tap('From the timeout event it should be possible to find out the delayed promise', (t) => { 150 | t.plan(1) 151 | const q = new Queue({ timeout: 5 }) 152 | let promise 153 | 154 | q.addEventListener('timeout',(event) => { 155 | t.equal(event.detail.job.promise, promise) 156 | }) 157 | 158 | q.push(() => { 159 | promise = new Promise((resolve) => setTimeout(resolve, 10)); 160 | 161 | return promise; 162 | }) 163 | 164 | q.start() 165 | }) 166 | --------------------------------------------------------------------------------