Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(317)

Side by Side Diff: chrome/browser/resources/google_now/utility.js

Issue 174053003: Task Management - Handle Promises "Then" or "Catch" callback case. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Bug Fix Created 6 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | chrome/browser/resources/google_now/utility_unittest.gtestjs » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 'use strict'; 5 'use strict';
6 6
7 /** 7 /**
8 * @fileoverview Utility objects and functions for Google Now extension. 8 * @fileoverview Utility objects and functions for Google Now extension.
9 * Most important entities here: 9 * Most important entities here:
10 * (1) 'wrapper' is a module used to add error handling and other services to 10 * (1) 'wrapper' is a module used to add error handling and other services to
(...skipping 201 matching lines...) Expand 10 before | Expand all | Expand 10 after
212 * to the original callback provided by the extension. 212 * to the original callback provided by the extension.
213 * 213 *
214 * @typedef {{ 214 * @typedef {{
215 * prologue: function (), 215 * prologue: function (),
216 * epilogue: function () 216 * epilogue: function ()
217 * }} 217 * }}
218 */ 218 */
219 var WrapperPlugin; 219 var WrapperPlugin;
220 220
221 /** 221 /**
222 * Promise Metadata. Holds fields that are used in identifying a promise during
223 * task processing.
224 *
225 * @typedef {{
226 * id: number,
227 * isCatch: boolean
228 * }}
229 */
230 var PromiseMetadata;
231
232 /**
222 * Wrapper for callbacks. Used to add error handling and other services to 233 * Wrapper for callbacks. Used to add error handling and other services to
223 * callbacks for HTML and Chrome functions and events. 234 * callbacks for HTML and Chrome functions and events.
224 */ 235 */
225 var wrapper = (function() { 236 var wrapper = (function() {
226 /** 237 /**
227 * Factory for wrapper plugins. If specified, it's used to generate an 238 * Factory for wrapper plugins. If specified, it's used to generate an
228 * instance of WrapperPlugin each time we wrap a callback (which corresponds 239 * instance of WrapperPlugin each time we wrap a callback (which corresponds
229 * to addListener call for Chrome events, and to every API call that specifies 240 * to addListener call for Chrome events, and to every API call that specifies
230 * a callback). WrapperPlugin's lifetime ends when the callback for which it 241 * a callback). WrapperPlugin's lifetime ends when the callback for which it
231 * was generated, exits. It's possible to have several instances of 242 * was generated, exits. It's possible to have several instances of
232 * WrapperPlugin at the same time. 243 * WrapperPlugin at the same time.
233 * An instance of WrapperPlugin can have state that can be shared by its 244 * An instance of WrapperPlugin can have state that can be shared by its
234 * constructor, prologue() and epilogue(). Also WrapperPlugins can change 245 * constructor, prologue() and epilogue(). Also WrapperPlugins can change
235 * state of other objects, for example, to do refcounting. 246 * state of other objects, for example, to do refcounting.
236 * @type {?function(): WrapperPlugin} 247 * @type {?function(PromiseMetadata): WrapperPlugin}
237 */ 248 */
238 var wrapperPluginFactory = null; 249 var wrapperPluginFactory = null;
239 250
240 /** 251 /**
241 * Registers a wrapper plugin factory. 252 * Registers a wrapper plugin factory.
242 * @param {function(): WrapperPlugin} factory Wrapper plugin factory. 253 * @param {function(PromiseMetadata): WrapperPlugin} factory
254 * Wrapper plugin factory.
243 */ 255 */
244 function registerWrapperPluginFactory(factory) { 256 function registerWrapperPluginFactory(factory) {
245 if (wrapperPluginFactory) { 257 if (wrapperPluginFactory) {
246 reportError(buildErrorWithMessageForServer( 258 reportError(buildErrorWithMessageForServer(
247 'registerWrapperPluginFactory: factory is already registered.')); 259 'registerWrapperPluginFactory: factory is already registered.'));
248 } 260 }
249 261
250 wrapperPluginFactory = factory; 262 wrapperPluginFactory = factory;
251 } 263 }
252 264
253 /** 265 /**
254 * True if currently executed code runs in a callback or event handler that 266 * True if currently executed code runs in a callback or event handler that
255 * was instrumented by wrapper.wrapCallback() call. 267 * was instrumented by wrapper.wrapCallback() call.
256 * @type {boolean} 268 * @type {boolean}
257 */ 269 */
258 var isInWrappedCallback = false; 270 var isInWrappedCallback = false;
259 271
260 /** 272 /**
261 * Required callbacks that are not yet called. Includes both task and non-task 273 * Required callbacks that are not yet called. Includes both task and non-task
262 * callbacks. This is a map from unique callback id to the stack at the moment 274 * callbacks. This is a map from unique callback id to the stack at the moment
263 * when the callback was wrapped. This stack identifies the callback. 275 * when the callback was wrapped. This stack identifies the callback.
264 * Used only for diagnostics. 276 * Used only for diagnostics.
265 * @type {Object.<number, string>} 277 * @type {Object.<number, string>}
266 */ 278 */
267 var pendingCallbacks = {}; 279 var pendingCallbacks = {};
268 280
269 /** 281 /**
282 * A map of promise IDs to an array of "then" callback IDs.
283 * @type {Object.<number, array.<number>>}
284 */
285 var pendingThenCallbacks = {};
286
287 /**
288 * A map of promise IDs to an array of "catch" callback IDs.
289 * @type {Object.<number, array.<number>>}
290 */
291 var pendingCatchCallbacks = {};
292
293 /**
270 * Unique ID of the next callback. 294 * Unique ID of the next callback.
271 * @type {number} 295 * @type {number}
272 */ 296 */
273 var nextCallbackId = 0; 297 var nextCallbackId = 0;
274 298
275 /** 299 /**
276 * Gets diagnostic string with the status of the wrapper. 300 * Gets diagnostic string with the status of the wrapper.
277 * @return {string} Diagnostic string. 301 * @return {string} Diagnostic string.
278 */ 302 */
279 function debugGetStateString() { 303 function debugGetStateString() {
280 return 'pendingCallbacks @' + Date.now() + ' = ' + 304 return 'pendingCallbacks @' + Date.now() + ' = ' +
281 JSON.stringify(pendingCallbacks); 305 JSON.stringify(pendingCallbacks);
282 } 306 }
283 307
284 /** 308 /**
285 * Checks that we run in a wrapped callback. 309 * Checks that we run in a wrapped callback.
286 */ 310 */
287 function checkInWrappedCallback() { 311 function checkInWrappedCallback() {
288 if (!isInWrappedCallback) { 312 if (!isInWrappedCallback) {
289 reportError(buildErrorWithMessageForServer( 313 reportError(buildErrorWithMessageForServer(
290 'Not in instrumented callback')); 314 'Not in instrumented callback'));
291 } 315 }
292 } 316 }
293 317
294 /** 318 /**
295 * Adds error processing to an API callback. 319 * Adds error processing to an API callback.
296 * @param {Function} callback Callback to instrument. 320 * @param {Function} callback Callback to instrument.
297 * @param {boolean=} opt_isEventListener True if the callback is a listener to 321 * @param {boolean=} opt_isEventListener True if the callback is a listener to
298 * a Chrome API event. 322 * a Chrome API event.
323 * @param {PromiseMetadata=} opt_promiseMetadata Set if wrapped from a
324 * promise.
299 * @return {Function} Instrumented callback. 325 * @return {Function} Instrumented callback.
300 */ 326 */
301 function wrapCallback(callback, opt_isEventListener) { 327 function wrapCallback(callback, opt_isEventListener, opt_promiseMetadata) {
302 var callbackId = nextCallbackId++; 328 var callbackId = nextCallbackId++;
303 329
304 if (!opt_isEventListener) { 330 if (!opt_isEventListener) {
305 checkInWrappedCallback(); 331 checkInWrappedCallback();
306 pendingCallbacks[callbackId] = new Error().stack + ' @' + Date.now(); 332 pendingCallbacks[callbackId] = new Error().stack + ' @' + Date.now();
333 if (opt_promiseMetadata) {
334 var callbackIdMap = opt_promiseMetadata.isCatch ?
335 pendingCatchCallbacks :
336 pendingThenCallbacks;
337 callbackIdMap[opt_promiseMetadata.id] =
338 callbackIdMap[opt_promiseMetadata.id] || [];
339 callbackIdMap[opt_promiseMetadata.id].push(callbackId);
340 }
307 } 341 }
308
309 // wrapperPluginFactory may be null before task manager is built, and in 342 // wrapperPluginFactory may be null before task manager is built, and in
310 // tests. 343 // tests.
311 var wrapperPluginInstance = wrapperPluginFactory && wrapperPluginFactory(); 344 var wrapperPluginInstance =
345 wrapperPluginFactory && wrapperPluginFactory(opt_promiseMetadata);
312 346
313 return function() { 347 return function() {
314 // This is the wrapper for the callback. 348 // This is the wrapper for the callback.
315 try { 349 try {
316 verify(!isInWrappedCallback, 'Re-entering instrumented callback'); 350 verify(!isInWrappedCallback, 'Re-entering instrumented callback');
317 isInWrappedCallback = true; 351 isInWrappedCallback = true;
318 352
319 if (!opt_isEventListener) 353 if (!opt_isEventListener) {
320 delete pendingCallbacks[callbackId]; 354 delete pendingCallbacks[callbackId];
355 if (opt_promiseMetadata) {
356 var callbackIdsToRemove = opt_promiseMetadata.isCatch ?
357 pendingThenCallbacks[opt_promiseMetadata.id] :
358 pendingCatchCallbacks[opt_promiseMetadata.id];
359 if (callbackIdsToRemove !== undefined) {
360 callbackIdsToRemove.forEach(function(idToRemove) {
361 delete pendingCallbacks[idToRemove];
362 });
363 }
364 }
365 }
321 366
322 if (wrapperPluginInstance) 367 if (wrapperPluginInstance)
323 wrapperPluginInstance.prologue(); 368 wrapperPluginInstance.prologue();
324 369
325 // Call the original callback. 370 // Call the original callback.
326 callback.apply(null, arguments); 371 callback.apply(null, arguments);
327 372
328 if (wrapperPluginInstance) 373 if (wrapperPluginInstance)
329 wrapperPluginInstance.epilogue(); 374 wrapperPluginInstance.epilogue();
330 375
(...skipping 99 matching lines...) Expand 10 before | Expand all | Expand 10 after
430 })(); 475 })();
431 476
432 wrapper.instrumentChromeApiFunction('alarms.get', 1); 477 wrapper.instrumentChromeApiFunction('alarms.get', 1);
433 wrapper.instrumentChromeApiFunction('alarms.onAlarm.addListener', 0); 478 wrapper.instrumentChromeApiFunction('alarms.onAlarm.addListener', 0);
434 wrapper.instrumentChromeApiFunction('identity.getAuthToken', 1); 479 wrapper.instrumentChromeApiFunction('identity.getAuthToken', 1);
435 wrapper.instrumentChromeApiFunction('identity.onSignInChanged.addListener', 0); 480 wrapper.instrumentChromeApiFunction('identity.onSignInChanged.addListener', 0);
436 wrapper.instrumentChromeApiFunction('identity.removeCachedAuthToken', 1); 481 wrapper.instrumentChromeApiFunction('identity.removeCachedAuthToken', 1);
437 wrapper.instrumentChromeApiFunction('webstorePrivate.getBrowserLogin', 0); 482 wrapper.instrumentChromeApiFunction('webstorePrivate.getBrowserLogin', 0);
438 483
439 /** 484 /**
485 * Unique ID of the next promise.
486 * @type {number}
487 */
488 var nextPromiseId = 0;
489
490 /**
440 * Add task tracking support to Promise.then. 491 * Add task tracking support to Promise.then.
441 * @override 492 * @override
442 */ 493 */
443 Promise.prototype.then = function() { 494 Promise.prototype.then = function() {
444 var originalThen = Promise.prototype.then; 495 var originalThen = Promise.prototype.then;
445 return function(callback) { 496 return function(callback) {
446 return originalThen.call(this, wrapper.wrapCallback(callback, false)); 497 var promiseId = this.__promiseId;
498 if (promiseId === undefined) {
499 promiseId = nextPromiseId;
500 nextPromiseId++;
501 }
502 // "then" may return a new promise, getting rid of the ID!
503 // Set it before and after the call to keep the value.
504 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/
505 var promise = originalThen.call(
506 this, wrapper.wrapCallback(callback, false,
507 {id: this.__promiseId, isCatch: false}));
508 promise.__promiseId = promiseId;
509 return promise;
447 } 510 }
448 }(); 511 }();
449 512
450 /** 513 /**
451 * Add task tracking support to Promise.catch. 514 * Add task tracking support to Promise.catch.
452 * @override 515 * @override
453 */ 516 */
454 Promise.prototype.catch = function() { 517 Promise.prototype.catch = function() {
455 var originalCatch = Promise.prototype.catch; 518 var originalCatch = Promise.prototype.catch;
456 return function(callback) { 519 return function(callback) {
457 return originalCatch.call(this, wrapper.wrapCallback(callback, false)); 520 var promiseId = this.__promiseId;
521 if (promiseId === undefined) {
522 promiseId = nextPromiseId;
523 nextPromiseId++;
524 }
525 // "catch" may return a new promise, getting rid of the ID!
526 // Set it before and after the call to keep the value.
527 this.__promiseId = promiseId;
528 var promise = originalCatch.call(
529 this, wrapper.wrapCallback(callback, false,
530 {id: this.__promiseId, isCatch: true}));
531 promise.__promiseId = promiseId;
532 return promise;
458 } 533 }
459 }(); 534 }();
460 535
461 /** 536 /**
537 * Promise Pending Callback Data. Counts outstanding "then" and "catch"
538 * callbacks;
539 *
540 * @typedef {{
541 * thenCount: number,
542 * catchCount: number
543 * }}
544 */
545 var PromisePendingCallbackData;
546
547 /**
462 * Builds the object to manage tasks (mutually exclusive chains of events). 548 * Builds the object to manage tasks (mutually exclusive chains of events).
463 * @param {function(string, string): boolean} areConflicting Function that 549 * @param {function(string, string): boolean} areConflicting Function that
464 * checks if a new task can't be added to a task queue that contains an 550 * checks if a new task can't be added to a task queue that contains an
465 * existing task. 551 * existing task.
466 * @return {Object} Task manager interface. 552 * @return {Object} Task manager interface.
467 */ 553 */
468 function buildTaskManager(areConflicting) { 554 function buildTaskManager(areConflicting) {
469 /** 555 /**
470 * Queue of scheduled tasks. The first element, if present, corresponds to the 556 * Queue of scheduled tasks. The first element, if present, corresponds to the
471 * currently running task. 557 * currently running task.
472 * @type {Array.<Object.<string, function()>>} 558 * @type {Array.<Object.<string, function()>>}
473 */ 559 */
474 var queue = []; 560 var queue = [];
475 561
476 /** 562 /**
477 * Count of unfinished callbacks of the current task. 563 * Count of unfinished callbacks of the current task.
478 * @type {number} 564 * @type {number}
479 */ 565 */
480 var taskPendingCallbackCount = 0; 566 var taskPendingCallbackCount = 0;
481 567
482 /** 568 /**
569 * Map of Promise ID to PromisePendingCallbackData to count the number of
570 * outstanding "then" and "catch" callbacks.
571 * @type {object.<number, PromisePendingCallbackData>}
572 */
573 var taskPromisePendingCallbackData = {};
574
575 /**
483 * True if currently executed code is a part of a task. 576 * True if currently executed code is a part of a task.
484 * @type {boolean} 577 * @type {boolean}
485 */ 578 */
486 var isInTask = false; 579 var isInTask = false;
487 580
488 /** 581 /**
489 * Starts the first queued task. 582 * Starts the first queued task.
490 */ 583 */
491 function startFirst() { 584 function startFirst() {
492 verify(queue.length >= 1, 'startFirst: queue is empty'); 585 verify(queue.length >= 1, 'startFirst: queue is empty');
(...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after
566 queue.length == 0, 659 queue.length == 0,
567 'Incomplete task when unloading event page,' + 660 'Incomplete task when unloading event page,' +
568 ' queue = ' + JSON.stringify(queue) + ', ' + 661 ' queue = ' + JSON.stringify(queue) + ', ' +
569 wrapper.debugGetStateString()); 662 wrapper.debugGetStateString());
570 }); 663 });
571 664
572 665
573 /** 666 /**
574 * Wrapper plugin for tasks. 667 * Wrapper plugin for tasks.
575 * @constructor 668 * @constructor
669 * @param {PromiseMetadata=} opt_promiseMetadata Set if wrapped from a
670 * promise.
576 */ 671 */
577 function TasksWrapperPlugin() { 672 function TasksWrapperPlugin(opt_promiseMetadata) {
578 this.isTaskCallback = isInTask; 673 this.isTaskCallback = isInTask;
579 if (this.isTaskCallback) 674 if (this.isTaskCallback) {
580 ++taskPendingCallbackCount; 675 ++taskPendingCallbackCount;
676 if (opt_promiseMetadata !== undefined) {
677 this.promiseId = opt_promiseMetadata.id;
678 if (!taskPromisePendingCallbackData[this.promiseId]) {
679 taskPromisePendingCallbackData[this.promiseId] =
680 {thenCount: 0, catchCount: 0};
681 }
682 if (opt_promiseMetadata.isCatch) {
683 taskPromisePendingCallbackData[this.promiseId].catchCount++;
684 this.isCatch = true;
685 } else {
686 taskPromisePendingCallbackData[this.promiseId].thenCount++;
687 this.isCatch = false;
688 }
689 }
690 }
581 } 691 }
582 692
583 TasksWrapperPlugin.prototype = { 693 TasksWrapperPlugin.prototype = {
584 /** 694 /**
585 * Plugin code to be executed before invoking the original callback. 695 * Plugin code to be executed before invoking the original callback.
586 */ 696 */
587 prologue: function() { 697 prologue: function() {
588 if (this.isTaskCallback) { 698 if (this.isTaskCallback) {
589 verify(!isInTask, 'TasksWrapperPlugin.prologue: already in task'); 699 verify(!isInTask, 'TasksWrapperPlugin.prologue: already in task');
590 isInTask = true; 700 isInTask = true;
591 } 701 }
592 }, 702 },
593 703
594 /** 704 /**
595 * Plugin code to be executed after invoking the original callback. 705 * Plugin code to be executed after invoking the original callback.
596 */ 706 */
597 epilogue: function() { 707 epilogue: function() {
598 if (this.isTaskCallback) { 708 if (this.isTaskCallback) {
599 verify(isInTask, 'TasksWrapperPlugin.epilogue: not in task at exit'); 709 verify(isInTask, 'TasksWrapperPlugin.epilogue: not in task at exit');
600 isInTask = false; 710 isInTask = false;
711 if (this.promiseId !== undefined) {
712 // Finishing up a promise callback. If a "then" calls back, then
713 // all "catch" callbacks will not call back and vice versa.
714 // Subtract the callbacks that will not call back.
715 if (this.isCatch) {
716 taskPendingCallbackCount -=
717 taskPromisePendingCallbackData[this.promiseId].thenCount;
718 taskPromisePendingCallbackData[this.promiseId].thenCount = 0;
719 } else {
720 taskPendingCallbackCount -=
721 taskPromisePendingCallbackData[this.promiseId].catchCount;
722 taskPromisePendingCallbackData[this.promiseId].catchCount = 0;
723 }
724 }
601 if (--taskPendingCallbackCount == 0) 725 if (--taskPendingCallbackCount == 0)
602 finish(); 726 finish();
603 } 727 }
604 } 728 }
605 }; 729 };
606 730
607 wrapper.registerWrapperPluginFactory(function() { 731 wrapper.registerWrapperPluginFactory(
608 return new TasksWrapperPlugin(); 732 function(opt_promiseMetadata) {
609 }); 733 return new TasksWrapperPlugin(opt_promiseMetadata);
734 });
610 735
611 return { 736 return {
612 add: add 737 add: add
613 }; 738 };
614 } 739 }
615 740
616 /** 741 /**
617 * Builds an object to manage retrying activities with exponential backoff. 742 * Builds an object to manage retrying activities with exponential backoff.
618 * @param {string} name Name of this attempt manager. 743 * @param {string} name Name of this attempt manager.
619 * @param {function()} attempt Activity that the manager retries until it 744 * @param {function()} attempt Activity that the manager retries until it
(...skipping 203 matching lines...) Expand 10 before | Expand all | Expand 10 after
823 // One hour is just an arbitrary amount of time chosen. 948 // One hour is just an arbitrary amount of time chosen.
824 chrome.alarms.create(alarmName, {periodInMinutes: 60}); 949 chrome.alarms.create(alarmName, {periodInMinutes: 60});
825 950
826 return { 951 return {
827 addListener: addListener, 952 addListener: addListener,
828 getAuthToken: getAuthToken, 953 getAuthToken: getAuthToken,
829 isSignedIn: isSignedIn, 954 isSignedIn: isSignedIn,
830 removeToken: removeToken 955 removeToken: removeToken
831 }; 956 };
832 } 957 }
OLDNEW
« no previous file with comments | « no previous file | chrome/browser/resources/google_now/utility_unittest.gtestjs » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698