├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── dist ├── lib │ └── sequential-task-queue.js └── types │ └── sequential-task-queue.d.ts ├── doc └── .gitignore ├── examples ├── .gitignore └── examples.ts ├── gulpfile.js ├── index.js ├── package.json ├── readme.md ├── src ├── .gitignore ├── readme.template.md └── sequential-task-queue.ts ├── test ├── .gitignore └── sequential-task-queue-spec.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | typings 3 | npm-debug.log -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug with gulp-mocha", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/node_modules/gulp/bin/gulp.js", 9 | "stopOnEntry": false, 10 | "args": ["test-debug"], 11 | "cwd": "${workspaceRoot}", 12 | "runtimeExecutable": null, 13 | "env": { "NODE_ENV": "testing"}, 14 | "sourceMaps": true 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "**/.git": true, 5 | "**/.gitignore": false 6 | //"src/**/*.js": true, 7 | //"test/**/*.js": true 8 | }, 9 | "typescript.tsdk": "node_modules/typescript/lib" 10 | 11 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "gulp", 4 | "isShellCommand": true, 5 | "args": [ 6 | "--no-color" 7 | ], 8 | "tasks": [ 9 | { 10 | "taskName": "build", 11 | "isBuildCommand": true, 12 | "showOutput": "always" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Márton Balassa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dist/lib/sequential-task-queue.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Standard cancellation reasons. {@link SequentialTaskQueue} sets {@link CancellationToken.reason} 4 | * to one of these values when cancelling a task for a reason other than the user code calling 5 | * {@link CancellationToken.cancel}. 6 | */ 7 | exports.cancellationTokenReasons = { 8 | /** Used when the task was cancelled in response to a call to {@link SequentialTaskQueue.cancel} */ 9 | cancel: Object.create(null), 10 | /** Used when the task was cancelled after its timeout has passed */ 11 | timeout: Object.create(null) 12 | }; 13 | /** 14 | * Standard event names used by {@link SequentialTaskQueue} 15 | */ 16 | exports.sequentialTaskQueueEvents = { 17 | drained: "drained", 18 | error: "error", 19 | timeout: "timeout" 20 | }; 21 | /** 22 | * FIFO task queue to run tasks in predictable order, without concurrency. 23 | */ 24 | class SequentialTaskQueue { 25 | /** 26 | * Creates a new instance of {@link SequentialTaskQueue} 27 | * @param options - Configuration options for the task queue. 28 | */ 29 | constructor(options) { 30 | this.queue = []; 31 | this._isClosed = false; 32 | this.waiters = []; 33 | if (!options) 34 | options = {}; 35 | this.defaultTimeout = options.timeout; 36 | this.name = options.name || "SequentialTaskQueue"; 37 | this.scheduler = options.scheduler || SequentialTaskQueue.defaultScheduler; 38 | } 39 | /** Indicates if the queue has been closed. Calling {@link SequentialTaskQueue.push} on a closed queue will result in an exception. */ 40 | get isClosed() { 41 | return this._isClosed; 42 | } 43 | /** 44 | * Adds a new task to the queue. 45 | * @param task - The function to call when the task is run 46 | * @param timeout - An optional timeout (in milliseconds) for the task, after which it should be cancelled to avoid hanging tasks clogging up the queue. 47 | * @returns A {@link CancellationToken} that may be used to cancel the task before it completes. 48 | */ 49 | push(task, options) { 50 | if (this._isClosed) 51 | throw new Error(`${this.name} has been previously closed`); 52 | var taskEntry = { 53 | callback: task, 54 | args: options && options.args ? (Array.isArray(options.args) ? options.args.slice() : [options.args]) : [], 55 | timeout: options && options.timeout !== undefined ? options.timeout : this.defaultTimeout, 56 | cancellationToken: { 57 | cancel: (reason) => this.cancelTask(taskEntry, reason) 58 | }, 59 | resolve: undefined, 60 | reject: undefined 61 | }; 62 | taskEntry.args.push(taskEntry.cancellationToken); 63 | this.queue.push(taskEntry); 64 | this.scheduler.schedule(() => this.next()); 65 | var result = new Promise((resolve, reject) => { 66 | taskEntry.resolve = resolve; 67 | taskEntry.reject = reject; 68 | }); 69 | result.cancel = (reason) => taskEntry.cancellationToken.cancel(reason); 70 | return result; 71 | } 72 | /** 73 | * Cancels the currently running task (if any), and clears the queue. 74 | * @returns {Promise} A Promise that is fulfilled when the queue is empty and the current task has been cancelled. 75 | */ 76 | cancel() { 77 | if (this.currentTask) 78 | this.cancelTask(this.currentTask, exports.cancellationTokenReasons.cancel); 79 | var queue = this.queue.splice(0); 80 | // Cancel all and emit a drained event if there were tasks waiting in the queue 81 | if (queue.length) { 82 | queue.forEach(task => this.cancelTask(task, exports.cancellationTokenReasons.cancel)); 83 | this.emit(exports.sequentialTaskQueueEvents.drained); 84 | } 85 | return this.wait(); 86 | } 87 | /** 88 | * Closes the queue, preventing new tasks to be added. 89 | * Any calls to {@link SequentialTaskQueue.push} after closing the queue will result in an exception. 90 | * @param {boolean} cancel - Indicates that the queue should also be cancelled. 91 | * @returns {Promise} A Promise that is fulfilled when the queue has finished executing remaining tasks. 92 | */ 93 | close(cancel) { 94 | if (!this._isClosed) { 95 | this._isClosed = true; 96 | if (cancel) 97 | return this.cancel(); 98 | } 99 | return this.wait(); 100 | } 101 | /** 102 | * Returns a promise that is fulfilled when the queue is empty. 103 | * @returns {Promise} 104 | */ 105 | wait() { 106 | if (!this.currentTask && this.queue.length === 0) 107 | return Promise.resolve(); 108 | return new Promise(resolve => { 109 | this.waiters.push(resolve); 110 | }); 111 | } 112 | /** 113 | * Adds an event handler for a named event. 114 | * @param {string} evt - Event name. See the readme for a list of valid events. 115 | * @param {Function} handler - Event handler. When invoking the handler, the queue will set itself as the `this` argument of the call. 116 | */ 117 | on(evt, handler) { 118 | this.events = this.events || {}; 119 | (this.events[evt] || (this.events[evt] = [])).push(handler); 120 | } 121 | /** 122 | * Adds a single-shot event handler for a named event. 123 | * @param {string} evt - Event name. See the readme for a list of valid events. 124 | * @param {Function} handler - Event handler. When invoking the handler, the queue will set itself as the `this` argument of the call. 125 | */ 126 | once(evt, handler) { 127 | var cb = (...args) => { 128 | this.removeListener(evt, cb); 129 | handler.apply(this, args); 130 | }; 131 | this.on(evt, cb); 132 | } 133 | /** 134 | * Removes an event handler. 135 | * @param {string} evt - Event name 136 | * @param {Function} handler - Event handler to be removed 137 | */ 138 | removeListener(evt, handler) { 139 | if (this.events) { 140 | var list = this.events[evt]; 141 | if (list) { 142 | var i = 0; 143 | while (i < list.length) { 144 | if (list[i] === handler) 145 | list.splice(i, 1); 146 | else 147 | i++; 148 | } 149 | } 150 | } 151 | } 152 | /** @see {@link SequentialTaskQueue.removeListener} */ 153 | off(evt, handler) { 154 | return this.removeListener(evt, handler); 155 | } 156 | emit(evt, ...args) { 157 | if (this.events && this.events[evt]) 158 | try { 159 | this.events[evt].forEach(fn => fn.apply(this, args)); 160 | } 161 | catch (e) { 162 | console.error(`${this.name}: Exception in '${evt}' event handler`, e); 163 | } 164 | } 165 | next() { 166 | // Try running the next task, if not currently running one 167 | if (!this.currentTask) { 168 | var task = this.queue.shift(); 169 | // skip cancelled tasks 170 | while (task && task.cancellationToken.cancelled) 171 | task = this.queue.shift(); 172 | if (task) { 173 | try { 174 | this.currentTask = task; 175 | if (task.timeout) { 176 | task.timeoutHandle = setTimeout(() => { 177 | this.emit(exports.sequentialTaskQueueEvents.timeout); 178 | this.cancelTask(task, exports.cancellationTokenReasons.timeout); 179 | }, task.timeout); 180 | } 181 | let res = task.callback.apply(undefined, task.args); 182 | if (res && isPromise(res)) { 183 | res.then(result => { 184 | task.result = result; 185 | this.doneTask(task); 186 | }, err => { 187 | this.doneTask(task, err); 188 | }); 189 | } 190 | else { 191 | task.result = res; 192 | this.doneTask(task); 193 | } 194 | } 195 | catch (e) { 196 | this.doneTask(task, e); 197 | } 198 | } 199 | else { 200 | // queue is empty, call waiters 201 | this.callWaiters(); 202 | } 203 | } 204 | } 205 | cancelTask(task, reason) { 206 | task.cancellationToken.cancelled = true; 207 | task.cancellationToken.reason = reason; 208 | this.doneTask(task); 209 | } 210 | doneTask(task, error) { 211 | if (task.timeoutHandle) 212 | clearTimeout(task.timeoutHandle); 213 | task.cancellationToken.cancel = noop; 214 | if (error) { 215 | this.emit(exports.sequentialTaskQueueEvents.error, error); 216 | task.reject.call(undefined, error); 217 | } 218 | else if (task.cancellationToken.cancelled) 219 | task.reject.call(undefined, task.cancellationToken.reason); 220 | else 221 | task.resolve.call(undefined, task.result); 222 | if (this.currentTask === task) { 223 | this.currentTask = undefined; 224 | if (!this.queue.length) { 225 | this.emit(exports.sequentialTaskQueueEvents.drained); 226 | this.callWaiters(); 227 | } 228 | else 229 | this.scheduler.schedule(() => this.next()); 230 | } 231 | } 232 | callWaiters() { 233 | let waiters = this.waiters.splice(0); 234 | waiters.forEach(waiter => waiter()); 235 | } 236 | } 237 | SequentialTaskQueue.defaultScheduler = { 238 | schedule: callback => setTimeout(callback, 0) 239 | }; 240 | exports.SequentialTaskQueue = SequentialTaskQueue; 241 | function noop() { 242 | } 243 | function isPromise(obj) { 244 | return (obj && typeof obj.then === "function"); 245 | } 246 | SequentialTaskQueue.defaultScheduler = { 247 | schedule: typeof setImmediate === "function" 248 | ? callback => setImmediate(callback) 249 | : callback => setTimeout(callback, 0) 250 | }; 251 | -------------------------------------------------------------------------------- /dist/types/sequential-task-queue.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents an object that schedules a function for asynchronous execution. 3 | * The default implementation used by {@link SequentialTaskQueue} calls {@link setImmediate} when available, 4 | * and {@link setTimeout} otherwise. 5 | * @see {@link SequentialTaskQueue.defaultScheduler} 6 | * @see {@link TaskQueueOptions.scheduler} 7 | */ 8 | export interface Scheduler { 9 | /** 10 | * Schedules a callback for asynchronous execution. 11 | */ 12 | schedule(callback: Function): void; 13 | } 14 | /** 15 | * Object used for passing configuration options to the {@link SequentialTaskQueue} constructor. 16 | */ 17 | export interface SequentialTaskQueueOptions { 18 | /** 19 | * Assigns a name to the task queue for diagnostic purposes. The name does not need to be unique. 20 | */ 21 | name?: string; 22 | /** 23 | * Default timeout (in milliseconds) for tasks pushed to the queue. Default is 0 (no timeout). 24 | * */ 25 | timeout?: number; 26 | /** 27 | * Scheduler used by the queue. Defaults to {@link SequentialTaskQueue.defaultScheduler}. 28 | */ 29 | scheduler?: Scheduler; 30 | } 31 | /** 32 | * Options object for individual tasks. 33 | */ 34 | export interface TaskOptions { 35 | /** 36 | * Timeout for the task, in milliseconds. 37 | * */ 38 | timeout?: number; 39 | /** 40 | * Arguments to pass to the task. Useful for minimalising the number of Function objects and closures created 41 | * when pushing the same task multiple times, with different arguments. 42 | * 43 | * @example 44 | * // The following code creates a single Function object and no closures: 45 | * for (let i = 0; i < 100; i++) 46 | * queue.push(process, {args: [i]}); 47 | * function process(n) { 48 | * console.log(n); 49 | * } 50 | */ 51 | args?: any; 52 | } 53 | /** 54 | * Provides the API for querying and invoking task cancellation. 55 | */ 56 | export interface CancellationToken { 57 | /** 58 | * When `true`, indicates that the task has been cancelled. 59 | */ 60 | cancelled?: boolean; 61 | /** 62 | * An arbitrary object representing the reason of the cancellation. Can be a member of the {@link cancellationTokenReasons} object or an `Error`, etc. 63 | */ 64 | reason?: any; 65 | /** 66 | * Cancels the task for which the cancellation token was created. 67 | * @param reason - The reason of the cancellation, see {@link CancellationToken.reason} 68 | */ 69 | cancel(reason?: any): any; 70 | } 71 | /** 72 | * Standard cancellation reasons. {@link SequentialTaskQueue} sets {@link CancellationToken.reason} 73 | * to one of these values when cancelling a task for a reason other than the user code calling 74 | * {@link CancellationToken.cancel}. 75 | */ 76 | export declare var cancellationTokenReasons: { 77 | cancel: any; 78 | timeout: any; 79 | }; 80 | /** 81 | * Standard event names used by {@link SequentialTaskQueue} 82 | */ 83 | export declare var sequentialTaskQueueEvents: { 84 | drained: string; 85 | error: string; 86 | timeout: string; 87 | }; 88 | /** 89 | * Promise interface with the ability to cancel. 90 | */ 91 | export interface CancellablePromiseLike extends PromiseLike { 92 | /** 93 | * Cancels (and consequently, rejects) the task associated with the Promise. 94 | * @param reason - Reason of the cancellation. This value will be passed when rejecting this Promise. 95 | */ 96 | cancel(reason?: any): void; 97 | } 98 | /** 99 | * FIFO task queue to run tasks in predictable order, without concurrency. 100 | */ 101 | export declare class SequentialTaskQueue { 102 | static defaultScheduler: Scheduler; 103 | private queue; 104 | private _isClosed; 105 | private waiters; 106 | private defaultTimeout; 107 | private currentTask; 108 | private scheduler; 109 | private events; 110 | name: string; 111 | /** Indicates if the queue has been closed. Calling {@link SequentialTaskQueue.push} on a closed queue will result in an exception. */ 112 | readonly isClosed: boolean; 113 | /** 114 | * Creates a new instance of {@link SequentialTaskQueue} 115 | * @param options - Configuration options for the task queue. 116 | */ 117 | constructor(options?: SequentialTaskQueueOptions); 118 | /** 119 | * Adds a new task to the queue. 120 | * @param task - The function to call when the task is run 121 | * @param timeout - An optional timeout (in milliseconds) for the task, after which it should be cancelled to avoid hanging tasks clogging up the queue. 122 | * @returns A {@link CancellationToken} that may be used to cancel the task before it completes. 123 | */ 124 | push(task: Function, options?: TaskOptions): CancellablePromiseLike; 125 | /** 126 | * Cancels the currently running task (if any), and clears the queue. 127 | * @returns {Promise} A Promise that is fulfilled when the queue is empty and the current task has been cancelled. 128 | */ 129 | cancel(): PromiseLike; 130 | /** 131 | * Closes the queue, preventing new tasks to be added. 132 | * Any calls to {@link SequentialTaskQueue.push} after closing the queue will result in an exception. 133 | * @param {boolean} cancel - Indicates that the queue should also be cancelled. 134 | * @returns {Promise} A Promise that is fulfilled when the queue has finished executing remaining tasks. 135 | */ 136 | close(cancel?: boolean): PromiseLike; 137 | /** 138 | * Returns a promise that is fulfilled when the queue is empty. 139 | * @returns {Promise} 140 | */ 141 | wait(): PromiseLike; 142 | /** 143 | * Adds an event handler for a named event. 144 | * @param {string} evt - Event name. See the readme for a list of valid events. 145 | * @param {Function} handler - Event handler. When invoking the handler, the queue will set itself as the `this` argument of the call. 146 | */ 147 | on(evt: string, handler: Function): void; 148 | /** 149 | * Adds a single-shot event handler for a named event. 150 | * @param {string} evt - Event name. See the readme for a list of valid events. 151 | * @param {Function} handler - Event handler. When invoking the handler, the queue will set itself as the `this` argument of the call. 152 | */ 153 | once(evt: string, handler: Function): void; 154 | /** 155 | * Removes an event handler. 156 | * @param {string} evt - Event name 157 | * @param {Function} handler - Event handler to be removed 158 | */ 159 | removeListener(evt: string, handler: Function): void; 160 | /** @see {@link SequentialTaskQueue.removeListener} */ 161 | off(evt: string, handler: Function): void; 162 | protected emit(evt: string, ...args: any[]): void; 163 | protected next(): void; 164 | private cancelTask(task, reason?); 165 | private doneTask(task, error?); 166 | private callWaiters(); 167 | } 168 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | api -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | *.js -------------------------------------------------------------------------------- /examples/examples.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { SequentialTaskQueue, CancellationToken, cancellationTokenReasons } from "../src/sequential-task-queue"; 3 | import * as sinon from "sinon"; 4 | 5 | describe("Examples", () => { 6 | describe("Basic usage", () => { 7 | it("", () => { 8 | sinon.spy(console, "log"); 9 | // --- snippet: Basic usage --- 10 | var queue = new SequentialTaskQueue(); 11 | queue.push(() => { 12 | console.log("first task"); 13 | }); 14 | queue.push(() => { 15 | console.log("second task"); 16 | }); 17 | // --- snip --- 18 | return queue.wait().then(() => { 19 | try { 20 | assert.deepEqual((console.log).args, [["first task"], ["second task"]]); 21 | } finally { 22 | (console.log).restore(); 23 | } 24 | }); 25 | }); 26 | }); 27 | 28 | describe("Promises", () => { 29 | it("", () => { 30 | sinon.spy(console, "log"); 31 | // --- snippet: Promises --- 32 | var queue = new SequentialTaskQueue(); 33 | queue.push(() => { 34 | console.log("1"); 35 | }); 36 | queue.push(() => { 37 | return new Promise(resolve => { 38 | setTimeout(() => { 39 | console.log("2"); 40 | resolve(); 41 | }, 500); 42 | }); 43 | }); 44 | queue.push(() => { 45 | return new Promise((resolve, reject) => { 46 | setTimeout(() => { 47 | console.log("3"); 48 | reject(); 49 | }, 100); 50 | }); 51 | }); 52 | queue.push(() => { 53 | console.log("4"); 54 | }); 55 | 56 | // Output: 57 | // 1 58 | // 2 59 | // 3 60 | // 4 61 | 62 | // --- snip --- 63 | return queue.wait().then(() => { 64 | try { 65 | assert.deepEqual((console.log).args, [["1"], ["2"], ["3"], ["4"]]) 66 | } finally { 67 | (console.log).restore(); 68 | } 69 | }); 70 | }); 71 | }); 72 | 73 | describe("Task cancellation", () => { 74 | it("", () => { 75 | // --- snippet: Task cancellation --- 76 | var queue = new SequentialTaskQueue(); 77 | var task = queue.push(token => { 78 | return new Promise((resolve, reject) => { 79 | setTimeout(resolve, 100); 80 | }).then(() => new Promise((resolve, reject) => { 81 | if (token.cancelled) 82 | reject(); 83 | else 84 | resolve(); 85 | })).then(() => { 86 | throw new Error("Should not ever get here"); 87 | }); 88 | }); 89 | setTimeout(() => { 90 | task.cancel(); 91 | }, 50); 92 | // --- snip --- 93 | return queue.wait(); 94 | }); 95 | }); 96 | 97 | describe("Timeouts", () => { 98 | it("", function() { 99 | this.timeout(0); 100 | // --- snippet: Timeouts --- 101 | // --- snip --- 102 | var resp = []; 103 | var timeouts = [20, 2000, 10]; 104 | var backend = { 105 | echo: query => new Promise(resolve => { 106 | setTimeout(() => resolve(query), timeouts.shift()); 107 | }), 108 | }; 109 | var state = { 110 | list: [], 111 | addResponse: function(response) { 112 | this.list.push(response); 113 | } 114 | }; 115 | // --- snip --- 116 | var queue = new SequentialTaskQueue(); 117 | // ... 118 | function onEcho(query) { 119 | queue.push(token => 120 | backend.echo(query).then(response => { 121 | if (!token.cancelled) { 122 | state.addResponse("Server responded: " + response); 123 | } 124 | }), { timeout: 1000 }); 125 | } 126 | // --- snip --- 127 | onEcho("foo"); 128 | onEcho("bar"); 129 | onEcho("baz"); 130 | return queue.wait().then(() => { assert.deepEqual(state.list, ["Server responded: foo", "Server responded: baz"]); }); 131 | }); 132 | }); 133 | 134 | describe("Arguments", () => { 135 | it("Without using args", function() { 136 | var handler: Function; 137 | var backend = { 138 | on: (evt: string, cb: Function) => { 139 | handler = cb; 140 | } 141 | }; 142 | sinon.spy(console, "log"); 143 | var queue = new SequentialTaskQueue(); 144 | // --- snippet: Arguments 1 --- 145 | backend.on("notification", (data) => { 146 | queue.push(() => { 147 | console.log(data); 148 | // todo: do something with data 149 | }); 150 | }); 151 | // --- snip --- 152 | handler(1); 153 | handler(3); 154 | handler(5); 155 | handler(7); 156 | return queue.wait().then(() => { 157 | try { 158 | assert.deepEqual((console.log).args, [[1], [3], [5], [7]]); 159 | } finally { 160 | (console.log).restore(); 161 | } 162 | }); 163 | }); 164 | 165 | it("With args", function() { 166 | var handler: Function; 167 | var backend = { 168 | on: (evt: string, cb: Function) => { 169 | handler = cb; 170 | } 171 | }; 172 | sinon.spy(console, "log"); 173 | var queue = new SequentialTaskQueue(); 174 | // --- snippet: Arguments 2 --- 175 | backend.on("notification", (data) => { 176 | queue.push(handleNotifiation, { args: data }); 177 | }); 178 | 179 | function handleNotifiation(data) { 180 | console.log(data); 181 | // todo: do something with data 182 | } 183 | // --- snip --- 184 | handler(1); 185 | handler(3); 186 | handler(5); 187 | handler(7); 188 | return queue.wait().then(() => { 189 | try { 190 | assert.deepEqual((console.log).args, [[1], [3], [5], [7]]); 191 | } finally { 192 | (console.log).restore(); 193 | } 194 | }); 195 | }); 196 | }); 197 | 198 | describe("Waiting for all tasks to finish", () => { 199 | it("", () => { 200 | var task1 = ()=>{}; 201 | var task2 = task1; 202 | var task3 = task2; 203 | // --- snippet: Wait --- 204 | var queue = new SequentialTaskQueue(); 205 | queue.push(task1); 206 | queue.push(task2); 207 | queue.push(task3); 208 | queue.wait().then(() => { /*...*/ }); 209 | // --- snip --- 210 | }); 211 | }); 212 | 213 | describe("Closing the queue", () => { 214 | it("", () => { 215 | // --- snippet: Close --- 216 | var queue = new SequentialTaskQueue(); 217 | // ... 218 | function deactivate(done) { 219 | queue.close(true).then(done); 220 | } 221 | // --- snip --- 222 | queue.push(() => new Promise(resolve => setTimeout(resolve, 500))); 223 | return new Promise(resolve => { 224 | deactivate(resolve); 225 | }); 226 | }); 227 | }); 228 | 229 | describe("Handling errors", () => { 230 | it("", () => { 231 | // --- snippet: Errors --- 232 | var queue = new SequentialTaskQueue(); 233 | queue.push(() => new Promise((resolve, reject) => { 234 | setTimeout(resolve, 100); 235 | }).then(() => new Promise((resolve, reject) => { 236 | throw new Error("Epic fail"); 237 | }))); 238 | // --- snip --- 239 | var spy = sinon.spy(); 240 | queue.on("error", spy); 241 | return queue.wait().then(() => assert(spy.called)); 242 | }); 243 | }); 244 | }); 245 | 246 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require("gulp"); 2 | var ts = require("gulp-typescript"); 3 | var mocha = require("gulp-mocha"); 4 | var sourceMaps = require("gulp-sourcemaps"); 5 | var snip = require("snip-text"); 6 | var template = require("gulp-template"); 7 | var rename = require("gulp-rename"); 8 | var fs = require("fs"); 9 | var del = require("del"); 10 | var sequence = require("run-sequence"); 11 | var typedoc = require("gulp-typedoc"); 12 | var vinylPaths = require("vinyl-paths"); 13 | 14 | var globs = { 15 | build: ["./src/**/*.ts"], 16 | testSource: ["./src/**/*.ts", "./test/**/*.ts", "./examples/**/*.ts"], 17 | test: ["./test/**/*.js"], 18 | docTemplates: ["./src/*.template.md"] 19 | }; 20 | 21 | gulp.task("doc:readme", function () { 22 | var examples = fs.readFileSync("./examples/examples.ts", "utf8"); 23 | var data = { 24 | examples: snip(examples, { unindent: true }) 25 | }; 26 | return gulp.src(globs.docTemplates) 27 | .pipe(template(data)) 28 | .pipe(rename(path => { 29 | path.basename = path.basename.replace(".template", ""); 30 | return path; 31 | })) 32 | .pipe(gulp.dest(".")); 33 | }); 34 | 35 | gulp.task("clean:api", function () { 36 | return del("./doc/api"); 37 | }); 38 | 39 | gulp.task("doc:api", ["clean:api"], function () { 40 | return gulp.src(globs.build) 41 | .pipe(typedoc({ 42 | target: "es6", 43 | out: "./doc/api", 44 | name: "SequentialTaskQueue", 45 | })); 46 | }); 47 | 48 | gulp.task("doc", ["doc:readme", "doc:api"], () => { }); 49 | 50 | gulp.task("test-compile", () => { 51 | var proj = ts.createProject("tsconfig.json"); 52 | 53 | return gulp.src(globs.testSource) 54 | .pipe(sourceMaps.init()) 55 | .pipe(proj()) 56 | .js 57 | .pipe(sourceMaps.write(".", { includeContent: false, sourceRoot: "." })) 58 | .pipe(gulp.dest(path => { 59 | return path.base; 60 | })); 61 | }); 62 | 63 | gulp.task("test-run", () => { 64 | return gulp.src(globs.test) 65 | .pipe(mocha()); 66 | }); 67 | 68 | gulp.task("test-run-debug", () => { 69 | return gulp.src(globs.test) 70 | .pipe(mocha({ enableTimeouts: false })); 71 | }); 72 | 73 | gulp.task("test-clean", () => { 74 | var p1 = gulp.src(globs.testSource) 75 | .pipe(rename(path => { 76 | if (path.extname == ".ts") 77 | path.extname = ".js"; 78 | })) 79 | .pipe(vinylPaths(del)); 80 | var p2 = gulp.src(globs.testSource) 81 | .pipe(rename(path => { 82 | if (path.extname == ".ts") 83 | path.extname = ".js.map"; 84 | })) 85 | .pipe(vinylPaths(del)); 86 | return Promise.all([p1, p2]); 87 | }); 88 | 89 | gulp.task("test", () => { 90 | return sequence("test-compile", "test-run", "test-clean"); 91 | }); 92 | 93 | gulp.task("test-debug-exit", () => { 94 | process.exit(); 95 | }); 96 | 97 | gulp.task("test-debug", () => { 98 | return sequence("test-compile", "test-run-debug", "test-clean", "test-debug-exit"); 99 | }); 100 | 101 | gulp.task("clean", () => { 102 | return del(["dist/**/*"]); 103 | }); 104 | 105 | gulp.task("build-ts", () => { 106 | var proj = ts.createProject("tsconfig.json"); 107 | var result = gulp.src(globs.build) 108 | .pipe(proj()); 109 | return result.js.pipe(gulp.dest("dist/lib")); 110 | }); 111 | 112 | gulp.task("build-dts", () => { 113 | var proj = ts.createProject("tsconfig.json", { target: "es6" }); 114 | var result = gulp.src(globs.build) 115 | .pipe(proj()); 116 | return result.dts.pipe(gulp.dest("dist/types")); 117 | }); 118 | 119 | gulp.task("build", () => sequence("clean", "build-ts", "build-dts")); 120 | 121 | gulp.task("prepublish", () => { 122 | return sequence("build", "test", "doc"); 123 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./dist/lib/sequential-task-queue.js"); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sequential-task-queue", 3 | "version": "1.2.1", 4 | "description": "FIFO task queue for node and the browser", 5 | "author": { 6 | "name": "BalassaMarton" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "http://github.com/BalassaMarton/sequential-task-queue" 11 | }, 12 | "types": "dist/types/sequential-task-queue.d.ts", 13 | "files": [ 14 | "dist", 15 | "doc", 16 | "index.js" 17 | ], 18 | "devDependencies": { 19 | "@types/mocha": "^2.2.39", 20 | "@types/node": "^7.0.5", 21 | "@types/sinon": "^1.16.35", 22 | "del": "^2.2.2", 23 | "gulp": "^3.9.1", 24 | "gulp-mocha": "^3.0.1", 25 | "gulp-rename": "^1.2.2", 26 | "gulp-sourcemaps": "^2.4.1", 27 | "gulp-template": "^4.0.0", 28 | "gulp-typedoc": "^2.0.2", 29 | "gulp-typescript": "^3.1.5", 30 | "mocha": "^3.2.0", 31 | "run-sequence": "^1.2.2", 32 | "sinon": "^1.17.7", 33 | "snip-text": "^1.0.0", 34 | "typedoc": "^0.5.6", 35 | "typedoc-markdown-theme": "0.0.4", 36 | "typescript": "^2.1.6", 37 | "vinyl-paths": "^2.1.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # SequentialTaskQueue 2 | 3 | `SequentialTaskQueue` is a FIFO task queue for node and the browser. It supports promises, timeouts and task cancellation. 4 | 5 | The primary goal of this component is to allow asynchronous tasks to be executed in a strict, predictable order. 6 | This is especially useful when the application state is frequently mutated in response to asynchronous events. 7 | 8 | ## Basic usage 9 | 10 | Use `push` to add tasks to the queue. The method returns a `Promise` that will fulfill when the task has been executed or cancelled. 11 | 12 | ```js 13 | var queue = new SequentialTaskQueue(); 14 | queue.push(() => { 15 | console.log("first task"); 16 | }); 17 | queue.push(() => { 18 | console.log("second task"); 19 | }); 20 | ``` 21 | 22 | ## Promises 23 | 24 | If the function passed to `push` returns a `Promise`, the queue will wait for it to fulfill before moving to the next task. 25 | Rejected promises don't cause the queue to stop executing tasks, but are reported in the `error` event (see below). 26 | 27 | ```js 28 | var queue = new SequentialTaskQueue(); 29 | queue.push(() => { 30 | console.log("1"); 31 | }); 32 | queue.push(() => { 33 | return new Promise(resolve => { 34 | setTimeout(() => { 35 | console.log("2"); 36 | resolve(); 37 | }, 500); 38 | }); 39 | }); 40 | queue.push(() => { 41 | return new Promise((resolve, reject) => { 42 | setTimeout(() => { 43 | console.log("3"); 44 | reject(); 45 | }, 100); 46 | }); 47 | }); 48 | queue.push(() => { 49 | console.log("4"); 50 | }); 51 | 52 | // Output: 53 | // 1 54 | // 2 55 | // 3 56 | // 4 57 | 58 | ``` 59 | 60 | ## Task cancellation 61 | 62 | Tasks waiting in the queue, as well as the currently executing task, can be cancelled. This is achieved by creating a `CancellationToken` object 63 | for every task pushed to the queue, and passing it (to the task function) as the last argument. The task can then query the token's `cancelled` property to check if it 64 | has been cancelled. The `Promise` returned by `push` is extended with a `cancel` method so that individual tasks can be cancelled. 65 | 66 | ```js 67 | var queue = new SequentialTaskQueue(); 68 | var task = queue.push(token => { 69 | return new Promise((resolve, reject) => { 70 | setTimeout(resolve, 100); 71 | }).then(() => new Promise((resolve, reject) => { 72 | if (token.cancelled) 73 | reject(); 74 | else 75 | resolve(); 76 | })).then(() => { 77 | throw new Error("Should not ever get here"); 78 | }); 79 | }); 80 | setTimeout(() => { 81 | task.cancel(); 82 | }, 50); 83 | ``` 84 | 85 | In the above example, the task is cancelled before the 100 ms timeout. 86 | 87 | When cancelling the current task, the queue will immediately schedule the next one, without waiting for the task to finish. 88 | It is the task's responsibility to abort when the cancellation token is set, thus avoiding invalid application state. 89 | When a task is cancelled, the corresponding `Promise` is rejected with the cancellation reason, regardless of where the task currently is in the execution chain (running, scheduled or queued). 90 | 91 | ## Timeouts 92 | 93 | Tasks can be pushed into the queue with a timeout, after which the queue will cancel the task (the timer starts when the task is run, not when queued). 94 | The timeout value is supplied to `push` in the second argument, which is interpreted as an options object for the task: 95 | 96 | ```js 97 | var queue = new SequentialTaskQueue(); 98 | // ... 99 | function onEcho(query) { 100 | queue.push(token => 101 | backend.echo(query).then(response => { 102 | if (!token.cancelled) { 103 | state.addResponse("Server responded: " + response); 104 | } 105 | }), { timeout: 1000 }); 106 | } 107 | ``` 108 | 109 | ## Passing arguments to the task 110 | 111 | In most scenarios, you will be using the queue to respond to frequent, asynchronous events. Consider the following code that processes push notifications 112 | coming from a server: 113 | 114 | ```js 115 | backend.on("notification", (data) => { 116 | queue.push(() => { 117 | console.log(data); 118 | // todo: do something with data 119 | }); 120 | }); 121 | ``` 122 | 123 | Every time the event handler is called, it creates a new function (and a closure), which can lead to poor performance. 124 | Let's rewrite this, now using the `args` property of the task options: 125 | 126 | ```js 127 | backend.on("notification", (data) => { 128 | queue.push(handleNotifiation, { args: data }); 129 | }); 130 | 131 | function handleNotifiation(data) { 132 | console.log(data); 133 | // todo: do something with data 134 | } 135 | ``` 136 | 137 | If the `args` member of the options object is an array, the queue will pass the elements of the array as arguments, otherwise 138 | the value is interpreted as the single argument to the task function. The cancellation token is always passed as the last argument. 139 | The last bit also means that you can't simply push a function that has a rest (`...`) parameter, or uses the `arguments` object, 140 | since the cancellation token would be appended. 141 | 142 | ## Waiting for all tasks to finish 143 | 144 | Use the `wait` method to obtain a `Promise` that fulfills when the queue is empty: 145 | 146 | ```js 147 | var queue = new SequentialTaskQueue(); 148 | queue.push(task1); 149 | queue.push(task2); 150 | queue.push(task3); 151 | queue.wait().then(() => { /*...*/ }); 152 | ``` 153 | 154 | ## Closing the queue 155 | 156 | At certain points in your code, you may want to prevent adding more tasks to a queue (e.g. screen deactivation). 157 | The `close` method closes the queue (sets the `isClosed` property to `true`), and returns a `Promise` that fulfills when the queue is empty. 158 | Calling `push` on a closed queue will throw an exception. Optionally, `close` can cancel all remaining tasks: 159 | to do so, pass a truthful value as its first parameter. 160 | 161 | ```js 162 | var queue = new SequentialTaskQueue(); 163 | // ... 164 | function deactivate(done) { 165 | queue.close(true).then(done); 166 | } 167 | ``` 168 | 169 | ## Handling errors 170 | 171 | Errors thrown inside a task are reported in the queue's `error` event (see below in the Events section). 172 | Exceptions thrown in event handlers, however, are catched and ignored to avoid inifinite loops of error handling code. 173 | 174 | ## Events 175 | 176 | `SequentialTaskQueue` implements the `on`, `removeListener` (`off`) and `once` methods of node's `EventEmitter` pattern. 177 | 178 | The following events are defined: 179 | 180 | ### error 181 | 182 | The `error` event is emitted when a task throws an error or the `Promise` returned by the task is rejected. The error object is 183 | passed as the first argument of the event handler. 184 | 185 | ### drained 186 | 187 | The `drained` event is emitted when a task has finished executing, and the queue is empty. A cancelled queue will also emit this event. 188 | 189 | ### timeout 190 | 191 | The `timeout` event is emitted when a task is cancelled due to an expired timeout. The event is emitted before calling `cancel` on the task's cancellation token. 192 | 193 | --- 194 | ## Changelog 195 | 196 | ### 1.2.1 197 | 198 | `next` and `emit` are now protected instead of private. 199 | 200 | ### 1.2.0 201 | 202 | `SequentialTaskQueue.push` now returns a `Promise`. Earlier versions only returned a cancellation token. 203 | 204 | --- 205 | This file was generated using [gulp-template](http://github.com/sindresorhus/gulp-template) and [snip-text](http://github.com/BalassaMarton/snip-text) -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | *.js -------------------------------------------------------------------------------- /src/readme.template.md: -------------------------------------------------------------------------------- 1 | # SequentialTaskQueue 2 | 3 | `SequentialTaskQueue` is a FIFO task queue for node and the browser. It supports promises, timeouts and task cancellation. 4 | 5 | The primary goal of this component is to allow asynchronous tasks to be executed in a strict, predictable order. 6 | This is especially useful when the application state is frequently mutated in response to asynchronous events. 7 | 8 | ## Basic usage 9 | 10 | Use `push` to add tasks to the queue. The method returns a `Promise` that will fulfill when the task has been executed or cancelled. 11 | 12 | ```js 13 | <%= examples["Basic usage"] %> 14 | ``` 15 | 16 | ## Promises 17 | 18 | If the function passed to `push` returns a `Promise`, the queue will wait for it to fulfill before moving to the next task. 19 | Rejected promises don't cause the queue to stop executing tasks, but are reported in the `error` event (see below). 20 | 21 | ```js 22 | <%= examples["Promises"] %> 23 | ``` 24 | 25 | ## Task cancellation 26 | 27 | Tasks waiting in the queue, as well as the currently executing task, can be cancelled. This is achieved by creating a `CancellationToken` object 28 | for every task pushed to the queue, and passing it (to the task function) as the last argument. The task can then query the token's `cancelled` property to check if it 29 | has been cancelled. The `Promise` returned by `push` is extended with a `cancel` method so that individual tasks can be cancelled. 30 | 31 | ```js 32 | <%= examples["Task cancellation"] %> 33 | ``` 34 | 35 | In the above example, the task is cancelled before the 100 ms timeout. 36 | 37 | When cancelling the current task, the queue will immediately schedule the next one, without waiting for the task to finish. 38 | It is the task's responsibility to abort when the cancellation token is set, thus avoiding invalid application state. 39 | When a task is cancelled, the corresponding `Promise` is rejected with the cancellation reason, regardless of where the task currently is in the execution chain (running, scheduled or queued). 40 | 41 | ## Timeouts 42 | 43 | Tasks can be pushed into the queue with a timeout, after which the queue will cancel the task (the timer starts when the task is run, not when queued). 44 | The timeout value is supplied to `push` in the second argument, which is interpreted as an options object for the task: 45 | 46 | ```js 47 | <%= examples["Timeouts"] %> 48 | ``` 49 | 50 | ## Passing arguments to the task 51 | 52 | In most scenarios, you will be using the queue to respond to frequent, asynchronous events. Consider the following code that processes push notifications 53 | coming from a server: 54 | 55 | ```js 56 | <%= examples["Arguments 1"] %> 57 | ``` 58 | 59 | Every time the event handler is called, it creates a new function (and a closure), which can lead to poor performance. 60 | Let's rewrite this, now using the `args` property of the task options: 61 | 62 | ```js 63 | <%= examples["Arguments 2"] %> 64 | ``` 65 | 66 | If the `args` member of the options object is an array, the queue will pass the elements of the array as arguments, otherwise 67 | the value is interpreted as the single argument to the task function. The cancellation token is always passed as the last argument. 68 | The last bit also means that you can't simply push a function that has a rest (`...`) parameter, or uses the `arguments` object, 69 | since the cancellation token would be appended. 70 | 71 | ## Waiting for all tasks to finish 72 | 73 | Use the `wait` method to obtain a `Promise` that fulfills when the queue is empty: 74 | 75 | ```js 76 | <%= examples["Wait"] %> 77 | ``` 78 | 79 | ## Closing the queue 80 | 81 | At certain points in your code, you may want to prevent adding more tasks to a queue (e.g. screen deactivation). 82 | The `close` method closes the queue (sets the `isClosed` property to `true`), and returns a `Promise` that fulfills when the queue is empty. 83 | Calling `push` on a closed queue will throw an exception. Optionally, `close` can cancel all remaining tasks: 84 | to do so, pass a truthful value as its first parameter. 85 | 86 | ```js 87 | <%= examples["Close"] %> 88 | ``` 89 | 90 | ## Handling errors 91 | 92 | Errors thrown inside a task are reported in the queue's `error` event (see below in the Events section). 93 | Exceptions thrown in event handlers, however, are catched and ignored to avoid inifinite loops of error handling code. 94 | 95 | ## Events 96 | 97 | `SequentialTaskQueue` implements the `on`, `removeListener` (`off`) and `once` methods of node's `EventEmitter` pattern. 98 | 99 | The following events are defined: 100 | 101 | ### error 102 | 103 | The `error` event is emitted when a task throws an error or the `Promise` returned by the task is rejected. The error object is 104 | passed as the first argument of the event handler. 105 | 106 | ### drained 107 | 108 | The `drained` event is emitted when a task has finished executing, and the queue is empty. A cancelled queue will also emit this event. 109 | 110 | ### timeout 111 | 112 | The `timeout` event is emitted when a task is cancelled due to an expired timeout. The event is emitted before calling `cancel` on the task's cancellation token. 113 | 114 | --- 115 | ## Changelog 116 | 117 | ### 1.2.0 118 | 119 | `SequentialTaskQueue.push` now returns a `Promise`. Earlier versions only returned a cancellation token. 120 | 121 | --- 122 | This file was generated using [gulp-template](http://github.com/sindresorhus/gulp-template) and [snip-text](http://github.com/BalassaMarton/snip-text) -------------------------------------------------------------------------------- /src/sequential-task-queue.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents an object that schedules a function for asynchronous execution. 3 | * The default implementation used by {@link SequentialTaskQueue} calls {@link setImmediate} when available, 4 | * and {@link setTimeout} otherwise. 5 | * @see {@link SequentialTaskQueue.defaultScheduler} 6 | * @see {@link TaskQueueOptions.scheduler} 7 | */ 8 | export interface Scheduler { 9 | /** 10 | * Schedules a callback for asynchronous execution. 11 | */ 12 | schedule(callback: Function): void; 13 | } 14 | 15 | /** 16 | * Object used for passing configuration options to the {@link SequentialTaskQueue} constructor. 17 | */ 18 | export interface SequentialTaskQueueOptions { 19 | /** 20 | * Assigns a name to the task queue for diagnostic purposes. The name does not need to be unique. 21 | */ 22 | name?: string; 23 | /** 24 | * Default timeout (in milliseconds) for tasks pushed to the queue. Default is 0 (no timeout). 25 | * */ 26 | timeout?: number; 27 | /** 28 | * Scheduler used by the queue. Defaults to {@link SequentialTaskQueue.defaultScheduler}. 29 | */ 30 | scheduler?: Scheduler; 31 | } 32 | 33 | /** 34 | * Options object for individual tasks. 35 | */ 36 | export interface TaskOptions { 37 | /** 38 | * Timeout for the task, in milliseconds. 39 | * */ 40 | timeout?: number; 41 | 42 | /** 43 | * Arguments to pass to the task. Useful for minimalising the number of Function objects and closures created 44 | * when pushing the same task multiple times, with different arguments. 45 | * 46 | * @example 47 | * // The following code creates a single Function object and no closures: 48 | * for (let i = 0; i < 100; i++) 49 | * queue.push(process, {args: [i]}); 50 | * function process(n) { 51 | * console.log(n); 52 | * } 53 | */ 54 | args?: any; 55 | } 56 | 57 | /** 58 | * Provides the API for querying and invoking task cancellation. 59 | */ 60 | export interface CancellationToken { 61 | /** 62 | * When `true`, indicates that the task has been cancelled. 63 | */ 64 | cancelled?: boolean; 65 | /** 66 | * An arbitrary object representing the reason of the cancellation. Can be a member of the {@link cancellationTokenReasons} object or an `Error`, etc. 67 | */ 68 | reason?: any; 69 | /** 70 | * Cancels the task for which the cancellation token was created. 71 | * @param reason - The reason of the cancellation, see {@link CancellationToken.reason} 72 | */ 73 | cancel(reason?: any); 74 | } 75 | 76 | /** 77 | * Standard cancellation reasons. {@link SequentialTaskQueue} sets {@link CancellationToken.reason} 78 | * to one of these values when cancelling a task for a reason other than the user code calling 79 | * {@link CancellationToken.cancel}. 80 | */ 81 | export var cancellationTokenReasons = { 82 | /** Used when the task was cancelled in response to a call to {@link SequentialTaskQueue.cancel} */ 83 | cancel: Object.create(null), 84 | /** Used when the task was cancelled after its timeout has passed */ 85 | timeout: Object.create(null) 86 | } 87 | 88 | /** 89 | * Standard event names used by {@link SequentialTaskQueue} 90 | */ 91 | export var sequentialTaskQueueEvents = { 92 | drained: "drained", 93 | error: "error", 94 | timeout: "timeout" 95 | } 96 | 97 | /** 98 | * Promise interface with the ability to cancel. 99 | */ 100 | export interface CancellablePromiseLike extends PromiseLike { 101 | /** 102 | * Cancels (and consequently, rejects) the task associated with the Promise. 103 | * @param reason - Reason of the cancellation. This value will be passed when rejecting this Promise. 104 | */ 105 | cancel(reason?: any): void; 106 | } 107 | 108 | /** 109 | * FIFO task queue to run tasks in predictable order, without concurrency. 110 | */ 111 | export class SequentialTaskQueue { 112 | 113 | static defaultScheduler: Scheduler = { 114 | schedule: callback => setTimeout(callback, 0) 115 | }; 116 | 117 | private queue: TaskEntry[] = []; 118 | private _isClosed: boolean = false; 119 | private waiters: Function[] = []; 120 | private defaultTimeout: number; 121 | private currentTask: TaskEntry; 122 | private scheduler: Scheduler; 123 | private events: { [key: string]: Function[] }; 124 | 125 | name: string; 126 | 127 | /** Indicates if the queue has been closed. Calling {@link SequentialTaskQueue.push} on a closed queue will result in an exception. */ 128 | get isClosed() { 129 | return this._isClosed; 130 | } 131 | 132 | /** 133 | * Creates a new instance of {@link SequentialTaskQueue} 134 | * @param options - Configuration options for the task queue. 135 | */ 136 | constructor(options?: SequentialTaskQueueOptions) { 137 | if (!options) 138 | options = {}; 139 | this.defaultTimeout = options.timeout; 140 | this.name = options.name || "SequentialTaskQueue"; 141 | this.scheduler = options.scheduler || SequentialTaskQueue.defaultScheduler; 142 | } 143 | 144 | /** 145 | * Adds a new task to the queue. 146 | * @param {Function} task - The function to call when the task is run 147 | * @param {TaskOptions} options - An object containing arguments and options for the task. 148 | * @returns {CancellablePromiseLike} A promise that can be used to await or cancel the task. 149 | */ 150 | push(task: Function, options?: TaskOptions): CancellablePromiseLike { 151 | if (this._isClosed) 152 | throw new Error(`${this.name} has been previously closed`); 153 | var taskEntry: TaskEntry = { 154 | callback: task, 155 | args: options && options.args ? (Array.isArray(options.args) ? options.args.slice() : [options.args]) : [], 156 | timeout: options && options.timeout !== undefined ? options.timeout : this.defaultTimeout, 157 | cancellationToken: { 158 | cancel: (reason?) => this.cancelTask(taskEntry, reason) 159 | }, 160 | resolve: undefined, 161 | reject: undefined 162 | }; 163 | taskEntry.args.push(taskEntry.cancellationToken); 164 | this.queue.push(taskEntry); 165 | this.scheduler.schedule(() => this.next()); 166 | var result = (new Promise((resolve, reject) => { 167 | taskEntry.resolve = resolve; 168 | taskEntry.reject = reject; 169 | }) as any) as CancellablePromiseLike; 170 | result.cancel = (reason?: any) => taskEntry.cancellationToken.cancel(reason); 171 | return result; 172 | } 173 | 174 | /** 175 | * Cancels the currently running task (if any), and clears the queue. 176 | * @returns {Promise} A Promise that is fulfilled when the queue is empty and the current task has been cancelled. 177 | */ 178 | cancel(): PromiseLike { 179 | if (this.currentTask) 180 | this.cancelTask(this.currentTask, cancellationTokenReasons.cancel); 181 | var queue = this.queue.splice(0); 182 | // Cancel all and emit a drained event if there were tasks waiting in the queue 183 | if (queue.length) { 184 | queue.forEach(task => this.cancelTask(task, cancellationTokenReasons.cancel)); 185 | this.emit(sequentialTaskQueueEvents.drained); 186 | } 187 | return this.wait(); 188 | } 189 | 190 | /** 191 | * Closes the queue, preventing new tasks to be added. 192 | * Any calls to {@link SequentialTaskQueue.push} after closing the queue will result in an exception. 193 | * @param {boolean} cancel - Indicates that the queue should also be cancelled. 194 | * @returns {Promise} A Promise that is fulfilled when the queue has finished executing remaining tasks. 195 | */ 196 | close(cancel?: boolean): PromiseLike { 197 | if (!this._isClosed) { 198 | this._isClosed = true; 199 | if (cancel) 200 | return this.cancel(); 201 | } 202 | return this.wait(); 203 | } 204 | 205 | /** 206 | * Returns a promise that is fulfilled when the queue is empty. 207 | * @returns {Promise} 208 | */ 209 | wait(): PromiseLike { 210 | if (!this.currentTask && this.queue.length === 0) 211 | return Promise.resolve(); 212 | return new Promise(resolve => { 213 | this.waiters.push(resolve); 214 | }); 215 | } 216 | 217 | /** 218 | * Adds an event handler for a named event. 219 | * @param {string} evt - Event name. See the readme for a list of valid events. 220 | * @param {Function} handler - Event handler. When invoking the handler, the queue will set itself as the `this` argument of the call. 221 | */ 222 | on(evt: string, handler: Function) { 223 | this.events = this.events || {}; 224 | (this.events[evt] || (this.events[evt] = [])).push(handler); 225 | } 226 | 227 | /** 228 | * Adds a single-shot event handler for a named event. 229 | * @param {string} evt - Event name. See the readme for a list of valid events. 230 | * @param {Function} handler - Event handler. When invoking the handler, the queue will set itself as the `this` argument of the call. 231 | */ 232 | once(evt: string, handler: Function) { 233 | var cb = (...args: any[]) => { 234 | this.removeListener(evt, cb); 235 | handler.apply(this, args); 236 | }; 237 | this.on(evt, cb); 238 | } 239 | 240 | /** 241 | * Removes an event handler. 242 | * @param {string} evt - Event name 243 | * @param {Function} handler - Event handler to be removed 244 | */ 245 | removeListener(evt: string, handler: Function) { 246 | if (this.events) { 247 | var list = this.events[evt]; 248 | if (list) { 249 | var i = 0; 250 | while (i < list.length) { 251 | if (list[i] === handler) 252 | list.splice(i, 1); 253 | else 254 | i++; 255 | } 256 | } 257 | } 258 | } 259 | 260 | /** @see {@link SequentialTaskQueue.removeListener} */ 261 | off(evt: string, handler: Function) { 262 | return this.removeListener(evt, handler); 263 | } 264 | 265 | protected emit(evt: string, ...args: any[]) { 266 | if (this.events && this.events[evt]) 267 | try { 268 | this.events[evt].forEach(fn => fn.apply(this, args)); 269 | } catch (e) { 270 | console.error(`${this.name}: Exception in '${evt}' event handler`, e); 271 | } 272 | } 273 | 274 | protected next() { 275 | // Try running the next task, if not currently running one 276 | if (!this.currentTask) { 277 | var task = this.queue.shift(); 278 | // skip cancelled tasks 279 | while (task && task.cancellationToken.cancelled) 280 | task = this.queue.shift(); 281 | if (task) { 282 | try { 283 | this.currentTask = task; 284 | if (task.timeout) { 285 | task.timeoutHandle = setTimeout( 286 | () => { 287 | this.emit(sequentialTaskQueueEvents.timeout); 288 | this.cancelTask(task, cancellationTokenReasons.timeout); 289 | }, 290 | task.timeout); 291 | } 292 | let res = task.callback.apply(undefined, task.args); 293 | if (res && isPromise(res)) { 294 | res.then(result => { 295 | task.result = result; 296 | this.doneTask(task); 297 | }, 298 | err => { 299 | this.doneTask(task, err); 300 | }); 301 | } else { 302 | task.result = res; 303 | this.doneTask(task); 304 | } 305 | 306 | } catch (e) { 307 | this.doneTask(task, e); 308 | } 309 | } else { 310 | // queue is empty, call waiters 311 | this.callWaiters(); 312 | } 313 | } 314 | } 315 | 316 | private cancelTask(task: TaskEntry, reason?: any) { 317 | task.cancellationToken.cancelled = true; 318 | task.cancellationToken.reason = reason; 319 | this.doneTask(task); 320 | } 321 | 322 | private doneTask(task: TaskEntry, error?: any) { 323 | if (task.timeoutHandle) 324 | clearTimeout(task.timeoutHandle); 325 | task.cancellationToken.cancel = noop; 326 | if (error) { 327 | this.emit(sequentialTaskQueueEvents.error, error); 328 | task.reject.call(undefined, error); 329 | } else if (task.cancellationToken.cancelled) 330 | task.reject.call(undefined, task.cancellationToken.reason) 331 | else 332 | task.resolve.call(undefined, task.result); 333 | 334 | if (this.currentTask === task) { 335 | this.currentTask = undefined; 336 | if (!this.queue.length) { 337 | this.emit(sequentialTaskQueueEvents.drained); 338 | this.callWaiters(); 339 | } 340 | else 341 | this.scheduler.schedule(() => this.next()); 342 | } 343 | } 344 | 345 | private callWaiters() { 346 | let waiters = this.waiters.splice(0); 347 | waiters.forEach(waiter => waiter()); 348 | } 349 | } 350 | 351 | interface TaskEntry { 352 | args: any[]; 353 | callback: Function; 354 | timeout?: number; 355 | timeoutHandle?: any; 356 | cancellationToken: CancellationToken; 357 | result?: any; 358 | resolve: (value: any | PromiseLike) => void; 359 | reject: (reason?: any) => void; 360 | } 361 | 362 | function noop() { 363 | } 364 | 365 | function isPromise(obj: any): obj is PromiseLike { 366 | return (obj && typeof obj.then === "function"); 367 | } 368 | 369 | SequentialTaskQueue.defaultScheduler = { 370 | schedule: typeof setImmediate === "function" 371 | ? callback => setImmediate(<(...args: any[]) => void>callback) 372 | : callback => setTimeout(<(...args: any[]) => void>callback, 0) 373 | }; 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | *.js -------------------------------------------------------------------------------- /test/sequential-task-queue-spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { SequentialTaskQueue, CancellationToken, cancellationTokenReasons } from "../src/sequential-task-queue"; 3 | import * as sinon from "sinon"; 4 | 5 | process.on('unhandledRejection', (err, p) => { 6 | console.log('Suppressed unhandled rejection'); 7 | }); 8 | 9 | describe("SequentialTaskQueue", () => { 10 | 11 | it("should execute a task", () => { 12 | var queue = new SequentialTaskQueue(); 13 | var spy = sinon.spy(); 14 | queue.push(spy); 15 | return queue.wait().then(() => { assert(spy.called); }); 16 | }); 17 | 18 | it("should execute a task with args (array)", () => { 19 | var queue = new SequentialTaskQueue(); 20 | var spy = sinon.spy(); 21 | queue.push(spy, { args: [1, 2, 3] }); 22 | queue.push(spy, { args: [4, 5, 6] }); 23 | return queue.wait().then(() => { 24 | assert(spy.callCount == 2); 25 | assert.deepEqual(spy.args[0].slice(0, 3), [1, 2, 3]); 26 | assert.deepEqual(spy.args[1].slice(0, 3), [4, 5, 6]); 27 | }); 28 | }); 29 | 30 | it("should execute a task with args (single value)", () => { 31 | var queue = new SequentialTaskQueue(); 32 | var spy = sinon.spy(); 33 | queue.push(spy, { args: "foo" }); 34 | queue.push(spy, { args: "bar" }); 35 | return queue.wait().then(() => { 36 | assert(spy.callCount == 2); 37 | assert.deepEqual(spy.args[0].slice(0, 1), ["foo"]); 38 | assert.deepEqual(spy.args[1].slice(0, 1), ["bar"]); 39 | }); 40 | }); 41 | 42 | it("should execute a chain of tasks", function () { 43 | 44 | this.timeout(0); 45 | 46 | var results = []; 47 | 48 | function createTask(id: number) { 49 | return () => { 50 | results.push(id); 51 | }; 52 | } 53 | 54 | function createAsyncTask(id: number) { 55 | return () => new Promise(resolve => { 56 | results.push(id); 57 | resolve(); 58 | }); 59 | } 60 | 61 | function createScheduledTask(id: number) { 62 | return () => new Promise(resolve => { 63 | setTimeout(() => { 64 | results.push(id); 65 | resolve(); 66 | }, Math.floor(Math.random() * 100)); 67 | }); 68 | } 69 | 70 | var functions: Function[] = [createTask, createAsyncTask, createScheduledTask]; 71 | 72 | var queue = new SequentialTaskQueue(); 73 | var count = 10; 74 | var expected = []; 75 | var idx = 0; 76 | while (idx < count) { 77 | for (let i = 0; i < functions.length; i++) { 78 | for (let j = 0; j < functions.length; j++) { 79 | expected.push(idx); 80 | queue.push(functions[i](idx)); 81 | idx++; 82 | expected.push(idx); 83 | queue.push(functions[j](idx)); 84 | idx++; 85 | } 86 | } 87 | } 88 | return queue.wait().then(() => assert.deepEqual(results, expected)); 89 | 90 | }); 91 | 92 | describe("# push: Promise", () => { 93 | 94 | it("should resolve when task is done", () => { 95 | var queue = new SequentialTaskQueue(); 96 | var p = queue.push(() => { 97 | return 123; 98 | }); 99 | return p.then(result => assert.equal(result, 123)); 100 | }); 101 | 102 | it("should resolve when async task is done", () => { 103 | var queue = new SequentialTaskQueue(); 104 | var p = queue.push(() => { 105 | return new Promise(resolve => setTimeout(() => resolve(123), 100)); 106 | }); 107 | return p.then(result => assert.equal(result, 123)); 108 | }); 109 | 110 | it("should reject when task is cancelled", () => { 111 | var queue = new SequentialTaskQueue(); 112 | var p = queue.push(() => { 113 | return new Promise(resolve => setTimeout(() => resolve(123), 200)); 114 | }); 115 | setTimeout(() => p.cancel("meh"), 50); 116 | return p.then(result => assert.ok(false), (reason) => assert.equal(reason, "meh")); 117 | }); 118 | 119 | it("should reject when task fails", () => { 120 | var queue = new SequentialTaskQueue(); 121 | var p = queue.push(() => { 122 | throw "fail"; 123 | }); 124 | return p.then(result => assert.ok(false), (reason) => assert.equal(reason, "fail")); 125 | }); 126 | 127 | }); 128 | 129 | describe("# wait", () => { 130 | it("should resolve when queue is empty", () => { 131 | var queue = new SequentialTaskQueue(); 132 | return queue.wait(); 133 | }); 134 | 135 | it("should resolve after synchronous task", () => { 136 | var queue = new SequentialTaskQueue(); 137 | var spy = sinon.spy(); 138 | queue.push(() => spy()); 139 | return queue.wait().then(() => assert(spy.called)); 140 | }); 141 | 142 | it("should resolve after previously resolved Promise", () => { 143 | var queue = new SequentialTaskQueue(); 144 | queue.push(() => Promise.resolve()); 145 | return queue.wait(); 146 | }); 147 | 148 | it("should resolve after resolved Promise", () => { 149 | var queue = new SequentialTaskQueue(); 150 | queue.push(() => new Promise(resolve => { resolve(); })); 151 | return queue.wait(); 152 | }); 153 | 154 | it("should resolve after resolved deferred Promise", () => { 155 | var queue = new SequentialTaskQueue(); 156 | queue.push(() => new Promise(resolve => { 157 | setTimeout(resolve, 50); 158 | })); 159 | return queue.wait(); 160 | }); 161 | 162 | it("should resolve after throw", () => { 163 | var queue = new SequentialTaskQueue(); 164 | queue.push(() => { 165 | throw new Error(); 166 | }); 167 | return queue.wait(); 168 | }); 169 | 170 | it("should resolve after previously rejected Promise", () => { 171 | var queue = new SequentialTaskQueue(); 172 | queue.push(() => Promise.reject("rejected")); 173 | return queue.wait(); 174 | }); 175 | 176 | it("should resolve after rejected Promise", () => { 177 | var queue = new SequentialTaskQueue(); 178 | queue.push(() => new Promise((resolve, reject) => { 179 | reject(); 180 | })); 181 | return queue.wait(); 182 | }); 183 | 184 | it("should resolve after rejected deferred Promise", () => { 185 | var queue = new SequentialTaskQueue(); 186 | queue.push(() => new Promise((resolve, reject) => { 187 | setTimeout(reject, 50); 188 | })); 189 | return queue.wait(); 190 | }); 191 | 192 | it("should resolve after multiple calls", () => { 193 | var queue = new SequentialTaskQueue(); 194 | queue.push(() => new Promise((resolve, reject) => { 195 | setTimeout(resolve, 50); 196 | })); 197 | var p1 = queue.wait(); 198 | var p2 = queue.wait(); 199 | var p3 = queue.wait().then(() => queue.wait()); 200 | return Promise.all([p1, p2, p3]); 201 | }); 202 | 203 | it("should resolve after cancel", () => { 204 | var queue = new SequentialTaskQueue(); 205 | queue.push(() => new Promise((resolve, reject) => { 206 | setTimeout(resolve, 50); 207 | })); 208 | queue.push(() => { }); 209 | var p = queue.wait(); 210 | queue.cancel(); 211 | return p; 212 | }); 213 | }); 214 | 215 | describe("# event: error", () => { 216 | 217 | it("should notify of thrown error", () => { 218 | var queue = new SequentialTaskQueue(); 219 | var spy = sinon.spy(); 220 | queue.on("error", spy); 221 | queue.push(() => { 222 | throw "fail"; 223 | }); 224 | return queue.wait().then(() => { assert(spy.calledWith("fail")); }); 225 | }); 226 | 227 | it("should notify of previously rejected Promise", () => { 228 | var queue = new SequentialTaskQueue(); 229 | var spy = sinon.spy(); 230 | queue.on("error", spy); 231 | queue.push(() => Promise.reject("rejected")); 232 | return queue.wait().then(() => assert(spy.calledWith("rejected"))); 233 | }); 234 | 235 | it("should notify of rejected Promise", () => { 236 | var queue = new SequentialTaskQueue(); 237 | var spy = sinon.spy(); 238 | queue.on("error", spy); 239 | queue.push(() => new Promise((resolve, reject) => { 240 | reject("rejected"); 241 | })); 242 | return queue.wait().then(() => assert(spy.calledWith("rejected"))); 243 | }); 244 | 245 | it("should notify of rejected deferred Promise", () => { 246 | var queue = new SequentialTaskQueue(); 247 | var spy = sinon.spy(); 248 | queue.on("error", spy); 249 | queue.push(() => new Promise((resolve, reject) => { 250 | setTimeout(() => reject("rejected"), 50); 251 | })); 252 | return queue.wait().then(() => assert(spy.calledWith("rejected"))); 253 | }); 254 | 255 | it("should catch and report exception in handler", () => { 256 | var queue = new SequentialTaskQueue(); 257 | sinon.spy(console, "error"); 258 | queue.on("error", () => { 259 | throw "Outer error"; 260 | }); 261 | queue.push(() => { 262 | throw "Inner error"; 263 | }); 264 | return queue.wait().then(() => { 265 | try { 266 | // hard coded this error message, see SequentialTaskQueue.emit if this test fails 267 | assert((console.error).calledWith("SequentialTaskQueue: Exception in 'error' event handler", "Outer error")); 268 | } finally { 269 | (console.error).restore(); 270 | } 271 | }); 272 | }); 273 | }); 274 | 275 | describe("# event: drained", () => { 276 | 277 | it("should notify when single-task chain has finished", () => { 278 | var queue = new SequentialTaskQueue(); 279 | var spy = sinon.spy(); 280 | queue.on("drained", spy); 281 | queue.push(() => { }); 282 | return queue.wait().then(() => { assert(spy.called); }); 283 | }); 284 | 285 | it("should notify when all tasks have finished", () => { 286 | var queue = new SequentialTaskQueue(); 287 | var spy = sinon.spy(); 288 | queue.on("drained", () => spy("drained")); 289 | queue.push(() => { 290 | spy(1); 291 | }); 292 | queue.push(() => new Promise(resolve => { 293 | setTimeout(() => { 294 | spy(2); 295 | resolve(); 296 | }, 10) 297 | })); 298 | queue.push(() => { 299 | spy(3); 300 | }); 301 | return queue.wait().then(() => { assert.deepEqual(spy.args, [[1], [2], [3], ["drained"]]); }); 302 | }); 303 | 304 | it("should notify after cancel", () => { 305 | var queue = new SequentialTaskQueue(); 306 | var spy = sinon.spy(); 307 | queue.on("drained", () => spy("drained")); 308 | queue.push(() => { spy(1) }); 309 | queue.push(() => { spy(2) }); 310 | return queue.cancel().then(() => { assert.deepEqual(spy.args, [["drained"]]); }); 311 | }); 312 | 313 | it("should not notify when empty queue is cancelled", () => { 314 | var queue = new SequentialTaskQueue(); 315 | var spy = sinon.spy(); 316 | queue.on("drained", spy); 317 | return queue.cancel().then(() => { assert(spy.notCalled); }); 318 | }); 319 | }); 320 | 321 | describe("# event: timeout", () => { 322 | it("should notify on timeout", () => { 323 | var queue = new SequentialTaskQueue(); 324 | var spy = sinon.spy(); 325 | queue.on("timeout", () => spy("timeout")); 326 | queue.push((ct: CancellationToken) => new Promise(resolve => { 327 | setTimeout(() => { 328 | if (!ct.cancelled) { 329 | spy("hello"); 330 | resolve(); 331 | } 332 | }, 500); 333 | }), { timeout: 100 }); 334 | return queue.wait().then(() => { assert.deepEqual(spy.args, [["timeout"]]) }); 335 | }); 336 | }); 337 | 338 | describe("# cancel", () => { 339 | 340 | it("should prevent queued tasks from running", () => { 341 | var queue = new SequentialTaskQueue(); 342 | var res = []; 343 | queue.push(() => res.push(1)); 344 | queue.push(() => new Promise(resolve => { 345 | setTimeout(() => { 346 | res.push(2); 347 | resolve(); 348 | }, 50); 349 | })); 350 | queue.push(() => new Promise(resolve => { 351 | setTimeout(() => { 352 | res.push(3); 353 | resolve(); 354 | }, 10); 355 | })); 356 | return queue.cancel().then(() => { 357 | assert.deepEqual(res, []); 358 | }); 359 | }); 360 | 361 | it("should cancel current task", () => { 362 | var queue = new SequentialTaskQueue(); 363 | var spy = sinon.spy(); 364 | 365 | queue.push((ct: CancellationToken) => { 366 | queue.cancel(); 367 | if (ct.cancelled) 368 | return; 369 | spy(); 370 | }); 371 | return queue.wait().then(() => assert(spy.notCalled)); 372 | }); 373 | 374 | it("should cancel current deferred task", () => { 375 | var queue = new SequentialTaskQueue(); 376 | var spy = sinon.spy(); 377 | 378 | queue.push((ct: CancellationToken) => 379 | new Promise((resolve, reject) => { 380 | setTimeout(() => { 381 | // cancel() should not have been cancelled at this point 382 | if (ct.cancelled) 383 | reject("cancelled"); 384 | else { 385 | spy(1); 386 | resolve(); 387 | } 388 | }, 389 | 10); 390 | }).then(() => new Promise((resolve, reject) => { 391 | setTimeout(() => { 392 | // cancel() should have been cancelled at this point 393 | if (ct.cancelled) 394 | reject("cancelled"); 395 | else { 396 | spy(2); 397 | resolve(); 398 | } 399 | }, 400 | 100); 401 | }))); 402 | setTimeout(() => queue.cancel(), 50); 403 | return queue.wait().then(() => { 404 | assert(spy.calledWith(1) && !spy.calledWith(2)); 405 | }); 406 | }); 407 | }); 408 | 409 | describe("# timeout", 410 | () => { 411 | it("should cancel task after timeout", 412 | () => { 413 | var queue = new SequentialTaskQueue(); 414 | var spy = sinon.spy(); 415 | var err = sinon.spy(); 416 | var timeouts = [50, 500, 50]; 417 | 418 | function pushTask(id, delay) { 419 | queue.push((ct: CancellationToken) => new Promise(resolve => { 420 | setTimeout(() => { 421 | if (!ct.cancelled) 422 | spy(id); 423 | resolve(); 424 | }, delay) 425 | }), { timeout: 200 }); 426 | } 427 | 428 | pushTask(1, 50); 429 | pushTask(2, 500); 430 | pushTask(3, 50); 431 | 432 | return queue.wait().then(() => { 433 | assert.deepEqual(spy.args, [[1], [3]]); 434 | }); 435 | }); 436 | }); 437 | 438 | describe("# close", () => { 439 | 440 | it("should prevent adding more tasks", () => { 441 | 442 | var queue = new SequentialTaskQueue(); 443 | queue.push(() => { }); 444 | queue.close(); 445 | assert.throws(() => { 446 | queue.push(() => { }); 447 | }); 448 | 449 | }); 450 | 451 | it("should execute remaining tasks", () => { 452 | 453 | var queue = new SequentialTaskQueue(); 454 | var res = []; 455 | queue.push(() => res.push(1)); 456 | queue.push(() => res.push(2)); 457 | queue.close(); 458 | try { 459 | queue.push(() => res.push(3)); 460 | } catch (e) { 461 | } 462 | return queue.wait().then(() => { assert.deepEqual(res, [1, 2]); }); 463 | }); 464 | 465 | }); 466 | 467 | describe("# once", () => { 468 | 469 | it("should register single-shot event handler", () => { 470 | 471 | var queue = new SequentialTaskQueue(); 472 | var spy = sinon.spy(); 473 | queue.once("error", spy); 474 | queue.push(() => { throw new Error("1"); }); 475 | queue.push(() => { throw new Error("2"); }); 476 | return queue.wait().then(() => assert(spy.calledOnce)); 477 | }); 478 | 479 | }); 480 | }); 481 | 482 | describe("CancellationToken", () => { 483 | describe("# cancel", () => { 484 | it("should prevent task from running", () => { 485 | var queue = new SequentialTaskQueue(); 486 | var res = []; 487 | queue.push(() => res.push(1)); 488 | var ct = queue.push(() => res.push(2)); 489 | queue.push(() => res.push(3)); 490 | ct.cancel(); 491 | return queue.wait().then(() => { 492 | assert.deepEqual(res, [1, 3]); 493 | }); 494 | }); 495 | 496 | it("should cancel running task and execute the next one immediately", () => { 497 | var clock = sinon.useFakeTimers(); 498 | try { 499 | var queue = new SequentialTaskQueue(); 500 | var res = []; 501 | queue.push(() => res.push(1)); 502 | var ct = queue.push(token => new Promise((resolve, reject) => { 503 | if (token.cancelled) 504 | reject(); 505 | setTimeout(() => { 506 | if (token.cancelled) 507 | reject(); 508 | else { 509 | res.push(2); 510 | resolve(); 511 | } 512 | }, 500); 513 | })); 514 | queue.push(() => res.push(3)); 515 | clock.tick(100); 516 | ct.cancel(); 517 | clock.tick(1000); 518 | assert.deepEqual(res, [1, 3]); 519 | queue.wait(); 520 | } finally { 521 | clock.restore(); 522 | } 523 | }); 524 | }); 525 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "declaration": true, 7 | "lib": [ 8 | "es5", 9 | "es6" 10 | ], 11 | "types": [ 12 | "node", 13 | "sinon", 14 | "mocha" 15 | ] 16 | }, 17 | "exclude": [ 18 | "node_modules" 19 | ] 20 | } --------------------------------------------------------------------------------