|
15 | 15 | * limitations under the License.
|
16 | 16 | */
|
17 | 17 |
|
18 |
| -import * as externs from '@firebase/auth-types-exp'; |
19 |
| -import { getUA } from '@firebase/util'; |
20 |
| - |
21 | 18 | import {
|
22 |
| - Persistence, |
23 |
| - PersistenceType, |
24 | 19 | PersistenceValue,
|
25 | 20 | STORAGE_AVAILABLE_KEY,
|
26 |
| - StorageEventListener |
| 21 | + PersistenceType |
27 | 22 | } from '../../core/persistence';
|
28 |
| -import { |
29 |
| - _isSafari, |
30 |
| - _isIOS, |
31 |
| - _isIframe, |
32 |
| - _isIE10, |
33 |
| - _isFirefox, |
34 |
| - _isMobileBrowser |
35 |
| -} from '../../core/util/browser'; |
36 | 23 |
|
37 | 24 | // There are two different browser persistence types: local and session.
|
38 | 25 | // Both have the same implementation but use a different underlying storage
|
39 |
| -// object. Using class inheritance compiles down to an es5 polyfill, which |
40 |
| -// prevents rollup from tree shaking. By making these "methods" free floating |
41 |
| -// functions bound to the classes, the two different types can share the |
42 |
| -// implementation without subclassing. |
43 |
| - |
44 |
| -interface BrowserPersistenceClass extends Persistence { |
45 |
| - storage: Storage; |
46 |
| -} |
47 |
| - |
48 |
| -function isAvailable(this: BrowserPersistenceClass): Promise<boolean> { |
49 |
| - try { |
50 |
| - if (!this.storage) { |
51 |
| - return Promise.resolve(false); |
52 |
| - } |
53 |
| - this.storage.setItem(STORAGE_AVAILABLE_KEY, '1'); |
54 |
| - this.storage.removeItem(STORAGE_AVAILABLE_KEY); |
55 |
| - return Promise.resolve(true); |
56 |
| - } catch { |
57 |
| - return Promise.resolve(false); |
58 |
| - } |
59 |
| -} |
60 |
| - |
61 |
| -function _iframeCannotSyncWebStorage(): boolean { |
62 |
| - const ua = getUA(); |
63 |
| - return _isSafari(ua) || _isIOS(ua); |
64 |
| -} |
65 |
| - |
66 |
| -function set( |
67 |
| - this: BrowserPersistenceClass, |
68 |
| - key: string, |
69 |
| - value: PersistenceValue |
70 |
| -): Promise<void> { |
71 |
| - this.storage.setItem(key, JSON.stringify(value)); |
72 |
| - return Promise.resolve(); |
73 |
| -} |
74 |
| - |
75 |
| -function get<T extends PersistenceValue>( |
76 |
| - this: BrowserPersistenceClass, |
77 |
| - key: string |
78 |
| -): Promise<T | null> { |
79 |
| - const json = this.storage.getItem(key); |
80 |
| - return Promise.resolve(json ? JSON.parse(json) : null); |
81 |
| -} |
82 |
| - |
83 |
| -function remove(this: BrowserPersistenceClass, key: string): Promise<void> { |
84 |
| - this.storage.removeItem(key); |
85 |
| - return Promise.resolve(); |
86 |
| -} |
87 |
| - |
88 |
| -class BrowserLocalPersistence implements BrowserPersistenceClass { |
89 |
| - static type: 'LOCAL' = 'LOCAL'; |
90 |
| - type = PersistenceType.LOCAL; |
91 |
| - storage = localStorage; |
92 |
| - isAvailable = isAvailable; |
93 |
| - |
94 |
| - set = set; |
95 |
| - get = get; |
96 |
| - remove = remove; |
97 |
| - |
98 |
| - // The polling period in case events are not supported |
99 |
| - private static readonly POLLING_TIMER_INTERVAL = 1000; |
100 |
| - // The IE 10 localStorage cross tab synchronization delay in milliseconds |
101 |
| - private static readonly IE10_LOCAL_STORAGE_SYNC_DELAY = 10; |
102 |
| - |
103 |
| - private readonly listeners: Record<string, Set<StorageEventListener>> = {}; |
104 |
| - private readonly localCache: Record<string, string | null> = {}; |
105 |
| - private pollTimer: NodeJS.Timeout | null = null; |
106 |
| - |
107 |
| - // Safari or iOS browser and embedded in an iframe. |
108 |
| - private readonly safariLocalStorageNotSynced = |
109 |
| - _iframeCannotSyncWebStorage() && _isIframe(); |
110 |
| - |
111 |
| - _forAllChangedKeys( |
112 |
| - cb: (key: string, oldValue: string | null, newValue: string | null) => void |
113 |
| - ): void { |
114 |
| - // Check all keys with listeners on them. |
115 |
| - for (const key of Object.keys(this.listeners)) { |
116 |
| - // Get value from localStorage. |
117 |
| - const newValue = this.storage.getItem(key); |
118 |
| - const oldValue = this.localCache[key]; |
119 |
| - // If local map value does not match, trigger listener with storage event. |
120 |
| - // Differentiate this simulated event from the real storage event. |
121 |
| - if (newValue !== oldValue) { |
122 |
| - cb(key, oldValue, newValue); |
123 |
| - } |
124 |
| - }; |
125 |
| - } |
126 |
| - |
127 |
| - _onStorageEvent(event: StorageEvent, poll: boolean = false): void { |
128 |
| - // Key would be null in some situations, like when localStorage is cleared |
129 |
| - if (!event.key) { |
130 |
| - this._forAllChangedKeys( |
131 |
| - (key: string, _oldValue: string | null, newValue: string | null) => { |
132 |
| - this._notifyListeners(key, newValue); |
133 |
| - } |
134 |
| - ); |
135 |
| - return; |
136 |
| - } |
137 |
| - |
138 |
| - const key = event.key; |
139 |
| - |
140 |
| - // Ignore keys that have no listeners. |
141 |
| - if (!this.listeners[key]) { |
142 |
| - return; |
143 |
| - } |
144 |
| - |
145 |
| - // Check the mechanism how this event was detected. |
146 |
| - // The first event will dictate the mechanism to be used. |
147 |
| - if (poll) { |
148 |
| - // Environment detects storage changes via polling. |
149 |
| - // Remove storage event listener to prevent possible event duplication. |
150 |
| - this._detachListener(); |
151 |
| - } else { |
152 |
| - // Environment detects storage changes via storage event listener. |
153 |
| - // Remove polling listener to prevent possible event duplication. |
154 |
| - this._stopPolling(); |
155 |
| - } |
156 |
| - |
157 |
| - // Safari embedded iframe. Storage event will trigger with the delta |
158 |
| - // changes but no changes will be applied to the iframe localStorage. |
159 |
| - if (this.safariLocalStorageNotSynced) { |
160 |
| - // Get current iframe page value. |
161 |
| - const storedValue = this.storage.getItem(key); |
162 |
| - // Value not synchronized, synchronize manually. |
163 |
| - if (event.newValue !== storedValue) { |
164 |
| - if (event.newValue !== null) { |
165 |
| - // Value changed from current value. |
166 |
| - this.storage.setItem(key, event.newValue); |
167 |
| - } else { |
168 |
| - // Current value deleted. |
169 |
| - this.storage.removeItem(key); |
170 |
| - } |
171 |
| - } else if (this.localCache[key] === event.newValue && !poll) { |
172 |
| - // Already detected and processed, do not trigger listeners again. |
173 |
| - return; |
174 |
| - } |
175 |
| - } |
176 |
| - |
177 |
| - const triggerListeners = (): void => { |
178 |
| - // Keep local map up to date in case storage event is triggered before |
179 |
| - // poll. |
180 |
| - const storedValue = this.storage.getItem(key); |
181 |
| - if (!poll && this.localCache[key] === storedValue) { |
182 |
| - // Real storage event which has already been detected, do nothing. |
183 |
| - // This seems to trigger in some IE browsers for some reason. |
184 |
| - return; |
185 |
| - } |
186 |
| - this._notifyListeners(key, storedValue); |
187 |
| - }; |
188 |
| - |
189 |
| - const storedValue = this.storage.getItem(key); |
190 |
| - if ( |
191 |
| - _isIE10() && |
192 |
| - storedValue !== event.newValue && |
193 |
| - event.newValue !== event.oldValue |
194 |
| - ) { |
195 |
| - // IE 10 has this weird bug where a storage event would trigger with the |
196 |
| - // correct key, oldValue and newValue but localStorage.getItem(key) does |
197 |
| - // not yield the updated value until a few milliseconds. This ensures |
198 |
| - // this recovers from that situation. |
199 |
| - setTimeout( |
200 |
| - triggerListeners, |
201 |
| - BrowserLocalPersistence.IE10_LOCAL_STORAGE_SYNC_DELAY |
202 |
| - ); |
203 |
| - } else { |
204 |
| - triggerListeners(); |
205 |
| - } |
206 |
| - } |
207 |
| - |
208 |
| - _notifyListeners(key: string, value: string | null): void { |
209 |
| - if (!this.listeners[key]) { |
210 |
| - return; |
211 |
| - } |
212 |
| - for (const listener of Array.from(this.listeners[key])) { |
213 |
| - this.localCache[key] = value; |
214 |
| - listener(value ? JSON.parse(value) : value); |
215 |
| - }; |
216 |
| - } |
217 |
| - |
218 |
| - _startPolling(): void { |
219 |
| - this._stopPolling(); |
220 |
| - |
221 |
| - this.pollTimer = setInterval(() => { |
222 |
| - this._forAllChangedKeys( |
223 |
| - (key: string, oldValue: string | null, newValue: string | null) => { |
224 |
| - this._onStorageEvent( |
225 |
| - new StorageEvent('storage', { |
226 |
| - key, |
227 |
| - oldValue, |
228 |
| - newValue |
229 |
| - }), |
230 |
| - /* poll */ true |
231 |
| - ); |
232 |
| - } |
233 |
| - ); |
234 |
| - }, BrowserLocalPersistence.POLLING_TIMER_INTERVAL); |
235 |
| - } |
236 |
| - |
237 |
| - _stopPolling(): void { |
238 |
| - if (this.pollTimer) { |
239 |
| - clearInterval(this.pollTimer); |
240 |
| - this.pollTimer = null; |
241 |
| - } |
242 |
| - } |
243 |
| - |
244 |
| - _attachListener(): void { |
245 |
| - window.addEventListener('storage', this._onStorageEvent); |
246 |
| - } |
247 |
| - |
248 |
| - _detachListener(): void { |
249 |
| - window.removeEventListener('storage', this._onStorageEvent); |
250 |
| - } |
251 |
| - |
252 |
| - addListener(key: string, listener: StorageEventListener): void { |
253 |
| - this.localCache[key] = this.storage.getItem(key); |
254 |
| - if (Object.keys(this.listeners).length === 0) { |
255 |
| - // Whether browser can detect storage event when it had already been pushed to the background. |
256 |
| - // This may happen in some mobile browsers. A localStorage change in the foreground window |
257 |
| - // will not be detected in the background window via the storage event. |
258 |
| - // This was detected in iOS 7.x mobile browsers |
259 |
| - if (_isMobileBrowser()) { |
260 |
| - this._startPolling(); |
261 |
| - } else { |
262 |
| - this._attachListener(); |
| 26 | +// object. |
| 27 | + |
| 28 | +export abstract class BrowserPersistenceClass { |
| 29 | + protected constructor( |
| 30 | + protected readonly storage: Storage, |
| 31 | + readonly type: PersistenceType |
| 32 | + ) {} |
| 33 | + |
| 34 | + isAvailable(this: BrowserPersistenceClass): Promise<boolean> { |
| 35 | + try { |
| 36 | + if (!this.storage) { |
| 37 | + return Promise.resolve(false); |
263 | 38 | }
|
| 39 | + this.storage.setItem(STORAGE_AVAILABLE_KEY, '1'); |
| 40 | + this.storage.removeItem(STORAGE_AVAILABLE_KEY); |
| 41 | + return Promise.resolve(true); |
| 42 | + } catch { |
| 43 | + return Promise.resolve(false); |
264 | 44 | }
|
265 |
| - this.listeners[key] = this.listeners[key] || []; |
266 |
| - this.listeners[key].add(listener); |
267 | 45 | }
|
268 | 46 |
|
269 |
| - removeListener(key: string, listener: StorageEventListener): void { |
270 |
| - if (this.listeners[key]) { |
271 |
| - this.listeners[key].delete(listener); |
272 |
| - |
273 |
| - if (this.listeners[key].size === 0) { |
274 |
| - delete this.listeners[key]; |
275 |
| - delete this.localCache[key]; |
276 |
| - } |
277 |
| - } |
278 |
| - |
279 |
| - if (Object.keys(this.listeners).length === 0) { |
280 |
| - this._detachListener(); |
281 |
| - this._stopPolling(); |
282 |
| - } |
| 47 | + set(key: string, value: PersistenceValue): Promise<void> { |
| 48 | + this.storage.setItem(key, JSON.stringify(value)); |
| 49 | + return Promise.resolve(); |
283 | 50 | }
|
284 |
| -} |
285 | 51 |
|
286 |
| -class BrowserSessionPersistence implements BrowserPersistenceClass { |
287 |
| - static type: 'SESSION' = 'SESSION'; |
288 |
| - type = PersistenceType.SESSION; |
289 |
| - storage = sessionStorage; |
290 |
| - isAvailable = isAvailable; |
291 |
| - |
292 |
| - set = set; |
293 |
| - get = get; |
294 |
| - remove = remove; |
295 |
| - |
296 |
| - addListener(_key: string, _listener: StorageEventListener): void { |
297 |
| - // Listeners are not supported for session storage since it cannot be shared across windows |
298 |
| - return; |
| 52 | + get<T extends PersistenceValue>(key: string): Promise<T | null> { |
| 53 | + const json = this.storage.getItem(key); |
| 54 | + return Promise.resolve(json ? JSON.parse(json) : null); |
299 | 55 | }
|
300 | 56 |
|
301 |
| - removeListener(_key: string, _listener: StorageEventListener): void { |
302 |
| - // Listeners are not supported for session storage since it cannot be shared across windows |
303 |
| - return; |
| 57 | + remove(key: string): Promise<void> { |
| 58 | + this.storage.removeItem(key); |
| 59 | + return Promise.resolve(); |
304 | 60 | }
|
305 | 61 | }
|
306 |
| - |
307 |
| -export const browserLocalPersistence: externs.Persistence = BrowserLocalPersistence; |
308 |
| - |
309 |
| -export const browserSessionPersistence: externs.Persistence = BrowserSessionPersistence; |
0 commit comments