Skip to content

Commit 0ede306

Browse files
committed
Split out local & session storage
1 parent 75f6f25 commit 0ede306

File tree

8 files changed

+396
-331
lines changed

8 files changed

+396
-331
lines changed

packages-exp/auth-exp/index.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,8 @@ export * from './src';
2626
// Additional DOM dependend functionality
2727

2828
// persistence
29-
export {
30-
browserLocalPersistence,
31-
browserSessionPersistence
32-
} from './src/platform_browser/persistence/browser';
29+
export { browserLocalPersistence } from './src/platform_browser/persistence/local_storage';
30+
export { browserSessionPersistence } from './src/platform_browser/persistence/session_storage';
3331
export { indexedDBLocalPersistence } from './src/platform_browser/persistence/indexed_db';
3432

3533
// providers

packages-exp/auth-exp/src/core/util/browser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export function _isIEMobile(ua: string): boolean {
111111
}
112112

113113
export function _isAndroid(ua: string): boolean {
114-
return /android\//i.test(ua);
114+
return /android/i.test(ua);
115115
}
116116

117117
export function _isBlackBerry(ua: string): boolean {

packages-exp/auth-exp/src/platform_browser/persistence/browser.ts

Lines changed: 27 additions & 275 deletions
Original file line numberDiff line numberDiff line change
@@ -15,295 +15,47 @@
1515
* limitations under the License.
1616
*/
1717

18-
import * as externs from '@firebase/auth-types-exp';
19-
import { getUA } from '@firebase/util';
20-
2118
import {
22-
Persistence,
23-
PersistenceType,
2419
PersistenceValue,
2520
STORAGE_AVAILABLE_KEY,
26-
StorageEventListener
21+
PersistenceType
2722
} from '../../core/persistence';
28-
import {
29-
_isSafari,
30-
_isIOS,
31-
_isIframe,
32-
_isIE10,
33-
_isFirefox,
34-
_isMobileBrowser
35-
} from '../../core/util/browser';
3623

3724
// There are two different browser persistence types: local and session.
3825
// 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);
26338
}
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);
26444
}
265-
this.listeners[key] = this.listeners[key] || [];
266-
this.listeners[key].add(listener);
26745
}
26846

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();
28350
}
284-
}
28551

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);
29955
}
30056

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();
30460
}
30561
}
306-
307-
export const browserLocalPersistence: externs.Persistence = BrowserLocalPersistence;
308-
309-
export const browserSessionPersistence: externs.Persistence = BrowserSessionPersistence;

0 commit comments

Comments
 (0)