Skip to content

Commit 2070553

Browse files
authored
Tree shakeable persistence (#3288)
* Make the persistence objects tree-shakeable * Formatting * Updated to stop using casts everywhere * Formatting * Make the browser impls tree shakeable * Formatting * Fix the react native implementation in auth-compat-exp * Formatting * Make the react native class wrapped in a closure, to avoid the prototype nonsense * Formatting * Fix index.rn.ts * Formatting * PR feedback * Fix ordering in index.ts * Formatting * Fix test * Formatting
1 parent b9ece92 commit 2070553

16 files changed

+238
-91
lines changed

packages-exp/auth-compat-exp/index.rn.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
*/
2424

2525
import { AsyncStorage } from 'react-native';
26-
import { ReactNativePersistence } from '@firebase/auth-exp/src/core/persistence/react_native';
26+
27+
import { getReactNativePersistence } from '@firebase/auth-exp/src/core/persistence/react_native';
2728

2829
// eslint-disable-next-line @typescript-eslint/no-unused-vars
29-
const reactNativeLocalPersistence = new ReactNativePersistence(AsyncStorage);
30+
const reactNativeLocalPersistence = getReactNativePersistence(AsyncStorage);

packages-exp/auth-exp/src/core/auth/auth_impl.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,18 @@ import { FirebaseError } from '@firebase/util';
2626
import { testUser } from '../../../test/mock_auth';
2727
import { Auth } from '../../model/auth';
2828
import { User } from '../../model/user';
29-
import { Persistence } from '../persistence';
29+
import { _getInstance, Persistence } from '../persistence';
3030
import { browserLocalPersistence } from '../persistence/browser';
3131
import { inMemoryPersistence } from '../persistence/in_memory';
3232
import { PersistenceUserManager } from '../persistence/persistence_user_manager';
33+
import * as navigator from '../util/navigator';
3334
import { _getClientVersion, ClientPlatform } from '../util/version';
3435
import {
3536
DEFAULT_API_HOST,
3637
DEFAULT_API_SCHEME,
3738
DEFAULT_TOKEN_API_HOST,
3839
initializeAuth
3940
} from './auth_impl';
40-
import * as navigator from '../util/navigator';
4141

4242
use(sinonChai);
4343

@@ -55,7 +55,7 @@ describe('core/auth/auth_impl', () => {
5555
let persistenceStub: sinon.SinonStubbedInstance<Persistence>;
5656

5757
beforeEach(() => {
58-
persistenceStub = sinon.stub(inMemoryPersistence as Persistence);
58+
persistenceStub = sinon.stub(_getInstance(inMemoryPersistence));
5959
auth = initializeAuth(FAKE_APP, {
6060
persistence: inMemoryPersistence
6161
}) as Auth;
@@ -108,8 +108,8 @@ describe('core/auth/auth_impl', () => {
108108

109109
describe('#setPersistence', () => {
110110
it('swaps underlying persistence', async () => {
111-
const newPersistence = browserLocalPersistence as Persistence;
112-
const newStub = sinon.stub(newPersistence);
111+
const newPersistence = browserLocalPersistence;
112+
const newStub = sinon.stub(_getInstance(newPersistence));
113113
persistenceStub.get.returns(
114114
Promise.resolve(testUser(auth, 'test').toPlainObject())
115115
);
@@ -302,13 +302,13 @@ describe('core/auth/initializeAuth', () => {
302302
it('converts single persistence to array', async () => {
303303
const auth = await initAndWait(inMemoryPersistence);
304304
expect(createManagerStub).to.have.been.calledWith(auth, [
305-
inMemoryPersistence
305+
_getInstance(inMemoryPersistence)
306306
]);
307307
});
308308

309309
it('pulls the user from storage', async () => {
310310
sinon
311-
.stub(inMemoryPersistence as Persistence, 'get')
311+
.stub(_getInstance(inMemoryPersistence), 'get')
312312
.returns(Promise.resolve(testUser({}, 'uid').toPlainObject()));
313313
const auth = await initAndWait(inMemoryPersistence);
314314
expect(auth.currentUser!.uid).to.eq('uid');
@@ -320,8 +320,8 @@ describe('core/auth/initializeAuth', () => {
320320
browserLocalPersistence
321321
]);
322322
expect(createManagerStub).to.have.been.calledWith(auth, [
323-
inMemoryPersistence,
324-
browserLocalPersistence
323+
_getInstance(inMemoryPersistence),
324+
_getInstance(browserLocalPersistence)
325325
]);
326326
});
327327

packages-exp/auth-exp/src/core/auth/auth_impl.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ import {
3131
import { Auth, Dependencies } from '../../model/auth';
3232
import { User } from '../../model/user';
3333
import { AuthErrorCode } from '../errors';
34-
import { Persistence } from '../persistence';
34+
import { _getInstance, Persistence } from '../persistence';
3535
import { PersistenceUserManager } from '../persistence/persistence_user_manager';
3636
import { assert } from '../util/assert';
37-
import { _getClientVersion, ClientPlatform } from '../util/version';
3837
import { _getUserLanguage } from '../util/navigator';
38+
import { _getClientVersion, ClientPlatform } from '../util/version';
3939

4040
interface AsyncAction {
4141
(): Promise<void>;
@@ -101,9 +101,9 @@ export class AuthImpl implements Auth {
101101
return this.updateCurrentUser(null);
102102
}
103103

104-
setPersistence(persistence: Persistence): Promise<void> {
104+
setPersistence(persistence: externs.Persistence): Promise<void> {
105105
return this.queue(async () => {
106-
await this.assertedPersistence.setPersistence(persistence);
106+
await this.assertedPersistence.setPersistence(_getInstance(persistence));
107107
});
108108
}
109109

@@ -215,7 +215,10 @@ export function initializeAuth(
215215
deps?: Dependencies
216216
): externs.Auth {
217217
const persistence = deps?.persistence || [];
218-
const hierarchy = Array.isArray(persistence) ? persistence : [persistence];
218+
const hierarchy = (Array.isArray(persistence)
219+
? persistence
220+
: [persistence]
221+
).map(_getInstance);
219222
const { apiKey, authDomain } = app.options;
220223

221224
// TODO: platform needs to be determined using heuristics
@@ -234,7 +237,7 @@ export function initializeAuth(
234237
// This promise is intended to float; auth initialization happens in the
235238
// background, meanwhile the auth object may be used by the app.
236239
// eslint-disable-next-line @typescript-eslint/no-floating-promises
237-
auth._initializeWithPersistence(hierarchy as Persistence[]);
240+
auth._initializeWithPersistence(hierarchy);
238241

239242
return auth;
240243
}

packages-exp/auth-exp/src/core/persistence/browser.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { expect } from 'chai';
1919
import * as sinon from 'sinon';
2020

2121
import { testUser } from '../../../test/mock_auth';
22-
import { PersistedBlob, Persistence, PersistenceType } from './';
22+
import { _getInstance, PersistedBlob, Persistence, PersistenceType } from './';
2323
import { browserLocalPersistence, browserSessionPersistence } from './browser';
2424

2525
describe('core/persistence/browser', () => {
@@ -31,7 +31,7 @@ describe('core/persistence/browser', () => {
3131
afterEach(() => sinon.restore());
3232

3333
describe('browserLocalPersistence', () => {
34-
const persistence: Persistence = browserLocalPersistence as Persistence;
34+
const persistence: Persistence = _getInstance(browserLocalPersistence);
3535

3636
it('should work with persistence type', async () => {
3737
const key = 'my-super-special-persistence-type';
@@ -74,7 +74,7 @@ describe('core/persistence/browser', () => {
7474
});
7575

7676
describe('browserSessionPersistence', () => {
77-
const persistence = browserSessionPersistence as Persistence;
77+
const persistence = _getInstance(browserSessionPersistence);
7878

7979
it('should work with persistence type', async () => {
8080
const key = 'my-super-special-persistence-type';

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

Lines changed: 60 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,41 +24,72 @@ import {
2424
STORAGE_AVAILABLE_KEY
2525
} from './';
2626

27-
class BrowserPersistence implements Persistence {
28-
type: PersistenceType = PersistenceType.LOCAL;
27+
// There are two different browser persistence types: local and session.
28+
// Both have the same implementation but use a different underlying storage
29+
// object. Using class inheritance compiles down to an es5 polyfill, which
30+
// prevents rollup from tree shaking. By making these "methods" free floating
31+
// functions bound to the classes, the two different types can share the
32+
// implementation without subclassing.
2933

30-
constructor(private readonly storage: Storage) {}
34+
interface BrowserPersistenceClass extends Persistence {
35+
storage: Storage;
36+
}
3137

32-
async isAvailable(): Promise<boolean> {
33-
try {
34-
if (!this.storage) {
35-
return false;
36-
}
37-
this.storage.setItem(STORAGE_AVAILABLE_KEY, '1');
38-
this.storage.removeItem(STORAGE_AVAILABLE_KEY);
39-
return true;
40-
} catch {
41-
return false;
38+
function isAvailable(this: BrowserPersistenceClass): Promise<boolean> {
39+
try {
40+
if (!this.storage) {
41+
return Promise.resolve(false);
4242
}
43+
this.storage.setItem(STORAGE_AVAILABLE_KEY, '1');
44+
this.storage.removeItem(STORAGE_AVAILABLE_KEY);
45+
return Promise.resolve(true);
46+
} catch {
47+
return Promise.resolve(false);
4348
}
49+
}
4450

45-
async set(key: string, value: PersistenceValue): Promise<void> {
46-
this.storage.setItem(key, JSON.stringify(value));
47-
}
51+
function set(
52+
this: BrowserPersistenceClass,
53+
key: string,
54+
value: PersistenceValue
55+
): Promise<void> {
56+
this.storage.setItem(key, JSON.stringify(value));
57+
return Promise.resolve();
58+
}
4859

49-
async get<T extends PersistenceValue>(key: string): Promise<T | null> {
50-
const json = this.storage.getItem(key);
51-
return json ? JSON.parse(json) : null;
52-
}
60+
function get<T extends PersistenceValue>(
61+
this: BrowserPersistenceClass,
62+
key: string
63+
): Promise<T | null> {
64+
const json = this.storage.getItem(key);
65+
return Promise.resolve(json ? JSON.parse(json) : null);
66+
}
5367

54-
async remove(key: string): Promise<void> {
55-
this.storage.removeItem(key);
56-
}
68+
function remove(this: BrowserPersistenceClass, key: string): Promise<void> {
69+
this.storage.removeItem(key);
70+
return Promise.resolve();
71+
}
72+
73+
class BrowserLocalPersistence implements BrowserPersistenceClass {
74+
static type: 'LOCAL' = 'LOCAL';
75+
type = PersistenceType.LOCAL;
76+
isAvailable = isAvailable;
77+
set = set;
78+
get = get;
79+
remove = remove;
80+
storage = localStorage;
5781
}
5882

59-
export const browserLocalPersistence: externs.Persistence = new BrowserPersistence(
60-
localStorage
61-
);
62-
export const browserSessionPersistence: externs.Persistence = new BrowserPersistence(
63-
sessionStorage
64-
);
83+
class BrowserSessionPersistence implements BrowserPersistenceClass {
84+
static type: 'SESSION' = 'SESSION';
85+
type = PersistenceType.SESSION;
86+
isAvailable = isAvailable;
87+
set = set;
88+
get = get;
89+
remove = remove;
90+
storage = sessionStorage;
91+
}
92+
93+
export const browserLocalPersistence: externs.Persistence = BrowserLocalPersistence;
94+
95+
export const browserSessionPersistence: externs.Persistence = BrowserSessionPersistence;

packages-exp/auth-exp/src/core/persistence/in_memory.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818
import { expect } from 'chai';
1919

2020
import { testUser } from '../../../test/mock_auth';
21-
import { Persistence, PersistenceType } from './';
21+
import { _getInstance, PersistenceType } from './';
2222
import { inMemoryPersistence } from './in_memory';
2323

24-
const persistence = inMemoryPersistence as Persistence;
24+
const persistence = _getInstance(inMemoryPersistence);
2525

2626
describe('core/persistence/in_memory', () => {
2727
it('should work with persistence type', async () => {

packages-exp/auth-exp/src/core/persistence/in_memory.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@ import * as externs from '@firebase/auth-types-exp';
2020
import { Persistence, PersistenceType, PersistenceValue } from '../persistence';
2121

2222
export class InMemoryPersistence implements Persistence {
23-
type: PersistenceType = PersistenceType.NONE;
24-
storage: {
25-
[key: string]: PersistenceValue;
26-
} = {};
23+
static type: 'NONE' = 'NONE';
24+
readonly type = PersistenceType.NONE;
25+
storage: Record<string, PersistenceValue> = {};
2726

2827
async isAvailable(): Promise<boolean> {
2928
return true;
@@ -43,4 +42,4 @@ export class InMemoryPersistence implements Persistence {
4342
}
4443
}
4544

46-
export const inMemoryPersistence: externs.Persistence = new InMemoryPersistence();
45+
export const inMemoryPersistence: externs.Persistence = InMemoryPersistence;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect } from 'chai';
19+
20+
import { _getInstance } from './';
21+
22+
describe('src/core/persistence/index', () => {
23+
context('_getInstance', () => {
24+
// All tests define their own classes since the Class object is used in the
25+
// global map.
26+
27+
it('instantiates a new class', () => {
28+
let classInstantiated = false;
29+
class Persistence {
30+
static type: 'LOCAL' = 'LOCAL';
31+
constructor() {
32+
classInstantiated = true;
33+
}
34+
}
35+
36+
_getInstance(Persistence);
37+
expect(classInstantiated).to.be.true;
38+
});
39+
40+
it('instantiates a class only once', () => {
41+
let instantiationCount = 0;
42+
class Persistence {
43+
static type: 'LOCAL' = 'LOCAL';
44+
constructor() {
45+
instantiationCount++;
46+
}
47+
}
48+
49+
_getInstance(Persistence);
50+
_getInstance(Persistence);
51+
_getInstance(Persistence);
52+
53+
expect(instantiationCount).to.eq(1);
54+
});
55+
56+
it('caches correctly', () => {
57+
class PersistenceA {
58+
static type: 'LOCAL' = 'LOCAL';
59+
}
60+
class PersistenceB {
61+
static type: 'LOCAL' = 'LOCAL';
62+
}
63+
64+
const a = _getInstance(PersistenceA);
65+
const b = _getInstance(PersistenceB);
66+
expect(_getInstance(PersistenceA)).to.eq(a);
67+
expect(_getInstance(PersistenceB)).to.eq(b);
68+
});
69+
});
70+
});

packages-exp/auth-exp/src/core/persistence/index.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
* limitations under the License.
1616
*/
1717

18+
import * as externs from '@firebase/auth-types-exp';
19+
1820
export enum PersistenceType {
1921
SESSION = 'SESSION',
2022
LOCAL = 'LOCAL',
@@ -40,3 +42,27 @@ export interface Persistence {
4042
get<T extends PersistenceValue>(key: string): Promise<T | null>;
4143
remove(key: string): Promise<void>;
4244
}
45+
46+
/**
47+
* We can't directly export all of the different types of persistence as
48+
* constants: this would cause tree-shaking libraries to keep all of the
49+
* various persistence classes in the bundle, even if they're not used, since
50+
* the system can't prove those constructors don't side-effect. Instead, the
51+
* persistence classes themselves all have a static method called _getInstance()
52+
* which does the instantiation.
53+
*/
54+
export interface PersistenceInstantiator extends externs.Persistence {
55+
new (): Persistence;
56+
}
57+
58+
const persistenceCache: Map<externs.Persistence, Persistence> = new Map();
59+
60+
export function _getInstance(cls: externs.Persistence): Persistence {
61+
if (persistenceCache.has(cls)) {
62+
return persistenceCache.get(cls)!;
63+
}
64+
65+
const persistence = new (cls as PersistenceInstantiator)();
66+
persistenceCache.set(cls, persistence);
67+
return persistence;
68+
}

0 commit comments

Comments
 (0)