Bug 1890689 accumulate input in LargerReceiverBlockSizeThanDesiredBuffering GTest...
[gecko.git] / services / fxaccounts / FxAccountsProfile.sys.mjs
blob8022a6d8a8cca41f58d3ddfa2e1a1f9a62a7aaea
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /**
6  * Firefox Accounts Profile helper.
7  *
8  * This class abstracts interaction with the profile server for an account.
9  * It will handle things like fetching profile data, listening for updates to
10  * the user's profile in open browser tabs, and cacheing/invalidating profile data.
11  */
13 import {
14   ON_PROFILE_CHANGE_NOTIFICATION,
15   log,
16 } from "resource://gre/modules/FxAccountsCommon.sys.mjs";
18 import { getFxAccountsSingleton } from "resource://gre/modules/FxAccounts.sys.mjs";
20 const fxAccounts = getFxAccountsSingleton();
22 const lazy = {};
24 ChromeUtils.defineESModuleGetters(lazy, {
25   FxAccountsProfileClient:
26     "resource://gre/modules/FxAccountsProfileClient.sys.mjs",
27 });
29 export var FxAccountsProfile = function (options = {}) {
30   this._currentFetchPromise = null;
31   this._cachedAt = 0; // when we saved the cached version.
32   this._isNotifying = false; // are we sending a notification?
33   this.fxai = options.fxai || fxAccounts._internal;
34   this.client =
35     options.profileClient ||
36     new lazy.FxAccountsProfileClient({
37       fxai: this.fxai,
38       serverURL: options.profileServerUrl,
39     });
41   // An observer to invalidate our _cachedAt optimization. We use a weak-ref
42   // just incase this.tearDown isn't called in some cases.
43   Services.obs.addObserver(this, ON_PROFILE_CHANGE_NOTIFICATION, true);
44   // for testing
45   if (options.channel) {
46     this.channel = options.channel;
47   }
50 FxAccountsProfile.prototype = {
51   // If we get subsequent requests for a profile within this period, don't bother
52   // making another request to determine if it is fresh or not.
53   PROFILE_FRESHNESS_THRESHOLD: 120000, // 2 minutes
55   observe(subject, topic) {
56     // If we get a profile change notification from our webchannel it means
57     // the user has just changed their profile via the web, so we want to
58     // ignore our "freshness threshold"
59     if (topic == ON_PROFILE_CHANGE_NOTIFICATION && !this._isNotifying) {
60       log.debug("FxAccountsProfile observed profile change");
61       this._cachedAt = 0;
62     }
63   },
65   tearDown() {
66     this.fxai = null;
67     this.client = null;
68     Services.obs.removeObserver(this, ON_PROFILE_CHANGE_NOTIFICATION);
69   },
71   _notifyProfileChange(uid) {
72     this._isNotifying = true;
73     Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, uid);
74     this._isNotifying = false;
75   },
77   // Cache fetched data and send out a notification so that UI can update.
78   _cacheProfile(response) {
79     return this.fxai.withCurrentAccountState(async state => {
80       const profile = response.body;
81       const userData = await state.getUserAccountData();
82       if (profile.uid != userData.uid) {
83         throw new Error(
84           "The fetched profile does not correspond with the current account."
85         );
86       }
87       let profileCache = {
88         profile,
89         etag: response.etag,
90       };
91       await state.updateUserAccountData({ profileCache });
92       if (profile.email != userData.email) {
93         await this.fxai._handleEmailUpdated(profile.email);
94       }
95       log.debug("notifying profile changed for user ${uid}", userData);
96       this._notifyProfileChange(userData.uid);
97       return profile;
98     });
99   },
101   async _getProfileCache() {
102     let data = await this.fxai.currentAccountState.getUserAccountData([
103       "profileCache",
104     ]);
105     return data ? data.profileCache : null;
106   },
108   async _fetchAndCacheProfileInternal() {
109     try {
110       const profileCache = await this._getProfileCache();
111       const etag = profileCache ? profileCache.etag : null;
112       let response;
113       try {
114         response = await this.client.fetchProfile(etag);
115       } catch (err) {
116         await this.fxai._handleTokenError(err);
117         // _handleTokenError always re-throws.
118         throw new Error("not reached!");
119       }
121       // response may be null if the profile was not modified (same ETag).
122       if (!response) {
123         return null;
124       }
125       return await this._cacheProfile(response);
126     } finally {
127       this._cachedAt = Date.now();
128       this._currentFetchPromise = null;
129     }
130   },
132   _fetchAndCacheProfile() {
133     if (!this._currentFetchPromise) {
134       this._currentFetchPromise = this._fetchAndCacheProfileInternal();
135     }
136     return this._currentFetchPromise;
137   },
139   // Returns cached data right away if available, otherwise returns null - if
140   // it returns null, or if the profile is possibly stale, it attempts to
141   // fetch the latest profile data in the background. After data is fetched a
142   // notification will be sent out if the profile has changed.
143   async getProfile() {
144     const profileCache = await this._getProfileCache();
145     if (!profileCache) {
146       // fetch and cache it in the background.
147       this._fetchAndCacheProfile().catch(err => {
148         log.error("Background refresh of initial profile failed", err);
149       });
150       return null;
151     }
152     if (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD) {
153       // Note that _fetchAndCacheProfile isn't returned, so continues
154       // in the background.
155       this._fetchAndCacheProfile().catch(err => {
156         log.error("Background refresh of profile failed", err);
157       });
158     } else {
159       log.trace("not checking freshness of profile as it remains recent");
160     }
161     return profileCache.profile;
162   },
164   // Get the user's profile data, fetching from the network if necessary.
165   // Most callers should instead use `getProfile()`; this methods exists to support
166   // callers who need to await the underlying network request.
167   async ensureProfile({ staleOk = false, forceFresh = false } = {}) {
168     if (staleOk && forceFresh) {
169       throw new Error("contradictory options specified");
170     }
171     const profileCache = await this._getProfileCache();
172     if (
173       forceFresh ||
174       !profileCache ||
175       (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD &&
176         !staleOk)
177     ) {
178       const profile = await this._fetchAndCacheProfile().catch(err => {
179         log.error("Background refresh of profile failed", err);
180       });
181       if (profile) {
182         return profile;
183       }
184     }
185     log.trace("not checking freshness of profile as it remains recent");
186     return profileCache ? profileCache.profile : null;
187   },
189   QueryInterface: ChromeUtils.generateQI([
190     "nsIObserver",
191     "nsISupportsWeakReference",
192   ]),