OLD | NEW |
---|---|
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 Loading... | |
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 Loading... | |
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 Loading... | |
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 Loading... | |
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 } |
OLD | NEW |