You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							269 lines
						
					
					
						
							7.5 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							269 lines
						
					
					
						
							7.5 KiB
						
					
					
				| /** | |
|  * @license | |
|  * Copyright 2016 Google Inc. All Rights Reserved. | |
|  * | |
|  * Licensed under the Apache License, Version 2.0 (the "License"); | |
|  * you may not use this file except in compliance with the License. | |
|  * You may obtain a copy of the License at | |
|  * | |
|  *     http://www.apache.org/licenses/LICENSE-2.0 | |
|  * | |
|  * Unless required by applicable law or agreed to in writing, software | |
|  * distributed under the License is distributed on an "AS IS" BASIS, | |
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
|  * See the License for the specific language governing permissions and | |
|  * limitations under the License. | |
|  */ | |
| 
 | |
| // This is a utility class that makes it easier to work with asynchronous tasks. | |
| // Here's why I don't just use Promises: | |
| // (a) I don't want to depend on a Promise implementation. | |
| // (b) Promises aren't cancellable (yet?), and I want cancellability. | |
| // | |
| // This is very stripped down, compared to Promises. | |
| // (a) You can only call .thenDo() once. Because there's only one party waiting | |
| //     on the result of a task, cancelling always propagates backwards. | |
| // (b) The argument to .thenDo() must return either undefined or a Task. I don't | |
| //     promote values to Tasks, like what happens with Promises. | |
|  | |
| var Task = exports; | |
| 
 | |
| /** | |
|  * Creates a Task. | |
|  * | |
|  * The `doSomething` function is called immediately, so that it can start | |
|  * whatever work is part of this task. | |
|  * | |
|  * The `doSomething` function is given a resolve function and a reject function, | |
|  * and it should call one of them when the task is finished, to report its | |
|  * result. | |
|  * | |
|  * The `doSomething` function can optionally return a cancel function. This will | |
|  * be called if the task is cancelled. | |
|  * | |
|  * @param  {function(function(T), function(?)): function()} doSomething | |
|  * @return {Task<T>} | |
|  * @template T | |
|  */ | |
| Task.start = function(doSomething) { | |
|   var me = {}; | |
| 
 | |
|   // onFinish should be called as soon as both finished and onFinish are | |
|   // defined. It should be called by the piece of code that just defined either | |
|   // finished or onFinish. | |
|   var finished; | |
|   var onFinish; | |
|   var cleaners = []; | |
| 
 | |
|   function finish(err, result) { | |
|     if (!finished) { | |
|       finished = {err: err, result: result}; | |
| 
 | |
|       if (onFinish) { | |
|         onFinish(); | |
|         // To prevent memory leaks, delete our reference to onFinish after | |
|         // calling it. | |
|         onFinish = function() {}; | |
|       } | |
| 
 | |
|       var cleanup; | |
|       while (cleanup = cleaners.pop()) { | |
|         cleanup(); | |
|       } | |
| 
 | |
|       if (err === 'cancelled') { | |
|         if (abort) abort(); | |
|       } | |
| 
 | |
|       abort = null; | |
|     } | |
|   } | |
| 
 | |
|   try { | |
|     // doSomething must be called immediately. | |
|     var abort = doSomething( | |
|         function(result) { finish(null, result); }, | |
|         function(err)    { finish(err,  null);   }); | |
|   } catch (err) { | |
|     finish(err, null); | |
|   } | |
| 
 | |
|   /** | |
|    * Cancels the task (unless the task has already finished, in which case | |
|    * this call is ignored). | |
|    * | |
|    * Subsequent tasks created with #thenDo will not be started. However, clean- | |
|    * up code added with #finished will run. | |
|    */ | |
|   me.cancel = function() { | |
|     finish('cancelled', null); | |
|   }; | |
| 
 | |
|   /** | |
|    * Sets the listener that will be called with the result of this task, when | |
|    * finished. This function can be called at most once. | |
|    * | |
|    * @param {function(?, T)} callback | |
|    */ | |
|   function setListener(callback) { | |
|     if (onFinish) { | |
|       throw new Error('thenDo/finally called more than once'); | |
|     } | |
|     if (finished) { | |
|       onFinish = function() {}; | |
|       callback(finished.err, finished.result); | |
|     } else { | |
|       onFinish = function() { | |
|         callback(finished.err, finished.result); | |
|       }; | |
|     } | |
|   } | |
| 
 | |
|   /** | |
|    * Creates and returns a composite task, consisting of this task and a | |
|    * subsequent task. | |
|    * | |
|    * @param {function(T): ?Task<U>} onResolve A function that will | |
|    *     create a subsequent task. This function will be called | |
|    *     asynchronously, with the result of this task, when it | |
|    *     finishes. The return value must be a Task, or null/undefined. | |
|    * @param {function(?): ?Task<U>} onReject A function that will | |
|    *     create a subsequent task. This function will be called | |
|    *     asynchronously, with the error produced by this task, when it | |
|    *     finishes. The return value must be a Task, or null/undefined. | |
|    * @return {Task<U>} The composite task. Cancelling the composite task cancels | |
|    *     either this task or the subsequent task, depending on whether this | |
|    *     task is finished. | |
|    * @template U | |
|    */ | |
|   me.thenDo = function(onResolve, onReject) { | |
|     return compose(me, setListener, onResolve, onReject); | |
|   }; | |
| 
 | |
|   /** | |
|    * Registers a cleanup function, that will be run when the task finishes, | |
|    * regardless of error or cancellation. | |
|    * | |
|    * @param {function()} cleanup | |
|    * @return {THIS} | |
|    */ | |
|   me.finally = function(cleanup) { | |
|     if (!finished) { | |
|       cleaners.push(function() { | |
|         process.nextTick(cleanup); | |
|       }); | |
|     } else { | |
|       process.nextTick(cleanup); | |
|     } | |
|     return me; | |
|   }; | |
| 
 | |
|   return me; | |
| }; | |
| 
 | |
