Chromium Code Reviews| Index: chrome/browser/resources/google_now/utility.js |
| diff --git a/chrome/browser/resources/google_now/utility.js b/chrome/browser/resources/google_now/utility.js |
| index f2236209fc498ffa15c0b2cc10c89d254e5a8c84..5a0042a9a7b3eda55253b6ad49d6a744b96e615a 100644 |
| --- a/chrome/browser/resources/google_now/utility.js |
| +++ b/chrome/browser/resources/google_now/utility.js |
| @@ -219,6 +219,17 @@ var instrumented = {}; |
| var WrapperPlugin; |
| /** |
| + * Promise Metadata. Holds fields that are used in identifying a promise during |
| + * task processing. |
| + * |
| + * @typedef {{ |
| + * id: number, |
| + * isCatch: boolean |
| + * }} |
| + */ |
| +var PromiseMetadata; |
| + |
| +/** |
| * Wrapper for callbacks. Used to add error handling and other services to |
| * callbacks for HTML and Chrome functions and events. |
| */ |
| @@ -233,13 +244,14 @@ var wrapper = (function() { |
| * An instance of WrapperPlugin can have state that can be shared by its |
| * constructor, prologue() and epilogue(). Also WrapperPlugins can change |
| * state of other objects, for example, to do refcounting. |
| - * @type {?function(): WrapperPlugin} |
| + * @type {?function(PromiseMetadata): WrapperPlugin} |
| */ |
| var wrapperPluginFactory = null; |
| /** |
| * Registers a wrapper plugin factory. |
| - * @param {function(): WrapperPlugin} factory Wrapper plugin factory. |
| + * @param {function(PromiseMetadata): WrapperPlugin} factory |
| + * Wrapper plugin factory. |
| */ |
| function registerWrapperPluginFactory(factory) { |
| if (wrapperPluginFactory) { |
| @@ -267,6 +279,18 @@ var wrapper = (function() { |
| var pendingCallbacks = {}; |
| /** |
| + * A map of promise IDs to an array of "then" callback IDs. |
| + * @type {Object.<number, array.<number>>} |
| + */ |
| + var pendingThenCallbacks = {}; |
| + |
| + /** |
| + * A map of promise IDs to an array of "catch" callback IDs. |
| + * @type {Object.<number, array.<number>>} |
| + */ |
| + var pendingCatchCallbacks = {}; |
| + |
| + /** |
| * Unique ID of the next callback. |
| * @type {number} |
| */ |
| @@ -296,19 +320,29 @@ var wrapper = (function() { |
| * @param {Function} callback Callback to instrument. |
| * @param {boolean=} opt_isEventListener True if the callback is a listener to |
| * a Chrome API event. |
| + * @param {PromiseMetadata=} opt_promiseMetadata Set if wrapped from a |
| + * promise. |
| * @return {Function} Instrumented callback. |
| */ |
| - function wrapCallback(callback, opt_isEventListener) { |
| + function wrapCallback(callback, opt_isEventListener, opt_promiseMetadata) { |
| var callbackId = nextCallbackId++; |
| if (!opt_isEventListener) { |
| checkInWrappedCallback(); |
| pendingCallbacks[callbackId] = new Error().stack + ' @' + Date.now(); |
| + if (opt_promiseMetadata) { |
| + var callbackIdMap = opt_promiseMetadata.isCatch ? |
| + pendingCatchCallbacks : |
| + pendingThenCallbacks; |
| + callbackIdMap[opt_promiseMetadata.id] = |
| + callbackIdMap[opt_promiseMetadata.id] || []; |
| + callbackIdMap[opt_promiseMetadata.id].push(callbackId); |
| + } |
| } |
| - |
| // wrapperPluginFactory may be null before task manager is built, and in |
| // tests. |
| - var wrapperPluginInstance = wrapperPluginFactory && wrapperPluginFactory(); |
| + var wrapperPluginInstance = |
| + wrapperPluginFactory && wrapperPluginFactory(opt_promiseMetadata); |
| return function() { |
| // This is the wrapper for the callback. |
| @@ -316,8 +350,19 @@ var wrapper = (function() { |
| verify(!isInWrappedCallback, 'Re-entering instrumented callback'); |
| isInWrappedCallback = true; |
| - if (!opt_isEventListener) |
| + if (!opt_isEventListener) { |
| delete pendingCallbacks[callbackId]; |
| + if (opt_promiseMetadata) { |
| + var callbackIdsToRemove = opt_promiseMetadata.isCatch ? |
| + pendingThenCallbacks[opt_promiseMetadata.id] : |
| + pendingCatchCallbacks[opt_promiseMetadata.id]; |
| + if (callbackIdsToRemove !== undefined) { |
| + callbackIdsToRemove.forEach(function(idToRemove) { |
| + delete pendingCallbacks[idToRemove]; |
| + }); |
| + } |
| + } |
| + } |
| if (wrapperPluginInstance) |
| wrapperPluginInstance.prologue(); |
| @@ -437,13 +482,31 @@ wrapper.instrumentChromeApiFunction('identity.removeCachedAuthToken', 1); |
| wrapper.instrumentChromeApiFunction('webstorePrivate.getBrowserLogin', 0); |
| /** |
| + * Unique ID of the next promise. |
| + * @type {number} |
| + */ |
| +var nextPromiseId = 0; |
| + |
| +/** |
| * Add task tracking support to Promise.then. |
| * @override |
| */ |
| Promise.prototype.then = function() { |
| var originalThen = Promise.prototype.then; |
| return function(callback) { |
| - return originalThen.call(this, wrapper.wrapCallback(callback, false)); |
| + var promiseId = this.__promiseId; |
| + if (promiseId === undefined) { |
| + promiseId = nextPromiseId; |
| + nextPromiseId++; |
| + } |
| + // "then" may return a new promise, getting rid of the ID! |
| + // Set it before and after the call to keep the value. |
| + this.__promiseId = promiseId; |
|
rgustafson
2014/02/24 21:55:12
Still kinda confused in general by setting the sam
robliao
2014/02/24 22:31:13
You're right in the general case (and we'll need t
robliao
2014/02/24 23:11:56
Added a comment about this limitation.
On 2014/02/
|
| + var promise = originalThen.call( |
| + this, wrapper.wrapCallback(callback, false, |
| + {id: this.__promiseId, isCatch: false})); |
| + promise.__promiseId = promiseId; |
| + return promise; |
| } |
| }(); |
| @@ -454,11 +517,34 @@ Promise.prototype.then = function() { |
| Promise.prototype.catch = function() { |
| var originalCatch = Promise.prototype.catch; |
| return function(callback) { |
| - return originalCatch.call(this, wrapper.wrapCallback(callback, false)); |
| + var promiseId = this.__promiseId; |
| + if (promiseId === undefined) { |
| + promiseId = nextPromiseId; |
| + nextPromiseId++; |
| + } |
| + // "catch" may return a new promise, getting rid of the ID! |
| + // Set it before and after the call to keep the value. |
| + this.__promiseId = promiseId; |
| + var promise = originalCatch.call( |
| + this, wrapper.wrapCallback(callback, false, |
| + {id: this.__promiseId, isCatch: true})); |
| + promise.__promiseId = promiseId; |
| + return promise; |
| } |
| }(); |
| /** |
| + * Promise Pending Callback Data. Counts outstanding "then" and "catch" |
| + * callbacks; |
| + * |
| + * @typedef {{ |
| + * thenCount: number, |
| + * catchCount: number |
| + * }} |
| + */ |
| +var PromisePendingCallbackData; |
| + |
| +/** |
| * Builds the object to manage tasks (mutually exclusive chains of events). |
| * @param {function(string, string): boolean} areConflicting Function that |
| * checks if a new task can't be added to a task queue that contains an |
| @@ -480,6 +566,13 @@ function buildTaskManager(areConflicting) { |
| var taskPendingCallbackCount = 0; |
| /** |
| + * Map of Promise ID to PromisePendingCallbackData to count the number of |
| + * outstanding "then" and "catch" callbacks. |
| + * @type {object.<number, PromisePendingCallbackData>} |
| + */ |
| + var taskPromisePendingCallbackData = {}; |
| + |
| + /** |
| * True if currently executed code is a part of a task. |
| * @type {boolean} |
| */ |
| @@ -573,11 +666,28 @@ function buildTaskManager(areConflicting) { |
| /** |
| * Wrapper plugin for tasks. |
| * @constructor |
| + * @param {PromiseMetadata=} opt_promiseMetadata Set if wrapped from a |
| + * promise. |
| */ |
| - function TasksWrapperPlugin() { |
| + function TasksWrapperPlugin(opt_promiseMetadata) { |
| this.isTaskCallback = isInTask; |
| - if (this.isTaskCallback) |
| + if (this.isTaskCallback) { |
| ++taskPendingCallbackCount; |
| + if (opt_promiseMetadata !== undefined) { |
| + this.promiseId = opt_promiseMetadata.id; |
| + if (!taskPromisePendingCallbackData[this.promiseId]) { |
| + taskPromisePendingCallbackData[this.promiseId] = |
| + {thenCount: 0, catchCount: 0}; |
| + } |
| + if (opt_promiseMetadata.isCatch) { |
| + taskPromisePendingCallbackData[this.promiseId].catchCount++; |
| + this.isCatch = true; |
| + } else { |
| + taskPromisePendingCallbackData[this.promiseId].thenCount++; |
| + this.isCatch = false; |
| + } |
| + } |
| + } |
| } |
| TasksWrapperPlugin.prototype = { |
| @@ -598,15 +708,30 @@ function buildTaskManager(areConflicting) { |
| if (this.isTaskCallback) { |
| verify(isInTask, 'TasksWrapperPlugin.epilogue: not in task at exit'); |
| isInTask = false; |
| + if (this.promiseId !== undefined) { |
| + // Finishing up a promise callback. If a "then" calls back, then |
| + // all "catch" callbacks will not call back and vice versa. |
| + // Subtract the callbacks that will not call back. |
| + if (this.isCatch) { |
| + taskPendingCallbackCount -= |
| + taskPromisePendingCallbackData[this.promiseId].thenCount; |
| + taskPromisePendingCallbackData[this.promiseId].thenCount = 0; |
| + } else { |
| + taskPendingCallbackCount -= |
| + taskPromisePendingCallbackData[this.promiseId].catchCount; |
| + taskPromisePendingCallbackData[this.promiseId].catchCount = 0; |
| + } |
| + } |
| if (--taskPendingCallbackCount == 0) |
| finish(); |
| } |
| } |
| }; |
| - wrapper.registerWrapperPluginFactory(function() { |
| - return new TasksWrapperPlugin(); |
| - }); |
| + wrapper.registerWrapperPluginFactory( |
| + function(opt_promiseMetadata) { |
| + return new TasksWrapperPlugin(opt_promiseMetadata); |
| + }); |
| return { |
| add: add |