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 419 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
430 })(); | 430 })(); |
431 | 431 |
432 wrapper.instrumentChromeApiFunction('alarms.get', 1); | 432 wrapper.instrumentChromeApiFunction('alarms.get', 1); |
433 wrapper.instrumentChromeApiFunction('alarms.onAlarm.addListener', 0); | 433 wrapper.instrumentChromeApiFunction('alarms.onAlarm.addListener', 0); |
434 wrapper.instrumentChromeApiFunction('identity.getAuthToken', 1); | 434 wrapper.instrumentChromeApiFunction('identity.getAuthToken', 1); |
435 wrapper.instrumentChromeApiFunction('identity.onSignInChanged.addListener', 0); | 435 wrapper.instrumentChromeApiFunction('identity.onSignInChanged.addListener', 0); |
436 wrapper.instrumentChromeApiFunction('identity.removeCachedAuthToken', 1); | 436 wrapper.instrumentChromeApiFunction('identity.removeCachedAuthToken', 1); |
437 wrapper.instrumentChromeApiFunction('webstorePrivate.getBrowserLogin', 0); | 437 wrapper.instrumentChromeApiFunction('webstorePrivate.getBrowserLogin', 0); |
438 | 438 |
439 /** | 439 /** |
440 * Add task tracking support to Promises. | 440 * Promise adapter for all JS promises to the task manager. |
441 * @override | |
442 */ | 441 */ |
443 Promise.prototype.then = function() { | 442 function registerPromiseAdapter() { |
444 var originalThen = Promise.prototype.then; | 443 var originalThen = Promise.prototype.then; |
445 return function(callback) { | 444 var originalCatch = Promise.prototype.catch; |
446 originalThen.call(this, wrapper.wrapCallback(callback, false)); | 445 |
| 446 /** |
| 447 * Takes a promise and adds the callback tracker to it. |
| 448 * @param {object} promise Promise that receives the callback tracker. |
| 449 */ |
| 450 function instrumentPromise(promise) { |
| 451 if (promise.__tracker === undefined) { |
| 452 promise.__tracker = createPromiseCallbackTracker(promise); |
| 453 } |
447 } | 454 } |
448 }(); | 455 |
| 456 Promise.prototype.then = function(onResolved, onRejected) { |
| 457 instrumentPromise(this); |
| 458 return this.__tracker.handleThen(onResolved, onRejected); |
| 459 } |
| 460 |
| 461 Promise.prototype.catch = function(onRejected) { |
| 462 instrumentPromise(this); |
| 463 return this.__tracker.handleCatch(onRejected); |
| 464 } |
| 465 |
| 466 /** |
| 467 * Promise Callback Tracker. |
| 468 * Handles coordination of 'then' and 'catch' callbacks in a task |
| 469 * manager compatible way. For an individual promise, either the 'then' |
| 470 * arguments or the 'catch' arguments will be processed, never both. |
| 471 * |
| 472 * Example: |
| 473 * var p = new Promise([Function]); |
| 474 * p.then([ThenA]); |
| 475 * p.then([ThenB]); |
| 476 * p.catch([CatchA]); |
| 477 * On resolution, [ThenA] and [ThenB] will be used. [CatchA] is discarded. |
| 478 * On rejection, vice versa. |
| 479 * |
| 480 * Clarification: |
| 481 * Chained promises create a new promise that is tracked separately from |
| 482 * the originaing promise, as the example below demonstrates: |
| 483 * |
| 484 * var p = new Promise([Function])); |
| 485 * p.then([ThenA]).then([ThenB]).catch([CatchA]); |
| 486 * ^ ^ ^ |
| 487 * | | + Returns a new promise. |
| 488 * | + Returns a new promise. |
| 489 * + Returns a new promise. |
| 490 * |
| 491 * Four promises exist in the above statement, each with its own |
| 492 * resolution and rejection state. However, by default, this state is |
| 493 * chained to the previous promise's resolution or rejection |
| 494 * state. |
| 495 * |
| 496 * If p resolves, then the 'then' calls will execute until all the 'then' |
| 497 * clauses are executed. If the result of either [ThenA] or [ThenB] is a |
| 498 * promise, then that execution state will guide the remaining chain. |
| 499 * Similarly, if [CatchA] returns a promise, it can also guide the |
| 500 * remaining chain. In this specific case, the chain ends, so there |
| 501 * is nothing left to do. |
| 502 * @param {object} promise Promise being tracked. |
| 503 * @return {object} A promise callback tracker. |
| 504 */ |
| 505 function createPromiseCallbackTracker(promise) { |
| 506 /** |
| 507 * Callback Tracker. Holds an array of callbacks created for this promise. |
| 508 * The indirection allows quick checks against the array and clearing the |
| 509 * array without ugly splicing and copying. |
| 510 * @typedef {{ |
| 511 * callback: array.<Function>= |
| 512 * }} |
| 513 */ |
| 514 var CallbackTracker; |
| 515 |
| 516 /** @type {CallbackTracker} */ |
| 517 var thenTracker = {callbacks: []}; |
| 518 /** @type {CallbackTracker} */ |
| 519 var catchTracker = {callbacks: []}; |
| 520 |
| 521 /** |
| 522 * Returns true if the specified value is callable. |
| 523 * @param {*} value Value to check. |
| 524 * @return {boolean} True if the value is a callable. |
| 525 */ |
| 526 function isCallable(value) { |
| 527 return typeof value === 'function'; |
| 528 } |
| 529 |
| 530 /** |
| 531 * Takes a tracker and clears its callbacks in a manner consistent with |
| 532 * the task manager. For the task manager, it also calls all callbacks |
| 533 * by no-oping them first and then calling them. |
| 534 * @param {CallbackTracker} tracker Tracker to clear. |
| 535 */ |
| 536 function clearTracker(tracker) { |
| 537 if (tracker.callbacks) { |
| 538 var callbacksToClear = tracker.callbacks; |
| 539 // No-ops all callbacks of this type. |
| 540 tracker.callbacks = undefined; |
| 541 // Do not wrap the promise then argument! |
| 542 // It will call wrapped callbacks. |
| 543 originalThen.call(Promise.resolve(), function() { |
| 544 for (var i = 0; i < callbacksToClear.length; i++) { |
| 545 callbacksToClear[i](); |
| 546 } |
| 547 }); |
| 548 } |
| 549 } |
| 550 |
| 551 /** |
| 552 * Takes the argument to a 'then' or 'catch' function and applies |
| 553 * a wrapping to callables consistent to ECMA promises. |
| 554 * @param {*} maybeCallback Argument to 'then' or 'catch'. |
| 555 * @param {CallbackTracker} sameTracker Tracker for the call type. |
| 556 * Example: If the argument is from a 'then' call, use thenTracker. |
| 557 * @param {CallbackTracker} otherTracker Tracker for the opposing call type. |
| 558 * Example: If the argument is from a 'then' call, use catchTracker. |
| 559 * @return {*} Consumable argument with necessary wrapping applied. |
| 560 */ |
| 561 function registerAndWrapMaybeCallback( |
| 562 maybeCallback, sameTracker, otherTracker) { |
| 563 // If sameTracker.callbacks is undefined, we've reached an ending state |
| 564 // that means this callback will never be called back. |
| 565 // We will still forward this call on to let the promise system |
| 566 // handle further processing, but since this promise is in an ending state |
| 567 // we can be confident it will never be called back. |
| 568 if (isCallable(maybeCallback) && sameTracker.callbacks) { |
| 569 var handler = wrapper.wrapCallback(function() { |
| 570 if (sameTracker.callbacks) { |
| 571 clearTracker(otherTracker); |
| 572 maybeCallback.apply(null, arguments); |
| 573 } |
| 574 }, false); |
| 575 sameTracker.callbacks.push(handler); |
| 576 return handler; |
| 577 } else { |
| 578 return maybeCallback; |
| 579 } |
| 580 } |
| 581 |
| 582 /** |
| 583 * Tracks then calls equivalent to Promise.prototype.then. |
| 584 * @param {*} onResolved Argument to use if the promise is resolved. |
| 585 * @param {*} onRejected Argument to use if the promise is rejected. |
| 586 * @return {object} Promise resulting from the 'then' call. |
| 587 */ |
| 588 function handleThen(onResolved, onRejected) { |
| 589 var resolutionHandler = |
| 590 registerAndWrapMaybeCallback(onResolved, thenTracker, catchTracker); |
| 591 var rejectionHandler = |
| 592 registerAndWrapMaybeCallback(onRejected, catchTracker, thenTracker); |
| 593 return originalThen.call(promise, resolutionHandler, rejectionHandler); |
| 594 } |
| 595 |
| 596 /** |
| 597 * Tracks then calls equivalent to Promise.prototype.catch. |
| 598 * @param {*} onRejected Argument to use if the promise is rejected. |
| 599 * @return {object} Promise resulting from the 'catch' call. |
| 600 */ |
| 601 function handleCatch(onRejected) { |
| 602 var rejectionHandler = |
| 603 registerAndWrapMaybeCallback(onRejected, catchTracker, thenTracker); |
| 604 return originalCatch.call(promise, rejectionHandler); |
| 605 } |
| 606 |
| 607 // Seeds this promise with at least one 'then' and 'catch' so we always |
| 608 // receive a callback to update the task manager on the state of callbacks. |
| 609 handleThen(function() {}); |
| 610 handleCatch(function() {}); |
| 611 |
| 612 return { |
| 613 handleThen: handleThen, |
| 614 handleCatch: handleCatch |
| 615 }; |
| 616 } |
| 617 } |
| 618 |
| 619 registerPromiseAdapter(); |
449 | 620 |
450 /** | 621 /** |
451 * Builds the object to manage tasks (mutually exclusive chains of events). | 622 * Builds the object to manage tasks (mutually exclusive chains of events). |
452 * @param {function(string, string): boolean} areConflicting Function that | 623 * @param {function(string, string): boolean} areConflicting Function that |
453 * checks if a new task can't be added to a task queue that contains an | 624 * checks if a new task can't be added to a task queue that contains an |
454 * existing task. | 625 * existing task. |
455 * @return {Object} Task manager interface. | 626 * @return {Object} Task manager interface. |
456 */ | 627 */ |
457 function buildTaskManager(areConflicting) { | 628 function buildTaskManager(areConflicting) { |
458 /** | 629 /** |
(...skipping 260 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
719 /** | 890 /** |
720 * Wraps chrome.identity to provide limited listening support for | 891 * Wraps chrome.identity to provide limited listening support for |
721 * the sign in state by polling periodically for the auth token. | 892 * the sign in state by polling periodically for the auth token. |
722 * @return {Object} The Authentication Manager interface. | 893 * @return {Object} The Authentication Manager interface. |
723 */ | 894 */ |
724 function buildAuthenticationManager() { | 895 function buildAuthenticationManager() { |
725 var alarmName = 'sign-in-alarm'; | 896 var alarmName = 'sign-in-alarm'; |
726 | 897 |
727 /** | 898 /** |
728 * Gets an OAuth2 access token. | 899 * Gets an OAuth2 access token. |
729 * @param {function(string=)} callback Called on completion. | 900 * @return {Promise} A promise to get the authentication token. If there is |
730 * The string contains the token. It's undefined if there was an error. | 901 * no token, the request is rejected. |
731 */ | 902 */ |
732 function getAuthToken(callback) { | 903 function getAuthToken() { |
733 instrumented.identity.getAuthToken({interactive: false}, function(token) { | 904 return new Promise(function(resolve, reject) { |
734 token = chrome.runtime.lastError ? undefined : token; | 905 instrumented.identity.getAuthToken({interactive: false}, function(token) { |
735 callback(token); | 906 if (chrome.runtime.lastError || !token) { |
| 907 reject(); |
| 908 } else { |
| 909 resolve(token); |
| 910 } |
| 911 }); |
736 }); | 912 }); |
737 } | 913 } |
738 | 914 |
739 /** | 915 /** |
740 * Determines whether there is an account attached to the profile. | 916 * Determines whether there is an account attached to the profile. |
741 * @param {function(boolean)} callback Called on completion. | 917 * @return {Promise} A promise to determine if there is an account attached |
| 918 * to the profile. |
742 */ | 919 */ |
743 function isSignedIn(callback) { | 920 function isSignedIn() { |
744 instrumented.webstorePrivate.getBrowserLogin(function(accountInfo) { | 921 return new Promise(function(resolve) { |
745 callback(!!accountInfo.login); | 922 instrumented.webstorePrivate.getBrowserLogin(function(accountInfo) { |
| 923 resolve(!!accountInfo.login); |
| 924 }); |
746 }); | 925 }); |
747 } | 926 } |
748 | 927 |
749 /** | 928 /** |
750 * Removes the specified cached token. | 929 * Removes the specified cached token. |
751 * @param {string} token Authentication Token to remove from the cache. | 930 * @param {string} token Authentication Token to remove from the cache. |
752 * @param {function()} callback Called on completion. | 931 * @return {Promise} A promise that resolves on completion. |
753 */ | 932 */ |
754 function removeToken(token, callback) { | 933 function removeToken(token) { |
755 instrumented.identity.removeCachedAuthToken({token: token}, function() { | 934 return new Promise(function(resolve) { |
756 // Let Chrome now about a possible problem with the token. | 935 instrumented.identity.removeCachedAuthToken({token: token}, function() { |
757 getAuthToken(function() {}); | 936 // Let Chrome know about a possible problem with the token. |
758 callback(); | 937 getAuthToken(); |
| 938 resolve(); |
| 939 }); |
759 }); | 940 }); |
760 } | 941 } |
761 | 942 |
762 var listeners = []; | 943 var listeners = []; |
763 | 944 |
764 /** | 945 /** |
765 * Registers a listener that gets called back when the signed in state | 946 * Registers a listener that gets called back when the signed in state |
766 * is found to be changed. | 947 * is found to be changed. |
767 * @param {function()} callback Called when the answer to isSignedIn changes. | 948 * @param {function()} callback Called when the answer to isSignedIn changes. |
768 */ | 949 */ |
769 function addListener(callback) { | 950 function addListener(callback) { |
770 listeners.push(callback); | 951 listeners.push(callback); |
771 } | 952 } |
772 | 953 |
773 /** | 954 /** |
774 * Checks if the last signed in state matches the current one. | 955 * Checks if the last signed in state matches the current one. |
775 * If it doesn't, it notifies the listeners of the change. | 956 * If it doesn't, it notifies the listeners of the change. |
776 */ | 957 */ |
777 function checkAndNotifyListeners() { | 958 function checkAndNotifyListeners() { |
778 isSignedIn(function(signedIn) { | 959 isSignedIn().then(function(signedIn) { |
779 instrumented.storage.local.get('lastSignedInState', function(items) { | 960 instrumented.storage.local.get('lastSignedInState', function(items) { |
780 items = items || {}; | 961 items = items || {}; |
781 if (items.lastSignedInState != signedIn) { | 962 if (items.lastSignedInState != signedIn) { |
782 chrome.storage.local.set( | 963 chrome.storage.local.set( |
783 {lastSignedInState: signedIn}); | 964 {lastSignedInState: signedIn}); |
784 listeners.forEach(function(callback) { | 965 listeners.forEach(function(callback) { |
785 callback(); | 966 callback(); |
786 }); | 967 }); |
787 } | 968 } |
788 }); | 969 }); |
(...skipping 13 matching lines...) Expand all Loading... |
802 // One hour is just an arbitrary amount of time chosen. | 983 // One hour is just an arbitrary amount of time chosen. |
803 chrome.alarms.create(alarmName, {periodInMinutes: 60}); | 984 chrome.alarms.create(alarmName, {periodInMinutes: 60}); |
804 | 985 |
805 return { | 986 return { |
806 addListener: addListener, | 987 addListener: addListener, |
807 getAuthToken: getAuthToken, | 988 getAuthToken: getAuthToken, |
808 isSignedIn: isSignedIn, | 989 isSignedIn: isSignedIn, |
809 removeToken: removeToken | 990 removeToken: removeToken |
810 }; | 991 }; |
811 } | 992 } |
OLD | NEW |