| /** | |
|  * Creates a Task with the given result. | |
|  */ | |
| Task.withValue = function(result) { | |
|   return Task.start(function(resolve) { | |
|     resolve(result); | |
|   }); | |
| }; | |
| 
 | |
| /** | |
|  * Creates a Task with the given error. | |
|  */ | |
| Task.withError = function(err) { | |
|   return Task.start(function(resolve, reject) { | |
|     reject(err); | |
|   }); | |
| }; | |
| 
 | |
| /** | |
|  * Returns a new task that races the given tasks. Eventually finishes with the | |
|  * result or error of whichever task finishes first. If any task is cancelled, | |
|  * all of the tasks are cancelled. | |
|  * | |
|  * @param {Array<Task<T>>} tasks | |
|  * @return {Task<T>} | |
|  * @template T | |
|  */ | |
| Task.race = function(tasks) { | |
|   return Task.start(function(resolve, reject) { | |
|     function cancelAll() { | |
|       tasks.forEach(function(task) { | |
|         task.cancel(); | |
|       }); | |
|     } | |
|     tasks.forEach(function(task) { | |
|       task.finally(cancelAll).thenDo(resolve, reject); | |
|     }); | |
|     return cancelAll; | |
|   }); | |
| }; | |
| 
 | |
| /** | |
|  * Creates a composite task, which uses the output of the first task to create | |
|  * a subsequent task, and represents the two tasks together. | |
|  * | |
|  * This function is internal-only. It is used by Task.thenDo(). | |
|  * | |
|  * @param {Task<T>} firstTask | |
|  * @param {function(function(?, T))} whenFirstTaskFinishes The private | |
|  *     setListener method on the firstTask. | |
|  * @param {function(T): Task<U>} onResolve | |
|  * @param {function(?): Task<U>} onReject | |
|  * @return {Task<U>} | |
|  * @template T, U | |
|  */ | |
| function compose(firstTask, whenFirstTaskFinishes, onResolve, onReject) { | |
|   return Task.start(function(resolve, reject) { | |
|     var cancelled; | |
|     var currentTask = firstTask; | |
| 
 | |
|     whenFirstTaskFinishes(function(err, result) { | |
|       currentTask = null; | |
|       // createSubsequentTask must be called asynchronously. | |
|       process.nextTick(function() { | |
|         if (cancelled || err === 'cancelled') { | |
|           return reject('cancelled'); | |
|         } | |
| 
 | |
|         // Start the subsequent task. | |
|         if (err == null) { | |
|           if (!onResolve) { | |
|             return resolve(result); | |
|           } | |
|           try { | |
|             currentTask = onResolve(result); | |
|           } catch (caughtErr) { | |
|             return reject(caughtErr); | |
|           } | |
|         } else { | |
|           if (!onReject) { | |
|             return reject(err); | |
|           } | |
|           try { | |
|             currentTask = onReject(err); | |
|           } catch (caughtErr) { | |
|             return reject(caughtErr); | |
|           } | |
|         } | |
| 
 | |
|         // Was a subsequent task returned? | |
|         if (!currentTask) { | |
|           return resolve(undefined); | |
|         } | |
| 
 | |
|         currentTask.thenDo(resolve, reject); | |
|       }); | |
|     }); | |
| 
 | |
|     return function cancelCompositeTask() { | |
|       cancelled = true; | |
|       if (currentTask) { | |
|         currentTask.cancel(); | |
|       } | |
|     }; | |
|   }); | |
| }
 |