├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── SECURITY.md ├── azure-pipelines.yml ├── package-lock.json ├── package.json ├── src └── SyncTasks.ts ├── test.html ├── test ├── SyncTasksTests.ts └── support │ ├── tsconfig.json │ └── webpack.config.js ├── tsconfig.eslint.json └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "project": "./tsconfig.eslint.json" 4 | }, 5 | "extends": ["skype"], 6 | "env": { 7 | "mocha": true 8 | }, 9 | "rules": { 10 | "no-console": ["error", { "allow": ["error"] }] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /src/.vs 3 | /src/bin 4 | /src/obj 5 | /dist 6 | /dist-test 7 | *.user 8 | test-results.xml -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /src/.vs 3 | /src/bin 4 | /src/obj 5 | *.user 6 | /SyncTasksTestsPack.js 7 | .eslintrc 8 | test.html 9 | tsconfig.json 10 | test 11 | dist-test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Microsoft Corporation 2 | 3 | All rights reserved. 4 | 5 | The MIT License (MIT) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SyncTasks 2 | 3 | [![Build Status](https://dev.azure.com/ms/SyncTasks/_apis/build/status/Microsoft.SyncTasks)](https://dev.azure.com/ms/SyncTasks/_build/latest?definitionId=13) 4 | 5 | Yet another promise library, but this one is designed intentionally against the ES6 promise pattern, which asynchronously resolves 6 | promise callbacks in the next tick of the JS engine. In many cases, asynchronous resolution is the safest and 7 | easiest-to-understand implementation of a promise, but it adds a huge delay to the resolution, which in most places is unnecessary. 8 | Moreover, when we attempted to wrap the IndexedDB architecture with standard ES6 promises, it falls apart, because IndexedDB 9 | closes database connections when control is passed back to the main thread. We started building 10 | [NoSQLProvider](https://www.github.com/Microsoft/NoSQLProvider/) and immediately ran into this problem. SyncTasks is the solution 11 | to that problem, but is also a performant answer to asynchronous programming problems in general. In addition, we've worked in 12 | a simple optional cancellation mechanism that chains through promise resolution as well (as long as you chain through SyncTasks 13 | promises, and don't mix in non-cancellation-supported promises.) 14 | 15 | ## Usage 16 | 17 | Usage of SyncTasks promises is somewhat similar to JQuery promises. If you need to create a promise deferral, you call 18 | `SyncTasks.Defer()` and it returns a `SyncTasks.Deferred` object. The usual flow is to stash the deferral away for your async 19 | logic to later resolve/reject, and call `.promise()` on the deferral and return that to your caller, so that the caller only 20 | works with the promise (for chaining and resolution-callback reasons). Any calls of resolve/reject are synchronously resolved 21 | before returning from the resolve/reject method. 22 | 23 | ### SyncTasks Static Reference 24 | 25 | SyncTasks has the basic `Defer` call, but also helper methods to save on common tasks for interacting with promises/deferrals: 26 | 27 | - `Defer()` - Returns a new blank `SyncTasks.Deferral`. 28 | - `Resolved(obj?)` - Returns a new `SyncTasks.Promise`, already in a resolved (success) state, with the optional passed 29 | resolution value. 30 | - `Rejected(obj?)` - Returns a new `SyncTasks.Promise`, already in a rejected (failure) state, with the optional passed 31 | resolution value. 32 | - `all([promises...])` - Returns a new `SyncTasks.Promise` that will resolve when all of the promises in the list have finished. 33 | If any of the promises resolved with failure, then `all` will resolve with failure, with the first non-`undefined` resolution 34 | value as the resolution value for `all`. If all are successful, then it will resolve successfully, with the resolution value 35 | being an array of the resolution values from the input promises. You can also pass values instead of promises in the input 36 | list, and those values will pass through as successful resolutions to the output list. 37 | - `race([promises...])` - Returns a new `SyncTasks.Promise` that will resolve when any one of the promises in the list finish. 38 | Resolves or rejects the output promise based on the resolution type and value of the first finished promise in the list. 39 | - `asyncCallback(callback)` - Runs the specified callback function on the next tick of the javascript engine. Uses a shared 40 | queue, so that all callbacks are played on the next tick sequentially, not one per tick. This is the same mechanism used 41 | by `thenAsync` to queue the callbacks for the next tick, but is also a handy helper for more optimized defering of multiple 42 | callbacks to the next tick. 43 | - `fromThenable(thenable)` - A handy helper function to wrap any sort of `Thenable` (usually used for wrapping ES6 Promises) into 44 | a SyncTask. This takes the thenable and maps its success and failure cases into a new `SyncTasks.Promise` that resolve and 45 | reject with the results of the passed thenable. 46 | - `setTracingEnabled(boolean)` - This option allows enabling of double resolution tracing individually per Promise. 47 | Could be used in the release in cases then the problem couldn't be reproduced locally. 48 | If option enabled assert will give you two stack traces - for the first resolve and the second. By default, you would see only second resolve stack trace. 49 | Keep in mind that it adds an extra overhead as resolve method call will create an extra Error object, so it should be used with caution in the release. 50 | Estimated overhead on mobile is around 0.05ms per resolve/reject call on Nexus 5x android. 51 | 52 | ### SyncTasks.Deferred Reference 53 | 54 | A created `Deferral` object only has 4 methods: 55 | 56 | - `resolve(obj?)` - Resolves any promises created by the deferral with success. Takes an optional value that is passed through the 57 | success chain of any of the promises. 58 | - `reject(obj?)` - Resolves any promises created by the deferral with failure. Takes an optional value that is passed through the 59 | success chain of any of the promises. 60 | - `onCancel(callback)` - Adds a cancellation callback function that is called whenever non-resolved/rejected promises created by the deferral get `.cancel` 61 | called on them (or called on chained promises chain back up to this one.) This callback can be used to handle aborting async tasks that return promises. 62 | - `promise()` - Returns a `SyncTasks.Promise` object from the deferral, which is then passed around the world. 63 | 64 | ### SyncTasks.Promise Reference 65 | 66 | The `Promise` object is the public face of the async process that your `Deferred` is managing. You add various callbacks to 67 | the promise object depending on what types of events you want to get notified about, and what type of chaining you want to 68 | support. The methods supported are: 69 | 70 | - `then(successCallback, failureCallback?)` - The most common resolution mechanism for a promise. If the promise successfully 71 | resolves, it will call the first callback (and will pass it the optional resolution value). If it resolves with failure, 72 | the second callback is called (also with an optional resolution value). From inside each callback, you are able to return 73 | either a value, which will then turn the resolution into a successful resolution and change the resolution value to the 74 | value you just returned, or you may also return a new promise, which then continues the resolution chaining process until 75 | something returns only a value (or undefined). Returning anything from either the successCallback or failureCallback 76 | functions will chain to success of any subsequent promises. The `then` call returns a new promise which will be resolved 77 | based on the resolution chain of the callbacks from this call. 78 | - `thenAsync(successCallback?, failureCallback?)` - Has the same nuances and behavior as `then`, but the callbacks are called 79 | asynchronously, in ES6 fashion, on the next tick of the javascript host engine. 80 | - `always(callback)` - A synonym for calling `then` with the same callback for both parameters. Use this when you want to 81 | always alter the resolution chain, regardless of whether it came in with success or failure. Just like `then`, returns 82 | a new promise for chaining. 83 | - `catch(callback)` - Has the same effect as calling `then` with the specified callback for the failureCallback parameter and 84 | undefined for the successCallback -- this will only call your callback in the event of a chained failure case, but anything 85 | returned from your callback here will chain future callbacks to success. Just like `then`, returns a new promise for 86 | chaining. 87 | - `done(callback)`, `fail(callback)`, and `finally(callback)` - If you would like to observe, but not change, the resolution 88 | chain, you can use these functions. In all three cases, nothing you return from the callback functions will have any 89 | effect, these are only for observing the resolution chain. The only difference between them is that `done` is only called 90 | if the resolution chain is successful, `fail` is only called if the resolution chain is failure, and `finally` is called 91 | in either case. The callback function is always passed the optional resolution value, but in `finally`'s case, you have 92 | no idea whether it was called based on success or failure. These three functions all return the same original promise 93 | object, so you can attach multiple "observation functions" to the same promise without having to store it in a temporary 94 | variable. 95 | - `cancel(obj?)` - This method will notify the original deferral object of cancellation, and will pass it the optional value 96 | it is called with, but has no further effects. If the deferral is not handling cancellation, then this call will do 97 | absolutely nothing -- it does not guarantee any effects like failure resolution or any sort of stopping of chaining. 98 | The cancellation attempt will walk backwards as far up the promise chain as possible, so if you cancel a promise, be 99 | aware that it may end up calling cancel functions for deferrals many steps back in the promise chain. 100 | - `toEs6Promise()` - A helper function that wraps `SyncTasks.Promise` object "back" into ES6 Promise. It directly maps success 101 | and failure cases into respective calls to ES6 Promise constructor arguments `resolve` and `reject` 102 | 103 | ## Examples 104 | 105 | ### Simple Usage 106 | 107 | ```typescript 108 | function sendMeAStringLater(numberOfMilliseconds: number, theString: string): SyncTasks.Promise { 109 | let defer = SyncTasks.Defer(); 110 | setTimeout(() => { 111 | defer.resolve(theString); 112 | }, numberOfMilliseconds); 113 | return defer.promise(); 114 | } 115 | 116 | sendMeAStringLater(500, 'hi').then(myString => { 117 | console.log(myString); 118 | }); 119 | 120 | // 500 ms after running this, you will end up with a new console log line, "hi". 121 | ``` 122 | 123 | ### Add Cancellation 124 | 125 | ```typescript 126 | function sendMeAStringLater(numberOfMilliseconds: number, theString: string): SyncTasks.Promise { 127 | let defer = SyncTasks.Defer(); 128 | let didFinish = false; 129 | defer.onCancel(whyWasICancelled => { 130 | if (!didFinish) { 131 | didFinish = true; 132 | defer.reject(whyWasICancelled); 133 | } 134 | }); 135 | setTimeout(() => { 136 | // Make sure to bail here if it's already done. If you resolve a second time, it will throw an exception, since the 137 | // cancel already resolved it once. 138 | if (!didFinish) { 139 | didFinish = true; 140 | defer.resolve(theString); 141 | } 142 | }, numberOfMilliseconds); 143 | return defer.promise(); 144 | } 145 | 146 | let promise = sendMeAStringLater(500, 'hi').then(myString => { 147 | console.log('Success: ' + myString); 148 | }, errString => { 149 | console.log('Failure: ' + errString); 150 | }); 151 | 152 | setTimeout(() => { 153 | promise.cancel('Sorry'); 154 | }, 200); 155 | 156 | // 200 ms after running this, you will end up with a new console log line, "Failure: Sorry". The success case will not be 157 | // run because it was already resolved with failure. If you change the 200ms timer to 600ms, then your console will change to 158 | // "Success: hi" because the cancellation will happen after the success already did, so the `didFinish` check will swallow it. 159 | ``` 160 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | pool: 2 | vmImage: ubuntu-16.04 3 | 4 | steps: 5 | - task: NodeTool@0 6 | inputs: 7 | versionSpec: '8.x' 8 | displayName: 'Install Node.js' 9 | 10 | - script: | 11 | npm ci 12 | npm run build 13 | npm run test:web 14 | displayName: 'Build' 15 | 16 | - script: | 17 | npm run test:ci 18 | displayName: 'Test' 19 | 20 | - task: PublishTestResults@2 21 | inputs: 22 | testResultsFiles: '**/test-results.xml' 23 | condition: succeededOrFailed() 24 | displayName: 'Publish test results' 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "synctasks", 3 | "version": "0.3.4", 4 | "description": "An explicitly non-A+ Promise library that resolves promises synchronously", 5 | "author": "David de Regt ", 6 | "scripts": { 7 | "prepare": "tsc", 8 | "test": "mocha -r ts-node/register test/*.ts", 9 | "test:ci": "npm run test -- --reporter mocha-junit-reporter", 10 | "test:web": "webpack --config test/support/webpack.config.js --mode=development", 11 | "build": "npm run lint && tsc", 12 | "lint": "eslint --config .eslintrc --ext .ts src test", 13 | "lint:fix": "npm run lint -- --fix" 14 | }, 15 | "main": "dist/SyncTasks.js", 16 | "devDependencies": { 17 | "@types/assert": "1.4.3", 18 | "@types/mocha": "5.2.7", 19 | "@typescript-eslint/eslint-plugin": "2.0.0", 20 | "@typescript-eslint/parser": "2.0.0", 21 | "awesome-typescript-loader": "5.2.1", 22 | "eslint": "6.1.0", 23 | "eslint-config-skype": "0.4.0", 24 | "mocha": "10.1.0", 25 | "mocha-junit-reporter": "1.23.1", 26 | "ts-node": "8.3.0", 27 | "typescript": "3.5.3", 28 | "webpack": "4.39.2", 29 | "webpack-cli": "3.3.12" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/Microsoft/SyncTasks" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/Microsoft/SyncTasks/issues" 37 | }, 38 | "typings": "dist/SyncTasks.d.ts", 39 | "typescript": { 40 | "definition": "dist/SyncTasks.d.ts" 41 | }, 42 | "keywords": [ 43 | "promises", 44 | "synchronous" 45 | ], 46 | "license": "MIT" 47 | } 48 | -------------------------------------------------------------------------------- /src/SyncTasks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SyncTasks.ts 3 | * Author: David de Regt 4 | * Copyright: Microsoft 2015 5 | * 6 | * A very simple promise library that resolves all promises synchronously instead of 7 | * kicking them back to the main ticking thread. This affirmatively rejects the A+ 8 | * standard for promises, and is used for a combination of performance (wrapping 9 | * things back to the main thread is really slow) and because indexeddb loses 10 | * context for its calls if you send them around the event loop and transactions 11 | * automatically close. 12 | */ 13 | 14 | // Vendor specific handles removed in typescript 3.X 15 | declare function setImmediate(handler: (...args: any[]) => void): number; 16 | declare function setImmediate(handler: any, ...args: any[]): number; 17 | 18 | export const config = { 19 | // If we catch exceptions in success/fail blocks, it silently falls back to the fail case of the outer promise. 20 | // If this is global variable is true, it will also spit out a console.error with the exception for debugging. 21 | exceptionsToConsole: true, 22 | 23 | // Whether or not to actually attempt to catch exceptions with try/catch blocks inside the resolution cases. 24 | // Disable this for debugging when you'd rather the debugger caught the exception synchronously rather than 25 | // digging through a stack trace. 26 | catchExceptions: true, 27 | 28 | // Use this option in order to debug double resolution asserts locally. 29 | // Enabling this option in the release would have a negative impact on the application performance. 30 | traceEnabled: false, 31 | 32 | exceptionHandler: undefined as (((ex: Error) => void) | undefined), 33 | 34 | // If an ErrorFunc is not added to the task (then, catch, always) before the task rejects or synchonously 35 | // after that, then this function is called with the error. Default throws the error. 36 | unhandledErrorHandler: ((err: any) => { throw err; }) as (err: any) => void, 37 | }; 38 | 39 | export interface Es6Thenable { 40 | then(onFulfilled?: (value: R) => U | Es6Thenable, onRejected?: (error: any) => U | Es6Thenable): Es6Thenable; 41 | then(onFulfilled?: (value: R) => U | Es6Thenable, onRejected?: (error: any) => void): Es6Thenable; 42 | } 43 | 44 | export function fromThenable(thenable: Es6Thenable): STPromise { 45 | const deferred = Defer(); 46 | // NOTE: The {} around the error handling is critical to ensure that 47 | // we do not trigger "Possible unhandled rejection" warnings. By adding 48 | // the braces, the error handler rejects the outer promise, but returns 49 | // void. If we remove the braces, it would *also* return something which 50 | // would be unhandled 51 | thenable.then( 52 | value => { deferred.resolve(value); }, 53 | (err: any) => { deferred.reject(err); }); 54 | // Force async before this promise resolves to prevent ES6 promises from catching thrown exceptions downstream 55 | return deferred.promise().thenAsync(x => x); 56 | } 57 | 58 | function isThenable(object: any): object is Thenable { 59 | return object !== null && object !== void 0 && typeof object.then === 'function'; 60 | } 61 | 62 | function isCancelable(object: any): object is Cancelable { 63 | return object !== null && object !== void 0 && typeof object.cancel === 'function'; 64 | } 65 | 66 | // Runs trier(). If config.catchExceptions is set then any exception is caught and handed to catcher. 67 | function run(trier: () => T, catcher?: (e: Error) => C): T | C { 68 | if (config.catchExceptions) { 69 | // Any try/catch/finally block in a function makes the entire function ineligible for optimization is most JS engines. 70 | // Make sure this stays in a small/quick function, or break out into its own function. 71 | try { 72 | return trier(); 73 | } catch (e) { 74 | return catcher!!!(e); 75 | } 76 | } else { 77 | return trier(); 78 | } 79 | } 80 | 81 | let asyncCallbacks: (() => void)[] = []; 82 | 83 | // Ideally, we use setImmediate, but that's only supported on some environments. 84 | // Suggestion: Use the "setimmediate" NPM package to polyfill where it's not available. 85 | const useSetImmediate = typeof setImmediate !== 'undefined'; 86 | 87 | /** 88 | * This function will defer callback of the specified callback lambda until the next JS tick, simulating standard A+ promise behavior 89 | */ 90 | export function asyncCallback(callback: () => void): void { 91 | asyncCallbacks.push(callback); 92 | 93 | if (asyncCallbacks.length === 1) { 94 | // Start a callback for the next tick 95 | if (useSetImmediate) { 96 | setImmediate(resolveAsyncCallbacks); 97 | } else { 98 | setTimeout(resolveAsyncCallbacks, 0); 99 | } 100 | } 101 | } 102 | 103 | function resolveAsyncCallbacks(): void { 104 | const savedCallbacks = asyncCallbacks; 105 | asyncCallbacks = []; 106 | for (let i = 0; i < savedCallbacks.length; i++) { 107 | savedCallbacks[i](); 108 | } 109 | } 110 | 111 | export type SuccessFunc = (value: T) => U | Thenable; 112 | export type ErrorFunc = (error: any) => U | Thenable; 113 | export type CancelFunc = (context: any) => void; 114 | 115 | export interface Deferred { 116 | resolve(obj: T): Deferred; 117 | 118 | reject(obj?: any): Deferred; 119 | 120 | promise(): STPromise; 121 | 122 | onCancel(callback: CancelFunc): Deferred; 123 | } 124 | 125 | export interface Thenable { 126 | then(successFunc: SuccessFunc, errorFunc?: ErrorFunc): STPromise; 127 | } 128 | 129 | export interface Cancelable { 130 | // Will call any cancellation lambdas up the call chain, and reject a chain up the fail blocks 131 | cancel(context?: any): void; 132 | } 133 | 134 | export interface STPromise extends Thenable, Cancelable { 135 | catch(errorFunc: ErrorFunc): STPromise; 136 | 137 | finally(func: (value: T | any) => void): STPromise; 138 | 139 | always(func: (value: T | any) => U | Thenable): STPromise; 140 | 141 | done(successFunc: (value: T) => void): STPromise; 142 | 143 | fail(errorFunc: (error: any) => void): STPromise; 144 | 145 | // Defer the resolution of the then until the next event loop, simulating standard A+ promise behavior 146 | thenAsync(successFunc: SuccessFunc, errorFunc?: ErrorFunc): STPromise; 147 | 148 | setTracingEnabled(enabled: boolean): STPromise; 149 | 150 | toEs6Promise(): Promise; 151 | } 152 | 153 | export { STPromise as Promise }; 154 | 155 | // eslint-disable-next-line @typescript-eslint/no-namespace 156 | namespace Internal { 157 | export interface CallbackSet { 158 | successFunc?: SuccessFunc; 159 | failFunc?: ErrorFunc; 160 | task?: Deferred; 161 | asyncCallback?: boolean; 162 | wasCanceled?: boolean; 163 | cancelContext?: any; 164 | } 165 | 166 | export class SyncTask implements Deferred, STPromise { 167 | private _storedResolution: T | undefined; 168 | private _storedErrResolution: any | undefined; 169 | private _completedSuccess = false; 170 | private _completedFail = false; 171 | private _traceEnabled = false; 172 | // If _traceEnabled is true we save stacktrace of the first resolution of the task in the _completeStack 173 | // we are storing the Error instead of stack because it would be faster in standard scenario. 174 | private _completeStack: Error | undefined; 175 | 176 | private _cancelCallbacks: CancelFunc[] = []; 177 | private _cancelContext: any; 178 | private _wasCanceled = false; 179 | // The owner of this promise should not call cancel twice. However, cancellation through bubbling is independent of this. 180 | private _wasExplicitlyCanceled = false; 181 | 182 | private _resolving = false; 183 | 184 | private _storedCallbackSets: CallbackSet[] = []; 185 | 186 | // 'Handled' just means there was a callback set added. 187 | // Note: If that callback does not handle the error then that callback's task will be 'unhandled' instead of this one. 188 | private _mustHandleError = true; 189 | 190 | private static _rejectedTasks: SyncTask[] = []; 191 | private static _enforceErrorHandledTimer: number | undefined; 192 | 193 | private _addCallbackSet(set: CallbackSet, callbackWillChain: boolean): STPromise { 194 | const task = new SyncTask(); 195 | task.onCancel(context => { 196 | set.wasCanceled = true; 197 | set.cancelContext = context; 198 | // Note: Cancel due to bubbling should not throw if the public cancel is called before/after. 199 | this._cancelInternal(context); 200 | }); 201 | set.task = task; 202 | this._storedCallbackSets.push(set); 203 | 204 | if (callbackWillChain) { 205 | // The callback inherits responsibility for "handling" errors. 206 | this._mustHandleError = false; 207 | } else { 208 | // The callback can never "handle" errors since nothing can chain to it. 209 | task._mustHandleError = false; 210 | } 211 | 212 | // The _resolve* functions handle callbacks being added while they are running. 213 | if (!this._resolving) { 214 | if (this._completedSuccess) { 215 | this._resolveSuccesses(); 216 | } else if (this._completedFail) { 217 | this._resolveFailures(); 218 | } 219 | } 220 | 221 | return task.promise(); 222 | } 223 | 224 | onCancel(callback: CancelFunc): Deferred { 225 | // Only register cancel callback handler on promise that hasn't been completed 226 | if (!this._completedSuccess && !this._completedFail) { 227 | if (this._wasCanceled) { 228 | callback(this._cancelContext); 229 | } else { 230 | this._cancelCallbacks.push(callback); 231 | } 232 | } 233 | 234 | return this; 235 | } 236 | 237 | then(successFunc: SuccessFunc, errorFunc?: ErrorFunc): STPromise { 238 | return this._addCallbackSet({ 239 | successFunc: successFunc, 240 | failFunc: errorFunc, 241 | }, true); 242 | } 243 | 244 | thenAsync(successFunc: SuccessFunc, errorFunc?: ErrorFunc): STPromise { 245 | return this._addCallbackSet({ 246 | successFunc: successFunc, 247 | failFunc: errorFunc, 248 | asyncCallback: true, 249 | }, true); 250 | } 251 | 252 | catch(errorFunc: ErrorFunc): STPromise { 253 | return this._addCallbackSet({ 254 | failFunc: errorFunc, 255 | }, true); 256 | } 257 | 258 | always(func: (value: T | any) => U | STPromise): STPromise { 259 | return this._addCallbackSet({ 260 | successFunc: func, 261 | failFunc: func, 262 | }, true); 263 | } 264 | 265 | setTracingEnabled(enabled: boolean): STPromise { 266 | this._traceEnabled = enabled; 267 | return this; 268 | } 269 | 270 | // Finally should let you inspect the value of the promise as it passes through without affecting the then chaining 271 | // i.e. a failed promise with a finally after it should then chain to the fail case of the next then 272 | finally(func: (value: T | any) => void): STPromise { 273 | this._addCallbackSet({ 274 | successFunc: func, 275 | failFunc: func, 276 | }, false); 277 | return this; 278 | } 279 | 280 | done(successFunc: (value: T) => void): STPromise { 281 | this._addCallbackSet({ 282 | successFunc: successFunc, 283 | }, false); 284 | return this; 285 | } 286 | 287 | fail(errorFunc: (error: any) => void): STPromise { 288 | this._addCallbackSet({ 289 | failFunc: errorFunc, 290 | }, false); 291 | return this; 292 | } 293 | 294 | resolve(obj: T): Deferred { 295 | this._checkState(true); 296 | this._completedSuccess = true; 297 | this._storedResolution = obj; 298 | // Cannot cancel resolved promise - nuke chain 299 | this._cancelCallbacks = []; 300 | 301 | this._resolveSuccesses(); 302 | 303 | return this; 304 | } 305 | 306 | reject(obj?: any): Deferred { 307 | this._checkState(false); 308 | this._completedFail = true; 309 | this._storedErrResolution = obj; 310 | // Cannot cancel resolved promise - nuke chain 311 | this._cancelCallbacks = []; 312 | 313 | this._resolveFailures(); 314 | 315 | SyncTask._enforceErrorHandled(this); 316 | 317 | return this; 318 | } 319 | 320 | private _checkState(resolve: boolean): void { 321 | if (this._completedSuccess || this._completedFail) { 322 | if (this._completeStack) { 323 | console.error(this._completeStack.message, this._completeStack.stack); 324 | } 325 | 326 | const message = 'Failed to ' + (resolve ? 'resolve' : 'reject') + 327 | ': the task is already ' + (this._completedSuccess ? 'resolved' : 'rejected'); 328 | throw new Error(message); 329 | } 330 | 331 | if (config.traceEnabled || this._traceEnabled) { 332 | this._completeStack = new Error('Initial ' + resolve ? 'resolve' : 'reject'); 333 | } 334 | } 335 | 336 | // Make sure any rejected task has its failured handled. 337 | private static _enforceErrorHandled(task: SyncTask): void { 338 | if (!task._mustHandleError) { 339 | return; 340 | } 341 | 342 | SyncTask._rejectedTasks.push(task); 343 | 344 | // Wait for some async time in the future to check these tasks. 345 | if (!SyncTask._enforceErrorHandledTimer) { 346 | SyncTask._enforceErrorHandledTimer = setTimeout(() => { 347 | SyncTask._enforceErrorHandledTimer = undefined; 348 | 349 | const rejectedTasks = SyncTask._rejectedTasks; 350 | SyncTask._rejectedTasks = []; 351 | 352 | rejectedTasks.forEach((rejectedTask, i) => { 353 | if (rejectedTask._mustHandleError) { 354 | // Unhandled! 355 | config.unhandledErrorHandler(rejectedTask._storedErrResolution); 356 | } 357 | }); 358 | }, 0); 359 | } 360 | } 361 | 362 | cancel(context?: any): void { 363 | if (this._wasExplicitlyCanceled) { 364 | throw new Error('Already Canceled'); 365 | } 366 | 367 | this._wasExplicitlyCanceled = true; 368 | this._cancelInternal(context); 369 | } 370 | 371 | private _cancelInternal(context?: any): void { 372 | if (this._wasCanceled) { 373 | return; 374 | } 375 | 376 | this._wasCanceled = true; 377 | this._cancelContext = context; 378 | const callbacks = this._cancelCallbacks; 379 | this._cancelCallbacks = []; 380 | 381 | if (callbacks.length > 0) { 382 | callbacks.forEach(callback => { 383 | if (!this._completedSuccess && !this._completedFail) { 384 | callback(this._cancelContext); 385 | } 386 | }); 387 | } 388 | } 389 | 390 | static cancelOtherInternal(promise: Cancelable, context: any): void { 391 | // Warning: this cast is a bit dirty, but we need to avoid .cancel for SyncTasks. 392 | // Note: Cancel due to bubbling should not throw if the public cancel is called before/after. 393 | const task = promise as SyncTask; 394 | if (task._storedCallbackSets && task._cancelInternal) { 395 | // Is probably a SyncTask. 396 | task._cancelInternal(context); 397 | } else { 398 | promise.cancel(context); 399 | } 400 | } 401 | 402 | promise(): STPromise { 403 | return this; 404 | } 405 | 406 | private _resolveSuccesses(): void { 407 | this._resolving = true; 408 | 409 | // New callbacks can be added as the current callbacks run: use a loop to get through all of them. 410 | while (this._storedCallbackSets.length) { 411 | // Only iterate over the current list of callbacks. 412 | const callbacks = this._storedCallbackSets; 413 | this._storedCallbackSets = []; 414 | 415 | callbacks.forEach(callback => { 416 | if (callback.asyncCallback) { 417 | asyncCallback(() => this._resolveSuccessCallback(callback)); 418 | } else { 419 | this._resolveSuccessCallback(callback); 420 | } 421 | }); 422 | } 423 | this._resolving = false; 424 | } 425 | 426 | private _resolveSuccessCallback(callback: CallbackSet): void { 427 | if (callback.successFunc) { 428 | run(() => { 429 | const ret = callback.successFunc!!!(this._storedResolution!!!); 430 | if (isCancelable(ret)) { 431 | if (callback.wasCanceled) { 432 | SyncTask.cancelOtherInternal(ret, callback.cancelContext); 433 | } else { 434 | callback.task!!!.onCancel(context => SyncTask.cancelOtherInternal(ret, context)); 435 | } 436 | // Note: don't care if ret is canceled. We don't need to bubble out since this is already resolved. 437 | } 438 | if (isThenable(ret)) { 439 | // The success block of a then returned a new promise, so 440 | ret.then(r => { callback.task!!!.resolve(r); }, e => { callback.task!!!.reject(e); }); 441 | } else { 442 | callback.task!!!.resolve(ret); 443 | } 444 | }, e => { 445 | this._handleException(e, 'SyncTask caught exception in success block: ' + e.toString()); 446 | callback.task!!!.reject(e); 447 | }); 448 | } else { 449 | callback.task!!!.resolve(this._storedResolution); 450 | } 451 | } 452 | 453 | private _resolveFailures(): void { 454 | this._resolving = true; 455 | 456 | // New callbacks can be added as the current callbacks run: use a loop to get through all of them. 457 | while (this._storedCallbackSets.length) { 458 | // Only iterate over the current list of callbacks. 459 | const callbacks = this._storedCallbackSets; 460 | this._storedCallbackSets = []; 461 | 462 | callbacks.forEach(callback => { 463 | if (callback.asyncCallback) { 464 | asyncCallback(() => this._resolveFailureCallback(callback)); 465 | } else { 466 | this._resolveFailureCallback(callback); 467 | } 468 | }); 469 | } 470 | this._resolving = false; 471 | } 472 | 473 | private _resolveFailureCallback(callback: CallbackSet): void { 474 | if (callback.failFunc) { 475 | run(() => { 476 | const ret = callback.failFunc!!!(this._storedErrResolution); 477 | if (isCancelable(ret)) { 478 | if (callback.wasCanceled) { 479 | SyncTask.cancelOtherInternal(ret, callback.cancelContext); 480 | } else { 481 | callback.task!!!.onCancel(context => SyncTask.cancelOtherInternal(ret, context)); 482 | } 483 | // Note: don't care if ret is canceled. We don't need to bubble out since this is already rejected. 484 | } 485 | if (isThenable(ret)) { 486 | ret.then(r => { callback.task!!!.resolve(r); }, e => { callback.task!!!.reject(e); }); 487 | } else { 488 | // The failure has been handled: ret is the resolved value. 489 | callback.task!!!.resolve(ret); 490 | } 491 | }, e => { 492 | this._handleException(e, 'SyncTask caught exception in failure block: ' + e.toString()); 493 | callback.task!!!.reject(e); 494 | }); 495 | } else { 496 | callback.task!!!.reject(this._storedErrResolution); 497 | } 498 | } 499 | 500 | private _handleException(e: Error, message: string): void { 501 | if (config.exceptionsToConsole) { 502 | console.error(message); 503 | } 504 | if (config.exceptionHandler) { 505 | config.exceptionHandler(e); 506 | } 507 | } 508 | 509 | toEs6Promise(): Promise { 510 | return new Promise((resolve, reject) => this.then(resolve, reject)); 511 | } 512 | } 513 | } 514 | 515 | export type Raceable = T | Thenable | undefined | null; 516 | 517 | // Resolves once all of the given items resolve (non-thenables are 'resolved'). 518 | // Rejects once any of the given thenables reject. 519 | // Note: resolves immediately if given no items. 520 | export function all( 521 | values: [Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, 522 | Raceable] 523 | ): STPromise<[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]>; 524 | 525 | export function all( 526 | values: [Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, Raceable] 527 | ): STPromise<[T1, T2, T3, T4, T5, T6, T7, T8, T9]>; 528 | 529 | export function all( 530 | values: [Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, Raceable] 531 | ): STPromise<[T1, T2, T3, T4, T5, T6, T7, T8]>; 532 | 533 | export function all( 534 | values: [Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, Raceable] 535 | ): STPromise<[T1, T2, T3, T4, T5, T6, T7]>; 536 | 537 | export function all( 538 | values: [Raceable, Raceable, Raceable, Raceable, Raceable, Raceable] 539 | ): STPromise<[T1, T2, T3, T4, T5, T6]>; 540 | 541 | export function all( 542 | values: [Raceable, Raceable, Raceable, Raceable, Raceable] 543 | ): STPromise<[T1, T2, T3, T4, T5]>; 544 | 545 | export function all(values: [Raceable, Raceable, Raceable, Raceable]): STPromise<[T1, T2, T3, T4]>; 546 | 547 | export function all(values: [Raceable, Raceable, Raceable]): STPromise<[T1, T2, T3]>; 548 | 549 | export function all(values: [Raceable, Raceable]): STPromise<[T1, T2]>; 550 | 551 | export function all(values: (T | Thenable)[]): STPromise; 552 | 553 | export function all(items: any[]): STPromise { 554 | if (items.length === 0) { 555 | return Resolved([]); 556 | } 557 | 558 | const outTask = Defer(); 559 | let countRemaining = items.length; 560 | let foundError: any; 561 | const results = Array(items.length); 562 | 563 | outTask.onCancel((val) => { 564 | items.forEach(item => { 565 | if (isCancelable(item)) { 566 | Internal.SyncTask.cancelOtherInternal(item, val); 567 | } 568 | }); 569 | }); 570 | 571 | const checkFinish = (): void => { 572 | if (--countRemaining === 0) { 573 | if (foundError !== undefined) { 574 | outTask.reject(foundError); 575 | } else { 576 | outTask.resolve(results); 577 | } 578 | } 579 | }; 580 | 581 | items.forEach((item, index) => { 582 | if (isThenable(item)) { 583 | const task = item as Thenable; 584 | 585 | task.then(res => { 586 | results[index] = res; 587 | checkFinish(); 588 | }, err => { 589 | if (foundError === undefined) { 590 | foundError = (err !== undefined) ? err : true; 591 | } 592 | checkFinish(); 593 | }); 594 | } else { 595 | // Not a task, so resolve directly with the item 596 | results[index] = item; 597 | checkFinish(); 598 | } 599 | }); 600 | 601 | return outTask.promise(); 602 | } 603 | 604 | export function Defer(): Deferred { 605 | return new Internal.SyncTask(); 606 | } 607 | 608 | export function Resolved(): STPromise; 609 | export function Resolved(val: T): STPromise; 610 | export function Resolved(val?: T): STPromise { 611 | return new Internal.SyncTask().resolve(val!!!).promise(); 612 | } 613 | 614 | export function Rejected(val?: any): STPromise { 615 | return new Internal.SyncTask().reject(val).promise(); 616 | } 617 | 618 | // Resolves/Rejects once any of the given items resolve or reject (non-thenables are 'resolved'). 619 | // Note: never resolves if given no items. 620 | export function race( 621 | values: [ 622 | Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, 623 | Raceable 624 | ] 625 | ): STPromise; 626 | 627 | export function race( 628 | values: [ 629 | Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, Raceable 630 | ] 631 | ): STPromise; 632 | 633 | export function race( 634 | values: [Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, Raceable] 635 | ): STPromise; 636 | 637 | export function race( 638 | values: [Raceable, Raceable, Raceable, Raceable, Raceable, Raceable, Raceable] 639 | ): STPromise; 640 | 641 | export function race( 642 | values: [Raceable, Raceable, Raceable, Raceable, Raceable, Raceable] 643 | ): STPromise; 644 | export function race( 645 | values: [Raceable, Raceable, Raceable, Raceable, Raceable]): STPromise; 646 | 647 | export function race(values: [Raceable, Raceable, Raceable, Raceable]): STPromise; 648 | 649 | export function race(values: [Raceable, Raceable, Raceable]): STPromise; 650 | 651 | export function race(values: [Raceable, Raceable]): STPromise; 652 | 653 | export function race(values: (T | Thenable)[]): STPromise; 654 | 655 | export function race(items: any[]): STPromise { 656 | const outTask = Defer(); 657 | let hasSettled = false; 658 | 659 | outTask.onCancel((val) => { 660 | items.forEach(item => { 661 | if (isCancelable(item)) { 662 | Internal.SyncTask.cancelOtherInternal(item, val); 663 | } 664 | }); 665 | }); 666 | 667 | items.forEach(item => { 668 | if (isThenable(item)) { 669 | const task = item as Thenable; 670 | task.then(res => { 671 | if (!hasSettled) { 672 | hasSettled = true; 673 | outTask.resolve(res); 674 | } 675 | }, err => { 676 | if (!hasSettled) { 677 | hasSettled = true; 678 | outTask.reject(err); 679 | } 680 | }); 681 | } else { 682 | // Not a task, so resolve directly with the item 683 | if (!hasSettled) { 684 | hasSettled = true; 685 | outTask.resolve(item); 686 | } 687 | } 688 | }); 689 | 690 | return outTask.promise(); 691 | } 692 | 693 | export type RaceTimerResponse = { timedOut: true; result: undefined } | { timedOut: false; result: T }; 694 | export function raceTimer(promise: STPromise, timeMs: number): STPromise> { 695 | let timerDef = Defer>(); 696 | const token = setTimeout(() => { 697 | timerDef.resolve({ timedOut: true, result: undefined }); 698 | }, timeMs); 699 | const adaptedPromise = promise.then>(resp => { 700 | clearTimeout(token); 701 | return { timedOut: false, result: resp }; 702 | }); 703 | return race([adaptedPromise, timerDef.promise()]); 704 | } 705 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha Tests 5 | 6 | 7 | 8 |
9 | 10 | 13 | 14 | 15 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/SyncTasksTests.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as SyncTasks from '../src/SyncTasks'; 3 | 4 | describe('SyncTasks', function () { 5 | function noop(): void {/*noop*/} 6 | 7 | // Amount of time to wait to ensure all sync and trivially async (e.g. setTimeout(..., 0)) things have finished. 8 | // Useful to do something 'later'. 9 | const waitTime = 25; 10 | 11 | it('Simple - null resolve after then', (done) => { 12 | const task = SyncTasks.Defer(); 13 | 14 | task.promise().then(val => { 15 | assert.equal(val, null); 16 | done(); 17 | }, err => { 18 | assert(false); 19 | }); 20 | 21 | task.resolve(null); 22 | }); 23 | 24 | it('Simple - null then after resolve', (done) => { 25 | const task = SyncTasks.Defer(); 26 | 27 | task.resolve(null); 28 | 29 | task.promise().then(val => { 30 | assert.equal(val, null); 31 | done(); 32 | }, err => { 33 | assert(false); 34 | }); 35 | }); 36 | 37 | it('Simple - reject', (done) => { 38 | const task = SyncTasks.Defer(); 39 | 40 | task.reject(2); 41 | 42 | task.promise().then(val => { 43 | assert(false); 44 | }, err => { 45 | assert.equal(err, 2); 46 | done(); 47 | }); 48 | }); 49 | 50 | it('Chain from success to success with value', (done) => { 51 | const task = SyncTasks.Defer(); 52 | 53 | task.promise().then(val => { 54 | assert.equal(val, 3); 55 | return 4; 56 | }, err => { 57 | assert(false); 58 | return null; 59 | }).then(val => { 60 | assert.equal(val, 4); 61 | done(); 62 | }, err => { 63 | assert(false); 64 | }); 65 | 66 | task.resolve(3); 67 | }); 68 | 69 | it('Chain from error to success with value', (done) => { 70 | const task = SyncTasks.Defer(); 71 | 72 | task.promise().then(val => { 73 | assert(false); 74 | return -1; 75 | }, err => { 76 | assert.equal(err, 2); 77 | return 4; 78 | }).then(val => { 79 | assert.equal(val, 4); 80 | done(); 81 | }, err => { 82 | assert(false); 83 | }); 84 | 85 | task.reject(2); 86 | }); 87 | 88 | it('Chain from success to success with promise', (done) => { 89 | const task = SyncTasks.Defer(); 90 | 91 | task.promise().then(val => { 92 | assert.equal(val, 3); 93 | return SyncTasks.Resolved(4); 94 | }, err => { 95 | assert(false); 96 | return -1; 97 | }).then(val => { 98 | assert.equal(val, 4); 99 | done(); 100 | }, err => { 101 | assert(false); 102 | }); 103 | 104 | task.resolve(3); 105 | }); 106 | 107 | it('Chain from error to success with promise', (done) => { 108 | const task = SyncTasks.Defer(); 109 | 110 | task.promise().then(val => { 111 | assert(false); 112 | return -1; 113 | }, err => { 114 | assert.equal(err, 3); 115 | return SyncTasks.Resolved(4); 116 | }).then(val => { 117 | assert.equal(val, 4); 118 | done(); 119 | }, err => { 120 | assert(false); 121 | }); 122 | 123 | task.reject(3); 124 | }); 125 | 126 | it('Chain from success to error with promise', (done) => { 127 | const task = SyncTasks.Defer(); 128 | 129 | task.promise().then(val => { 130 | assert.equal(val, 2); 131 | return SyncTasks.Rejected(4); 132 | }, err => { 133 | assert(false); 134 | return -1; 135 | }).then(val => { 136 | assert(false); 137 | }, err => { 138 | assert.equal(err, 4); 139 | done(); 140 | }); 141 | 142 | task.resolve(2); 143 | }); 144 | 145 | it('Chain from error to error with promise', (done) => { 146 | const task = SyncTasks.Defer(); 147 | 148 | task.promise().then(val => { 149 | assert(false); 150 | return -1; 151 | }, err => { 152 | assert.equal(err, 2); 153 | return SyncTasks.Rejected(4); 154 | }).then(val => { 155 | assert(false); 156 | }, err => { 157 | assert.equal(err, 4); 158 | done(); 159 | }); 160 | 161 | task.reject(2); 162 | }); 163 | 164 | it('Chain from success to promise to success with promise', (done) => { 165 | const task = SyncTasks.Defer(); 166 | 167 | task.promise().then(val => { 168 | assert.equal(val, 3); 169 | const itask = SyncTasks.Resolved(4); 170 | return itask.then(val2 => { 171 | assert.equal(val2, 4, 'inner'); 172 | return 5; 173 | }); 174 | }, err => { 175 | assert(false); 176 | return 2; 177 | }).then(val => { 178 | assert.equal(val, 5, 'outer'); 179 | done(); 180 | }, err => { 181 | assert(false); 182 | }); 183 | 184 | task.resolve(3); 185 | }); 186 | 187 | it('Exception in success to error', (done) => { 188 | const task = SyncTasks.Defer(); 189 | 190 | SyncTasks.config.exceptionsToConsole = false; 191 | 192 | task.promise().then(val => { 193 | const blah: any = null; 194 | blah.blowup(); 195 | }, err => { 196 | assert(false); 197 | }).then(val => { 198 | assert(false); 199 | }, err => { 200 | SyncTasks.config.exceptionsToConsole = true; 201 | done(); 202 | }); 203 | 204 | task.resolve(3); 205 | }); 206 | 207 | it('Exception in error to error', (done) => { 208 | const task = SyncTasks.Defer(); 209 | 210 | SyncTasks.config.exceptionsToConsole = false; 211 | 212 | task.promise().then(val => { 213 | assert(false); 214 | }, err => { 215 | const blah: any = null; 216 | blah.blowup(); 217 | }).then(val => { 218 | assert(false); 219 | }, err => { 220 | SyncTasks.config.exceptionsToConsole = true; 221 | done(); 222 | }); 223 | 224 | task.reject(3); 225 | }); 226 | 227 | it('"done" basic', (done) => { 228 | const task = SyncTasks.Defer(); 229 | 230 | task.promise().then(val => { 231 | return 4; 232 | }, err => { 233 | assert(false); 234 | return -1; 235 | }).done(val => { 236 | assert.equal(val, 4); 237 | return 2; // should be ignored 238 | }).then(val => { 239 | assert.equal(val, 4); 240 | done(); 241 | }, err => { 242 | assert(false); 243 | }); 244 | 245 | task.resolve(3); 246 | }); 247 | 248 | it('"done" does not chain', (done) => { 249 | const task = SyncTasks.Defer(); 250 | const innertask = SyncTasks.Defer(); 251 | 252 | let innerFinished = false; 253 | 254 | task.promise().then(val => { 255 | return 4; 256 | }, err => { 257 | assert(false); 258 | return -1; 259 | }).done(val => { 260 | assert.equal(val, 4); 261 | return innertask.promise().then(() => { 262 | innerFinished = true; 263 | return 2; // should be ignored 264 | }); 265 | }).then(val => { 266 | assert(!innerFinished); 267 | assert.equal(val, 4); 268 | done(); 269 | }, err => { 270 | assert(false); 271 | }); 272 | 273 | task.resolve(3); 274 | innertask.resolve(1); 275 | }); 276 | 277 | it('Finally basic', (done) => { 278 | const task = SyncTasks.Defer(); 279 | 280 | task.promise().then(val => { 281 | return 4; 282 | }, err => { 283 | assert(false); 284 | return -1; 285 | }).finally(val => { 286 | assert.equal(val, 4); 287 | return 2; // should be ignored 288 | }).then(val => { 289 | assert.equal(val, 4); 290 | done(); 291 | }, err => { 292 | assert(false); 293 | }); 294 | 295 | task.resolve(3); 296 | }); 297 | 298 | it('Finally does not chain', (done) => { 299 | const task = SyncTasks.Defer(); 300 | const innertask = SyncTasks.Defer(); 301 | 302 | let innerFinished = false; 303 | 304 | task.promise().then(val => { 305 | return 4; 306 | }, err => { 307 | assert(false); 308 | return -1; 309 | }).finally(val => { 310 | assert.equal(val, 4); 311 | return innertask.promise().then(() => { 312 | innerFinished = true; 313 | return 2; // should be ignored 314 | }); 315 | }).then(val => { 316 | assert(!innerFinished); 317 | assert.equal(val, 4); 318 | done(); 319 | }, err => { 320 | assert(false); 321 | }); 322 | 323 | task.resolve(3); 324 | innertask.resolve(1); 325 | }); 326 | 327 | it('"all" basic success', (done) => { 328 | const task = SyncTasks.Defer(); 329 | const task2 = SyncTasks.Defer(); 330 | const task3 = SyncTasks.Defer(); 331 | const task4 = SyncTasks.Defer(); 332 | 333 | SyncTasks.all([task.promise(), task2.promise(), task3.promise(), task4.promise()]).then(rets => { 334 | assert.equal(rets.length, 4); 335 | assert.equal(rets[0], 1); 336 | assert.equal(rets[1], 2); 337 | assert.equal(rets[2], 3); 338 | assert.equal(rets[3], 4); 339 | done(); 340 | }, err => { 341 | assert(false); 342 | }); 343 | 344 | task.resolve(1); 345 | task2.resolve(2); 346 | task3.resolve(3); 347 | task4.resolve(4); 348 | }); 349 | 350 | it('"all" basic failure', (done) => { 351 | const task = SyncTasks.Defer(); 352 | const task2 = SyncTasks.Defer(); 353 | 354 | SyncTasks.all([task.promise(), task2.promise()]).then(rets => { 355 | assert(false); 356 | }, err => { 357 | done(); 358 | }); 359 | 360 | task.resolve(1); 361 | task2.reject(2); 362 | }); 363 | 364 | it('"all" zero tasks', (done) => { 365 | SyncTasks.all([]).then(rets => { 366 | assert.equal(rets.length, 0); 367 | done(); 368 | }, err => { 369 | assert(false); 370 | }); 371 | }); 372 | 373 | it('"all" single null task', (done) => { 374 | SyncTasks.all([null]).then(rets => { 375 | assert.equal(rets.length, 1); 376 | done(); 377 | }, err => { 378 | assert(false); 379 | }); 380 | }); 381 | 382 | it('"all" tasks and nulls', (done) => { 383 | const task = SyncTasks.Defer(); 384 | 385 | SyncTasks.all([null, task.promise()]).then(rets => { 386 | assert.equal(rets.length, 2); 387 | done(); 388 | }, err => { 389 | assert(false); 390 | }); 391 | 392 | task.resolve(1); 393 | }); 394 | 395 | it('"race" basic success', (done) => { 396 | const task = SyncTasks.Defer(); 397 | const task2 = SyncTasks.Defer(); 398 | const task3 = SyncTasks.Defer(); 399 | const task4 = SyncTasks.Defer(); 400 | 401 | SyncTasks.race([task.promise(), task2.promise(), task3.promise(), task4.promise()]).then(ret => { 402 | assert.equal(ret, 1); 403 | done(); 404 | }, err => { 405 | assert(false); 406 | }); 407 | 408 | task.resolve(1); 409 | task2.resolve(2); 410 | task3.resolve(3); 411 | task4.resolve(4); 412 | }); 413 | 414 | it('"race" basic failure', (done) => { 415 | const task = SyncTasks.Defer(); 416 | const task2 = SyncTasks.Defer(); 417 | 418 | SyncTasks.race([task.promise(), task2.promise()]).then(ret => { 419 | assert(false); 420 | }, err => { 421 | assert.equal(err, 1); 422 | done(); 423 | }); 424 | 425 | task.reject(1); 426 | task2.resolve(2); 427 | }); 428 | 429 | it('"race" zero tasks', (done) => { 430 | SyncTasks.race([]).then(ret => { 431 | assert(false); 432 | }, err => { 433 | assert(false); 434 | }); 435 | 436 | setTimeout(() => done(), 20); 437 | }); 438 | 439 | it('"race" single null task', (done) => { 440 | SyncTasks.race([null]).then(ret => { 441 | assert.equal(ret, null); 442 | done(); 443 | }, err => { 444 | assert(false); 445 | }); 446 | }); 447 | 448 | it('"race" tasks and nulls', (done) => { 449 | const task = SyncTasks.Defer(); 450 | 451 | SyncTasks.race([null, task.promise()]).then(ret => { 452 | assert.equal(ret, null); 453 | done(); 454 | }, err => { 455 | assert(false); 456 | }); 457 | 458 | task.resolve(2); 459 | }); 460 | 461 | it('"raceTimer" basic success', (done) => { 462 | const task = SyncTasks.Defer(); 463 | 464 | SyncTasks.raceTimer(task.promise(), 20).then(ret => { 465 | assert.equal(ret.timedOut, false); 466 | assert.equal(ret.result, 1); 467 | done(); 468 | }, err => { 469 | assert(false); 470 | }); 471 | 472 | task.resolve(1); 473 | }); 474 | 475 | it('"raceTimer" basic failure', (done) => { 476 | const task = SyncTasks.Defer(); 477 | 478 | SyncTasks.raceTimer(task.promise(), 20).then(ret => { 479 | assert(false); 480 | }, err => { 481 | assert.equal(err, 1); 482 | done(); 483 | }); 484 | 485 | task.reject(1); 486 | }); 487 | 488 | it('"raceTimer" basic timeout', (done) => { 489 | SyncTasks.raceTimer(SyncTasks.Defer().promise(), 10).then(ret => { 490 | assert.equal(ret.timedOut, true); 491 | assert.equal(ret.result, undefined); 492 | done(); 493 | }, err => { 494 | assert(false); 495 | }); 496 | }); 497 | 498 | it('Callbacks resolve synchronously', (done) => { 499 | const task = SyncTasks.Defer(); 500 | let resolvedCount = 0; 501 | 502 | task.promise().then(() => { 503 | ++resolvedCount; 504 | }, err => { 505 | assert(false); 506 | }); 507 | 508 | task.resolve(1); 509 | assert(resolvedCount === 1); 510 | done(); 511 | }); 512 | 513 | it('Callbacks resolve in order added', (done) => { 514 | const task = SyncTasks.Defer(); 515 | let resolvedCount = 0; 516 | 517 | task.promise().then(() => { 518 | assert(resolvedCount === 0); 519 | ++resolvedCount; 520 | }, err => { 521 | assert(false); 522 | }); 523 | 524 | task.promise().then(() => { 525 | assert(resolvedCount === 1); 526 | ++resolvedCount; 527 | }, err => { 528 | assert(false); 529 | }); 530 | 531 | task.resolve(1); 532 | assert(resolvedCount === 2); 533 | done(); 534 | }); 535 | 536 | it('Failure callbacks resolve in order added', (done) => { 537 | const task = SyncTasks.Defer(); 538 | let rejectedCount = 0; 539 | 540 | task.promise().then(() => { 541 | assert(false); 542 | }, err => { 543 | assert(rejectedCount === 0); 544 | ++rejectedCount; 545 | }); 546 | 547 | task.promise().then(() => { 548 | assert(false); 549 | }, err => { 550 | assert(rejectedCount === 1); 551 | ++rejectedCount; 552 | }); 553 | 554 | task.reject(1); 555 | assert(rejectedCount === 2); 556 | done(); 557 | }); 558 | 559 | it('"unhandledErrorHandler": Failure without any callback', (done) => { 560 | let unhandledErrorHandlerCalled = false; 561 | 562 | const oldUnhandledErrorHandler = SyncTasks.config.unhandledErrorHandler; 563 | SyncTasks.config.unhandledErrorHandler = () => { 564 | unhandledErrorHandlerCalled = true; 565 | }; 566 | 567 | SyncTasks.Rejected(); 568 | 569 | setTimeout(() => { 570 | SyncTasks.config.unhandledErrorHandler = oldUnhandledErrorHandler; 571 | assert(unhandledErrorHandlerCalled); 572 | done(); 573 | }, 20); 574 | }); 575 | 576 | it('"unhandledErrorHandler": Failure with only success callback', (done) => { 577 | let unhandledErrorHandlerCalled = false; 578 | 579 | const oldUnhandledErrorHandler = SyncTasks.config.unhandledErrorHandler; 580 | SyncTasks.config.unhandledErrorHandler = () => { 581 | unhandledErrorHandlerCalled = true; 582 | }; 583 | 584 | SyncTasks.Rejected().then(() => { 585 | assert(false); 586 | }); 587 | 588 | setTimeout(() => { 589 | SyncTasks.config.unhandledErrorHandler = oldUnhandledErrorHandler; 590 | assert(unhandledErrorHandlerCalled); 591 | done(); 592 | }, 20); 593 | }); 594 | 595 | it('"unhandledErrorHandler": Failure with success callback with failure callback', (done) => { 596 | let catchBlockReached = false; 597 | 598 | SyncTasks.Rejected().then(() => { 599 | assert(false); 600 | }).catch(() => { 601 | catchBlockReached = true; 602 | }); 603 | 604 | setTimeout(() => { 605 | assert(catchBlockReached); 606 | done(); 607 | }, 20); 608 | }); 609 | 610 | it('"unhandledErrorHandler": Success to inner failure without any callback', (done) => { 611 | let unhandledErrorHandlerCalled = false; 612 | 613 | const oldUnhandledErrorHandler = SyncTasks.config.unhandledErrorHandler; 614 | SyncTasks.config.unhandledErrorHandler = () => { 615 | unhandledErrorHandlerCalled = true; 616 | }; 617 | 618 | SyncTasks.Resolved(4).then(() => { 619 | return SyncTasks.Rejected(); 620 | }); 621 | 622 | setTimeout(() => { 623 | SyncTasks.config.unhandledErrorHandler = oldUnhandledErrorHandler; 624 | assert(unhandledErrorHandlerCalled); 625 | done(); 626 | }, 20); 627 | }); 628 | 629 | it('"unhandledErrorHandler": Failure to inner failure without any callback', (done) => { 630 | let unhandledErrorHandlerCalled = 0; 631 | 632 | const oldUnhandledErrorHandler = SyncTasks.config.unhandledErrorHandler; 633 | SyncTasks.config.unhandledErrorHandler = (n: number) => { 634 | unhandledErrorHandlerCalled = n; 635 | }; 636 | 637 | SyncTasks.Rejected(1).catch(() => { 638 | return SyncTasks.Rejected(2); 639 | }); 640 | // Note: the outer "catch" has no failure handling so the inner error leaks out. 641 | 642 | setTimeout(() => { 643 | SyncTasks.config.unhandledErrorHandler = oldUnhandledErrorHandler; 644 | assert.equal(unhandledErrorHandlerCalled, 2); 645 | done(); 646 | }, 20); 647 | }); 648 | 649 | it('"unhandledErrorHandler": Each chained promise must handle', (done) => { 650 | let unhandledErrorHandlerCalled = false; 651 | let catchBlockReached = false; 652 | 653 | const oldUnhandledErrorHandler = SyncTasks.config.unhandledErrorHandler; 654 | SyncTasks.config.unhandledErrorHandler = () => { 655 | unhandledErrorHandlerCalled = true; 656 | }; 657 | 658 | const task = SyncTasks.Rejected(); 659 | task.catch(() => { 660 | catchBlockReached = true; 661 | }); 662 | 663 | // Does not handle failure. 664 | task.then(() => { 665 | assert(false); 666 | }); 667 | 668 | setTimeout(() => { 669 | SyncTasks.config.unhandledErrorHandler = oldUnhandledErrorHandler; 670 | assert(unhandledErrorHandlerCalled); 671 | assert(catchBlockReached); 672 | done(); 673 | }, 20); 674 | }); 675 | 676 | it('"unhandledErrorHandler": "fail" never "handles" the failure', (done) => { 677 | let unhandledErrorHandlerCalled = false; 678 | let failBlockReached = false; 679 | 680 | const oldUnhandledErrorHandler = SyncTasks.config.unhandledErrorHandler; 681 | SyncTasks.config.unhandledErrorHandler = () => { 682 | unhandledErrorHandlerCalled = true; 683 | }; 684 | 685 | SyncTasks.Rejected().fail(() => { 686 | failBlockReached = true; 687 | // If this was .catch, it would resolve the promise (with undefined) and the failure would be handled. 688 | }); 689 | 690 | setTimeout(() => { 691 | SyncTasks.config.unhandledErrorHandler = oldUnhandledErrorHandler; 692 | assert(unhandledErrorHandlerCalled); 693 | assert(failBlockReached); 694 | done(); 695 | }, 20); 696 | }); 697 | 698 | it('"unhandledErrorHandler": "done" does not create another "unhandled"', (done) => { 699 | let unhandledErrorHandlerCalled = false; 700 | let catchBlockReached = false; 701 | 702 | const oldUnhandledErrorHandler = SyncTasks.config.unhandledErrorHandler; 703 | SyncTasks.config.unhandledErrorHandler = () => { 704 | unhandledErrorHandlerCalled = true; 705 | }; 706 | 707 | SyncTasks.Rejected().done(() => { 708 | // Should not create a separate "unhandled" error since there is no way to "handle" it from here. 709 | // The existing "unhandled" error should continue to be "unhandled", as other tests have verified. 710 | }).catch(() => { 711 | // "Handle" the failure. 712 | catchBlockReached = true; 713 | }); 714 | 715 | setTimeout(() => { 716 | SyncTasks.config.unhandledErrorHandler = oldUnhandledErrorHandler; 717 | assert(!unhandledErrorHandlerCalled); 718 | assert(catchBlockReached); 719 | done(); 720 | }, 20); 721 | }); 722 | 723 | it('"unhandledErrorHandler": "fail" does not create another "unhandled"', (done) => { 724 | let unhandledErrorHandlerCalled = false; 725 | let catchBlockReached = false; 726 | 727 | const oldUnhandledErrorHandler = SyncTasks.config.unhandledErrorHandler; 728 | SyncTasks.config.unhandledErrorHandler = () => { 729 | unhandledErrorHandlerCalled = true; 730 | }; 731 | 732 | SyncTasks.Rejected().fail(() => { 733 | // Should not create a separate "unhandled" error since there is no way to "handle" it from here. 734 | // The existing "unhandled" error should continue to be "unhandled", as other tests have verified. 735 | }).catch(() => { 736 | // "Handle" the failure. 737 | catchBlockReached = true; 738 | }); 739 | 740 | setTimeout(() => { 741 | SyncTasks.config.unhandledErrorHandler = oldUnhandledErrorHandler; 742 | assert(!unhandledErrorHandlerCalled); 743 | assert(catchBlockReached); 744 | done(); 745 | }, 20); 746 | }); 747 | 748 | it('Add callback while resolving', (done) => { 749 | const task = SyncTasks.Defer(); 750 | const promise = task.promise(); 751 | let resolvedCount = 0; 752 | 753 | const innerTask1 = SyncTasks.Defer(); 754 | const innerTask2 = SyncTasks.Defer(); 755 | 756 | promise.then(() => { 757 | // While resolving: add callback to same promise. 758 | promise.then(() => { 759 | innerTask2.resolve(++resolvedCount); 760 | }, err => { 761 | assert(false); 762 | }); 763 | // This line should be reached before innerTask2 resolves. 764 | innerTask1.resolve(++resolvedCount); 765 | }, err => { 766 | assert(false); 767 | }); 768 | 769 | task.resolve(1); 770 | 771 | SyncTasks.all([innerTask1.promise(), innerTask2.promise()]).then(rets => { 772 | assert(rets.length === 2); 773 | assert(rets[0] === 1); 774 | assert(rets[1] === 2); 775 | done(); 776 | }, err => { 777 | assert(false); 778 | }); 779 | }); 780 | 781 | it('Add callback while rejecting', (done) => { 782 | const task = SyncTasks.Defer(); 783 | const promise = task.promise(); 784 | let rejectedCount = 0; 785 | 786 | const innerTask1 = SyncTasks.Defer(); 787 | const innerTask2 = SyncTasks.Defer(); 788 | 789 | promise.then(() => { 790 | assert(false); 791 | }, err => { 792 | // While resolving: add callback to same promise. 793 | promise.then(() => { 794 | assert(false); 795 | }, err => { 796 | innerTask2.resolve(++rejectedCount); 797 | }); 798 | // This line should be reached before innerTask2 resolves. 799 | innerTask1.resolve(++rejectedCount); 800 | }); 801 | 802 | task.reject(1); 803 | 804 | SyncTasks.all([innerTask1.promise(), innerTask2.promise()]).then(rets => { 805 | assert(rets.length === 2); 806 | assert(rets[0] === 1); 807 | assert(rets[1] === 2); 808 | done(); 809 | }, err => { 810 | assert(false); 811 | }); 812 | }); 813 | 814 | it('Cancel task (happy path)', () => { 815 | let canceled = false; 816 | let cancelContext: any; 817 | 818 | const task = SyncTasks.Defer(); 819 | const promise = task.promise(); 820 | task.onCancel((context) => { 821 | canceled = true; 822 | cancelContext = context; 823 | task.reject(5); 824 | }); 825 | 826 | promise.cancel(4); 827 | 828 | // Check the cancel caused rejection. 829 | return promise.then(() => { 830 | assert(false); 831 | return SyncTasks.Rejected(); 832 | }, (err) => { 833 | assert.equal(err, 5); 834 | assert(canceled); 835 | assert.equal(cancelContext, 4); 836 | return SyncTasks.Resolved(1); 837 | }); 838 | }); 839 | 840 | it('Cancel chain cancels task (bubble up)', () => { 841 | let canceled = false; 842 | let cancelContext: any; 843 | 844 | const task = SyncTasks.Defer(); 845 | const promise = task.promise(); 846 | task.onCancel((context) => { 847 | canceled = true; 848 | cancelContext = context; 849 | task.reject(5); 850 | }); 851 | 852 | const chain = promise.then(() => { 853 | assert(false); 854 | return SyncTasks.Rejected(); 855 | }, (err) => { 856 | assert.equal(err, 5); 857 | assert(canceled); 858 | assert.equal(cancelContext, 4); 859 | return -1; 860 | }); 861 | 862 | chain.cancel(4); 863 | 864 | // Check the chain cancel caused task rejection. 865 | return promise.then(() => { 866 | assert(false); 867 | return SyncTasks.Rejected(); 868 | }, (err) => { 869 | assert.equal(err, 5); 870 | assert(canceled); 871 | assert.equal(cancelContext, 4); 872 | return SyncTasks.Resolved(1); 873 | }); 874 | }); 875 | 876 | it('Cancel deep chain cancels task (bubble up)', () => { 877 | let canceled = false; 878 | let cancelContext: any; 879 | 880 | const task = SyncTasks.Defer(); 881 | const promise = task.promise(); 882 | task.onCancel((context) => { 883 | canceled = true; 884 | cancelContext = context; 885 | task.reject(5); 886 | }); 887 | 888 | const chain = promise.then(() => { 889 | assert(false); 890 | return SyncTasks.Rejected(); 891 | }, (err) => { 892 | assert.equal(err, 5); 893 | assert(canceled); 894 | assert.equal(cancelContext, 4); 895 | return -1; 896 | }) 897 | // Make the chain longer to further separate the cancel from the task. 898 | .always(noop) 899 | .always(noop) 900 | .always(noop); 901 | 902 | chain.cancel(4); 903 | 904 | // Check the chain cancel caused task rejection. 905 | return promise.then(() => { 906 | assert(false); 907 | return SyncTasks.Rejected(); 908 | }, (err) => { 909 | assert.equal(err, 5); 910 | assert(canceled); 911 | assert.equal(cancelContext, 4); 912 | return SyncTasks.Resolved(1); 913 | }); 914 | }); 915 | 916 | it('Cancel finished task does not call cancellation handlers', () => { 917 | const task = SyncTasks.Defer(); 918 | const promise = task.promise(); 919 | task.onCancel((context) => { 920 | assert(false); 921 | }); 922 | 923 | task.resolve(2); 924 | promise.cancel(4); 925 | }); 926 | 927 | it('Cancel finished task does not cancel inner', () => { 928 | const task = SyncTasks.Defer(); 929 | const promise = task.promise(); 930 | task.onCancel((context) => { 931 | assert(false); 932 | }); 933 | 934 | promise.then(() => { 935 | const inner = SyncTasks.Defer(); 936 | inner.onCancel((context) => { 937 | assert(false); 938 | }); 939 | return inner.promise(); 940 | }); 941 | 942 | task.resolve(2); 943 | promise.cancel(4); 944 | }); 945 | 946 | it('Cancel task then resolve during cancellation then does not call further handlers', () => { 947 | let canceled = false; 948 | 949 | const task = SyncTasks.Defer(); 950 | const promise = task.promise(); 951 | task.onCancel(() => { 952 | canceled = true; 953 | }); 954 | task.onCancel(() => { 955 | task.resolve(2); 956 | }); 957 | task.onCancel(() => { 958 | assert(false); 959 | }); 960 | 961 | promise.cancel(); 962 | assert(canceled); 963 | 964 | // Check the onCancel caused task resolution. 965 | return promise; 966 | }); 967 | 968 | it('Cancel task then reject during cancellation then does not call further handlers', () => { 969 | let canceled = false; 970 | 971 | const task = SyncTasks.Defer(); 972 | const promise = task.promise(); 973 | task.onCancel(() => { 974 | canceled = true; 975 | }); 976 | task.onCancel(() => { 977 | task.reject(5); 978 | }); 979 | task.onCancel(() => { 980 | assert(false); 981 | }); 982 | 983 | promise.cancel(); 984 | assert(canceled); 985 | 986 | // Check the onCancel caused task rejection. 987 | return promise.then(() => { 988 | assert(false); 989 | return SyncTasks.Rejected(); 990 | }, (err) => { 991 | assert.equal(err, 5); 992 | return SyncTasks.Resolved(2); 993 | }); 994 | }); 995 | 996 | it('Cancel inner does not cancel root task (no bubble out)', () => { 997 | let canceled = false; 998 | let cancelContext: any; 999 | 1000 | const task = SyncTasks.Defer(); 1001 | const promise = task.promise(); 1002 | task.onCancel((context) => { 1003 | assert(false); 1004 | }); 1005 | 1006 | const inner = SyncTasks.Defer(); 1007 | inner.onCancel((context) => { 1008 | canceled = true; 1009 | cancelContext = context; 1010 | inner.reject(5); 1011 | }); 1012 | const innerPromise = inner.promise(); 1013 | 1014 | const chain = promise.then(() => { 1015 | return innerPromise; 1016 | }, (err) => { 1017 | assert(false); 1018 | return SyncTasks.Rejected(); 1019 | }).catch(err => { 1020 | assert.equal(err, 5); 1021 | assert(canceled); 1022 | assert.equal(cancelContext, 4); 1023 | return SyncTasks.Resolved(2); 1024 | }); 1025 | 1026 | innerPromise.cancel(4); 1027 | task.resolve(2); 1028 | return chain; 1029 | }); 1030 | 1031 | it('Cancel chain cancels inner task (bubble in), with task resolved late', () => { 1032 | let canceled = false; 1033 | let cancelContext: any; 1034 | 1035 | const task = SyncTasks.Defer(); 1036 | const promise = task.promise(); 1037 | 1038 | const chain = promise.then(() => { 1039 | const inner = SyncTasks.Defer(); 1040 | inner.onCancel((context) => { 1041 | canceled = true; 1042 | cancelContext = context; 1043 | inner.reject(5); 1044 | }); 1045 | return inner.promise(); 1046 | }, (err) => { 1047 | assert(false); 1048 | return SyncTasks.Rejected(); 1049 | }).catch(err => { 1050 | assert.equal(err, 5); 1051 | assert(canceled); 1052 | assert.equal(cancelContext, 4); 1053 | return SyncTasks.Resolved(2); 1054 | }); 1055 | 1056 | chain.cancel(4); 1057 | task.resolve(1); 1058 | return chain; 1059 | }); 1060 | 1061 | it('Cancel chain cancels inner task (bubble in), with task resolved early', () => { 1062 | let canceled = false; 1063 | let cancelContext: any; 1064 | 1065 | const task = SyncTasks.Defer(); 1066 | const promise = task.promise(); 1067 | 1068 | const chain = promise.then(() => { 1069 | const inner = SyncTasks.Defer(); 1070 | inner.onCancel((context) => { 1071 | canceled = true; 1072 | cancelContext = context; 1073 | inner.reject(5); 1074 | }); 1075 | setTimeout(() => { 1076 | chain.cancel(4); 1077 | }, waitTime); 1078 | return inner.promise(); 1079 | }, (err) => { 1080 | assert(false); 1081 | return SyncTasks.Rejected(); 1082 | }).catch(err => { 1083 | assert.equal(err, 5); 1084 | assert(canceled); 1085 | assert.equal(cancelContext, 4); 1086 | return SyncTasks.Resolved(2); 1087 | }); 1088 | 1089 | task.resolve(1); 1090 | return chain; 1091 | }); 1092 | 1093 | it('Cancel chain cancels inner chained task, with task resolved late', () => { 1094 | let canceled = false; 1095 | let cancelContext: any; 1096 | 1097 | const task = SyncTasks.Defer(); 1098 | const promise = task.promise(); 1099 | 1100 | const chain = promise.then(() => { 1101 | const inner = SyncTasks.Defer(); 1102 | inner.onCancel((context) => { 1103 | canceled = true; 1104 | cancelContext = context; 1105 | inner.reject(5); 1106 | }); 1107 | return inner.promise().then(() => { 1108 | // Chain another promise in place to make sure it works its way up to inner at some point. 1109 | return 6; 1110 | }); 1111 | }, (err) => { 1112 | assert(false); 1113 | return SyncTasks.Rejected(); 1114 | }).catch(err => { 1115 | assert.equal(err, 5); 1116 | assert(canceled); 1117 | assert.equal(cancelContext, 4); 1118 | return SyncTasks.Resolved(2); 1119 | }); 1120 | 1121 | chain.cancel(4); 1122 | task.resolve(1); 1123 | return chain; 1124 | }); 1125 | 1126 | it('Cancel chain cancels inner chained task, with task resolved early', () => { 1127 | let canceled = false; 1128 | let cancelContext: any; 1129 | 1130 | const task = SyncTasks.Defer(); 1131 | const promise = task.promise(); 1132 | 1133 | const ret = promise.then(() => { 1134 | const newTask = SyncTasks.Defer(); 1135 | newTask.onCancel((context) => { 1136 | canceled = true; 1137 | cancelContext = context; 1138 | newTask.reject(5); 1139 | }); 1140 | setTimeout(() => { 1141 | ret.cancel(4); 1142 | }, waitTime); 1143 | return newTask.promise().then(() => { 1144 | // Chain another promise in place to make sure it works its way up to the newTask at some point. 1145 | return 6; 1146 | }); 1147 | }, (err) => { 1148 | assert(false); 1149 | return SyncTasks.Rejected(); 1150 | }).catch(err => { 1151 | assert.equal(err, 5); 1152 | assert(canceled); 1153 | assert.equal(cancelContext, 4); 1154 | return SyncTasks.Resolved(2); 1155 | }); 1156 | 1157 | task.resolve(1); 1158 | return ret; 1159 | }); 1160 | 1161 | it('Cancel .all task cancels array of tasks', () => { 1162 | let canceled1 = false; 1163 | let canceled2 = false; 1164 | 1165 | const task1 = SyncTasks.Defer(); 1166 | const promise1 = task1.promise(); 1167 | task1.onCancel((context) => { 1168 | canceled1 = true; 1169 | assert.equal(context, 4); 1170 | }); 1171 | 1172 | const task2 = SyncTasks.Defer(); 1173 | const promise2 = task2.promise(); 1174 | task2.onCancel((context) => { 1175 | canceled2 = true; 1176 | assert.equal(context, 4); 1177 | }); 1178 | 1179 | const allPromise = SyncTasks.all([promise1, promise2]); 1180 | allPromise.cancel(4); 1181 | assert(canceled1); 1182 | assert(canceled2); 1183 | }); 1184 | 1185 | it('Cancel chain from .all cancels array of tasks', () => { 1186 | let canceled1 = false; 1187 | 1188 | const task1 = SyncTasks.Defer(); 1189 | const promise1 = task1.promise(); 1190 | task1.onCancel((context) => { 1191 | canceled1 = true; 1192 | assert.equal(context, 4); 1193 | }); 1194 | 1195 | const chain = SyncTasks.all([promise1]) 1196 | .always(noop); 1197 | 1198 | chain.cancel(4); 1199 | assert(canceled1); 1200 | }); 1201 | 1202 | it('Cancel .race task cancels array of tasks', () => { 1203 | let canceled1 = false; 1204 | let canceled2 = false; 1205 | 1206 | const task1 = SyncTasks.Defer(); 1207 | const promise1 = task1.promise(); 1208 | task1.onCancel((context) => { 1209 | canceled1 = true; 1210 | assert.equal(context, 4); 1211 | }); 1212 | 1213 | const task2 = SyncTasks.Defer(); 1214 | const promise2 = task2.promise(); 1215 | task2.onCancel((context) => { 1216 | canceled2 = true; 1217 | assert.equal(context, 4); 1218 | }); 1219 | 1220 | const allPromise = SyncTasks.race([promise1, promise2]); 1221 | allPromise.cancel(4); 1222 | assert(canceled1); 1223 | assert(canceled2); 1224 | }); 1225 | 1226 | it('Cancel chain from .race cancels array of task', () => { 1227 | let canceled1 = false; 1228 | 1229 | const task1 = SyncTasks.Defer(); 1230 | const promise1 = task1.promise(); 1231 | task1.onCancel((context) => { 1232 | canceled1 = true; 1233 | assert.equal(context, 4); 1234 | }); 1235 | 1236 | const chain = SyncTasks.race([promise1]) 1237 | .always(noop); 1238 | 1239 | chain.cancel(4); 1240 | assert(canceled1); 1241 | }); 1242 | 1243 | it('Cancel chain cancels inner chained .all task, with task resolved late', () => { 1244 | let canceled = false; 1245 | let cancelContext: any; 1246 | 1247 | const task = SyncTasks.Defer(); 1248 | const promise = task.promise(); 1249 | 1250 | const ret = promise.then(() => { 1251 | const newTask = SyncTasks.Defer(); 1252 | newTask.onCancel((context) => { 1253 | canceled = true; 1254 | cancelContext = context; 1255 | newTask.reject(5); 1256 | }); 1257 | return SyncTasks.all([newTask.promise()]).then(() => { 1258 | // Chain another promise in place to make sure it works its way up to the newTask at some point. 1259 | return 6; 1260 | }); 1261 | }, (err) => { 1262 | assert(false); 1263 | return SyncTasks.Rejected(); 1264 | }).catch(err => { 1265 | assert.equal(err, 5); 1266 | assert(canceled); 1267 | assert.equal(cancelContext, 4); 1268 | return SyncTasks.Resolved(2); 1269 | }); 1270 | 1271 | ret.cancel(4); 1272 | task.resolve(1); 1273 | return ret; 1274 | }); 1275 | 1276 | it('Cancel chain cancels inner chained .all task, with task resolved early', () => { 1277 | let canceled = false; 1278 | let cancelContext: any; 1279 | 1280 | const task = SyncTasks.Defer(); 1281 | const promise = task.promise(); 1282 | 1283 | const ret = promise.then(() => { 1284 | const newTask = SyncTasks.Defer(); 1285 | newTask.onCancel((context) => { 1286 | canceled = true; 1287 | cancelContext = context; 1288 | newTask.reject(5); 1289 | }); 1290 | setTimeout(() => { 1291 | ret.cancel(4); 1292 | }, waitTime); 1293 | return SyncTasks.all([newTask.promise()]).then(() => { 1294 | // Chain another promise in place to make sure it works its way up to the newTask at some point. 1295 | return 6; 1296 | }); 1297 | }, (err) => { 1298 | assert(false); 1299 | return SyncTasks.Rejected(); 1300 | }).catch(err => { 1301 | assert.equal(err, 5); 1302 | assert(canceled); 1303 | assert.equal(cancelContext, 4); 1304 | return SyncTasks.Resolved(2); 1305 | }); 1306 | 1307 | task.resolve(1); 1308 | return ret; 1309 | }); 1310 | 1311 | it('Cancel shared task does not cancel children (no bubble down)', () => { 1312 | const task = SyncTasks.Defer(); 1313 | const promise = task.promise(); 1314 | 1315 | const inner1 = SyncTasks.Defer(); 1316 | inner1.onCancel((context) => { 1317 | assert(false); 1318 | }); 1319 | 1320 | const inner2 = SyncTasks.Defer(); 1321 | inner2.onCancel((context) => { 1322 | assert(false); 1323 | }); 1324 | 1325 | promise.then(() => inner1.promise()); 1326 | promise.then(() => inner2.promise()); 1327 | 1328 | promise.cancel(4); 1329 | task.resolve(1); 1330 | }); 1331 | 1332 | it('Cancel chain of shared task does not cancel other chain (no bubble across)', () => { 1333 | let canceled = false; 1334 | let cancelContext: any; 1335 | 1336 | const task = SyncTasks.Defer(); 1337 | const promise = task.promise(); 1338 | 1339 | const inner1 = SyncTasks.Defer(); 1340 | inner1.onCancel((context) => { 1341 | canceled = true; 1342 | cancelContext = context; 1343 | }); 1344 | 1345 | const inner2 = SyncTasks.Defer(); 1346 | inner2.onCancel((context) => { 1347 | assert(false); 1348 | }); 1349 | 1350 | const chain1 = promise.then(() => inner1.promise()); 1351 | promise.then(() => inner2.promise()); 1352 | 1353 | chain1.cancel(4); 1354 | task.resolve(1); 1355 | assert(canceled); 1356 | assert.equal(cancelContext, 4); 1357 | }); 1358 | 1359 | it('Cancel throws for "double cancel"', () => { 1360 | const oldCatchExceptions = SyncTasks.config.catchExceptions; 1361 | SyncTasks.config.catchExceptions = false; 1362 | 1363 | const promise = SyncTasks.Defer().promise(); 1364 | promise.cancel(); 1365 | try { 1366 | promise.cancel(); 1367 | assert.ok(false); 1368 | } catch (e) { 1369 | // Expected. 1370 | SyncTasks.config.catchExceptions = oldCatchExceptions; 1371 | } 1372 | }); 1373 | 1374 | it('Cancel does not throw for "double cancel" due to bubble up', () => { 1375 | const oldCatchExceptions = SyncTasks.config.catchExceptions; 1376 | SyncTasks.config.catchExceptions = false; 1377 | 1378 | let countCancels = 0; 1379 | const task = SyncTasks.Defer(); 1380 | task.onCancel(context => { 1381 | countCancels++; 1382 | }); 1383 | 1384 | const root = task.promise(); 1385 | const promise1 = root.then(noop); 1386 | const promise2 = root.then(noop); 1387 | 1388 | try { 1389 | promise1.cancel(); 1390 | promise2.cancel(); 1391 | SyncTasks.config.catchExceptions = oldCatchExceptions; 1392 | } catch (e) { 1393 | assert.ok(false); 1394 | } 1395 | 1396 | // Make sure the root's cancel was called, but we should not be called back more than once. 1397 | assert.equal(countCancels, 1); 1398 | }); 1399 | 1400 | it('Cancel does not throw for "double cancel" due to bubble in', () => { 1401 | const oldCatchExceptions = SyncTasks.config.catchExceptions; 1402 | SyncTasks.config.catchExceptions = false; 1403 | 1404 | const task = SyncTasks.Defer(); 1405 | const promise = task.promise(); 1406 | 1407 | const chain = promise.then(() => { 1408 | const inner = SyncTasks.Defer(); 1409 | const innerPromise = inner.promise(); 1410 | innerPromise.cancel(); 1411 | return innerPromise; 1412 | }); 1413 | 1414 | try { 1415 | chain.cancel(32); 1416 | task.resolve(1); 1417 | SyncTasks.config.catchExceptions = oldCatchExceptions; 1418 | } catch (e) { 1419 | assert.ok(false); 1420 | } 1421 | }); 1422 | 1423 | it('Cancel .all does not throw for "double cancel" due to bubble up', () => { 1424 | const oldCatchExceptions = SyncTasks.config.catchExceptions; 1425 | SyncTasks.config.catchExceptions = false; 1426 | 1427 | const promise1 = SyncTasks.Defer().promise(); 1428 | const promise2 = SyncTasks.Defer().promise(); 1429 | const sink = SyncTasks.all([promise1, promise2]); 1430 | 1431 | try { 1432 | sink.cancel(); 1433 | promise1.cancel(); 1434 | SyncTasks.config.catchExceptions = oldCatchExceptions; 1435 | } catch (e) { 1436 | assert.ok(false); 1437 | } 1438 | }); 1439 | 1440 | it('Cancel .all does not throw for "double cancel" due to bubble up for already canceled promise', () => { 1441 | const oldCatchExceptions = SyncTasks.config.catchExceptions; 1442 | SyncTasks.config.catchExceptions = false; 1443 | 1444 | const promise1 = SyncTasks.Defer().promise(); 1445 | const promise2 = SyncTasks.Defer().promise(); 1446 | const sink = SyncTasks.all([promise1, promise2]); 1447 | 1448 | try { 1449 | promise1.cancel(); 1450 | sink.cancel(); 1451 | SyncTasks.config.catchExceptions = oldCatchExceptions; 1452 | } catch (e) { 1453 | assert.ok(false); 1454 | } 1455 | }); 1456 | 1457 | it('Cancel .race does not throw for "double cancel" due to bubble up', () => { 1458 | const oldCatchExceptions = SyncTasks.config.catchExceptions; 1459 | SyncTasks.config.catchExceptions = false; 1460 | 1461 | const promise1 = SyncTasks.Defer().promise(); 1462 | const promise2 = SyncTasks.Defer().promise(); 1463 | const sink = SyncTasks.race([promise1, promise2]); 1464 | 1465 | try { 1466 | sink.cancel(); 1467 | promise1.cancel(); 1468 | SyncTasks.config.catchExceptions = oldCatchExceptions; 1469 | } catch (e) { 1470 | assert.ok(false); 1471 | } 1472 | }); 1473 | 1474 | it('Cancel .race does not throw for "double cancel" due to bubble up for already canceled promise', () => { 1475 | const oldCatchExceptions = SyncTasks.config.catchExceptions; 1476 | SyncTasks.config.catchExceptions = false; 1477 | 1478 | const promise1 = SyncTasks.Defer().promise(); 1479 | const promise2 = SyncTasks.Defer().promise(); 1480 | const sink = SyncTasks.race([promise1, promise2]); 1481 | 1482 | try { 1483 | promise1.cancel(); 1484 | sink.cancel(); 1485 | SyncTasks.config.catchExceptions = oldCatchExceptions; 1486 | } catch (e) { 1487 | assert.ok(false); 1488 | } 1489 | }); 1490 | 1491 | it('Cancel resolved promise does not call cancellation handlers', () => { 1492 | const defer = SyncTasks.Defer(); 1493 | const promise = defer.promise(); 1494 | 1495 | defer.onCancel(() => { 1496 | assert(false, 'Handler should not be called'); 1497 | }); 1498 | 1499 | defer.resolve(void 0); 1500 | promise.cancel(); 1501 | }); 1502 | 1503 | it('Multiple bubble promise cancellation results in single cancel handler callbacks', () => { 1504 | const defer = SyncTasks.Defer(); 1505 | const promise1 = defer.promise().then(() => { /* noop */}); 1506 | const promise2 = defer.promise().then(() => { /* noop */}); 1507 | let callbackCount = 0; 1508 | 1509 | defer.onCancel(() => { 1510 | callbackCount++; 1511 | }); 1512 | 1513 | promise1.cancel(); 1514 | promise2.cancel(); 1515 | 1516 | assert.equal(callbackCount, 1, 'onCancel handler not called correct number of times'); 1517 | }); 1518 | 1519 | it('deferCallback', (done) => { 1520 | let got = false; 1521 | let got2 = false; 1522 | SyncTasks.asyncCallback(() => { 1523 | got = true; 1524 | }); 1525 | setTimeout(() => { 1526 | assert(got); 1527 | assert(got2); 1528 | done(); 1529 | }, 1); 1530 | SyncTasks.asyncCallback(() => { 1531 | got2 = true; 1532 | }); 1533 | assert(!got); 1534 | assert(!got2); 1535 | }); 1536 | 1537 | it('thenDeferred Simple', (done) => { 1538 | const task = SyncTasks.Defer(); 1539 | 1540 | let tooEarly = true; 1541 | task.promise().then(val => { 1542 | assert.equal(val, 1); 1543 | return 2; 1544 | }, err => { 1545 | assert(false); 1546 | return null; 1547 | }).thenAsync(val => { 1548 | assert.equal(val, 2); 1549 | assert(!tooEarly); 1550 | done(); 1551 | }, err => { 1552 | assert(false); 1553 | }); 1554 | 1555 | SyncTasks.asyncCallback(() => { 1556 | tooEarly = false; 1557 | }); 1558 | task.resolve(1); 1559 | 1560 | assert(tooEarly); 1561 | }); 1562 | 1563 | it('thenDeferred Failure', (done) => { 1564 | const task = SyncTasks.Defer(); 1565 | 1566 | let tooEarly = true; 1567 | task.promise().then(val => { 1568 | assert.equal(val, 1); 1569 | return SyncTasks.Rejected(4); 1570 | }, err => { 1571 | assert(false); 1572 | return 5; 1573 | }).thenAsync(val => { 1574 | assert(false); 1575 | }, err => { 1576 | assert.equal(err, 4); 1577 | assert(!tooEarly); 1578 | done(); 1579 | }); 1580 | 1581 | SyncTasks.asyncCallback(() => { 1582 | tooEarly = false; 1583 | }); 1584 | task.resolve(1); 1585 | 1586 | assert(tooEarly); 1587 | }); 1588 | 1589 | it('toEs6Promise Simple', (done) => { 1590 | const task = SyncTasks.Defer(); 1591 | let tooEarly = true; 1592 | 1593 | task.promise().toEs6Promise().then(val => { 1594 | assert.equal(val, 3.50); 1595 | done(); 1596 | }, err => { 1597 | assert(false); 1598 | }); 1599 | 1600 | SyncTasks.asyncCallback(() => { 1601 | tooEarly = false; 1602 | }); 1603 | task.resolve(3.50); 1604 | 1605 | assert(tooEarly); 1606 | }); 1607 | 1608 | it('toEs6Promise Resolved', (done) => { 1609 | const resolved = SyncTasks.Resolved(42); 1610 | let tooEarly = true; 1611 | 1612 | resolved.toEs6Promise().then(val => { 1613 | assert.equal(val, 42); 1614 | assert(tooEarly); 1615 | done(); 1616 | }, err => { 1617 | assert(false); 1618 | }); 1619 | 1620 | SyncTasks.asyncCallback(() => { 1621 | tooEarly = false; 1622 | }); 1623 | assert(tooEarly); 1624 | }); 1625 | 1626 | it('toEs6Promise Rejected', (done) => { 1627 | const rejected = SyncTasks.Rejected(42); 1628 | let tooEarly = true; 1629 | 1630 | rejected.toEs6Promise().then(val => { 1631 | assert(false); 1632 | }, err => { 1633 | assert.equal(err, 42); 1634 | assert(tooEarly); 1635 | done(); 1636 | }); 1637 | 1638 | SyncTasks.asyncCallback(() => { 1639 | tooEarly = false; 1640 | }); 1641 | assert(tooEarly); 1642 | }); 1643 | 1644 | it('toEs6Promise and back', (done) => { 1645 | const task = SyncTasks.Defer(); 1646 | const stPromise = task.promise(); 1647 | const esPromise = stPromise.toEs6Promise(); 1648 | const stPromiseAgain = SyncTasks.fromThenable(esPromise); 1649 | 1650 | stPromiseAgain.then(val => { 1651 | assert.equal(val, 100500); 1652 | done(); 1653 | }, err => { 1654 | assert(false); 1655 | }); 1656 | 1657 | task.resolve(100500); 1658 | }); 1659 | }); 1660 | -------------------------------------------------------------------------------- /test/support/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "dist-test" 6 | }, 7 | 8 | "include": [ 9 | "../../test/*" 10 | ] 11 | } -------------------------------------------------------------------------------- /test/support/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ROOT_PATH = path.resolve(__dirname, '..', '..'); 3 | const TEST_PATH = path.resolve(ROOT_PATH, 'test'); 4 | 5 | module.exports = { 6 | devtool: 'cheap-eval-source-map', 7 | entry: path.resolve(TEST_PATH, 'SyncTasksTests.ts'), 8 | 9 | output: { 10 | filename: './SyncTasksTestsPack.js', 11 | path: path.resolve(ROOT_PATH, 'dist-test'), 12 | }, 13 | 14 | resolve: { 15 | extensions: ['.ts', '.js'] 16 | }, 17 | 18 | module: { 19 | rules: [{ 20 | test: /\.ts?$/, 21 | loader: 'awesome-typescript-loader', 22 | exclude: /node_modules/, 23 | options: { 24 | configFileName: path.resolve(TEST_PATH, 'support', 'tsconfig.json'), 25 | } 26 | }] 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "src/**/*", 5 | "test/**/*" 6 | ] 7 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "forceConsistentCasingInFileNames": true, 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": true, 6 | "noImplicitThis": true, 7 | "noImplicitAny": true, 8 | "declaration": true, 9 | "noResolve": false, 10 | "strict": true, 11 | "module": "commonjs", 12 | "target": "es5", 13 | "outDir": "dist", 14 | "lib": ["es5", "dom", "es2015.promise", "es2015"] 15 | }, 16 | 17 | "include": [ 18 | "src/**/*" 19 | ] 20 | } --------------------------------------------------------------------------------