Skip to content

Add persistence layer: index db, in memory, and browser{local, session} #2908

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 16, 2020
3 changes: 2 additions & 1 deletion packages-exp/auth-exp/src/api/authentication/sign_up.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import { Endpoint } from '..';
import { ServerError } from '../errors';
import { FirebaseError } from '@firebase/util';
import * as mockFetch from '../../../test/mock_fetch';
import { mockEndpoint, mockAuth } from '../../../test/api/helper';
import { mockEndpoint } from '../../../test/api/helper';
import { mockAuth } from '../../../test/mock_auth';

use(chaiAsPromised);

Expand Down
122 changes: 122 additions & 0 deletions packages-exp/auth-exp/src/core/persistence/browser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* @license
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as sinon from 'sinon';
import { PersistenceType } from '.';
import { expect } from 'chai';
import { browserLocalPersistence, browserSessionPersistence } from './browser';
import { User } from '../../model/user';
import { testUser } from '../../../test/mock_auth';

describe('core/persistence/browser', () => {
beforeEach(() => {
localStorage.clear();
sessionStorage.clear();
});

afterEach(() => sinon.restore());

describe('browserLocalPersistence', () => {
const persistence = browserLocalPersistence;

it('should work with persistence type', async () => {
const key = 'my-super-special-persistence-type';
const value = PersistenceType.LOCAL;
expect(await persistence.get(key)).to.be.null;
await persistence.set(key, value);
expect(await persistence.get(key)).to.be.eq(value);
expect(await persistence.get('other-key')).to.be.null;
await persistence.remove(key);
expect(await persistence.get(key)).to.be.null;
});

it('should call instantiator function if provided', async () => {
const key = 'my-super-special-user';
const value = testUser('some-uid');

expect(await persistence.get(key)).to.be.null;
await persistence.set(key, value);
const out = await persistence.get<User>(key, blob =>
testUser(`test-${blob.uid}`)
);
expect(out?.uid).to.eql('test-some-uid');
await persistence.remove(key);
expect(await persistence.get(key)).to.be.null;
});

describe('#isAvailable', () => {
it('should emit false if localStorage setItem throws', async () => {
sinon.stub(localStorage, 'setItem').throws(new Error('nope'));
expect(await persistence.isAvailable()).to.be.false;
});

it('should emit false if localStorage removeItem throws', async () => {
sinon.stub(localStorage, 'removeItem').throws(new Error('nope'));
expect(await persistence.isAvailable()).to.be.false;
});

it('should emit true if everything works properly', async () => {
expect(await persistence.isAvailable()).to.be.true;
});
});
});

describe('browserSessionPersistence', () => {
const persistence = browserSessionPersistence;

it('should work with persistence type', async () => {
const key = 'my-super-special-persistence-type';
const value = PersistenceType.SESSION;
expect(await persistence.get(key)).to.be.null;
await persistence.set(key, value);
expect(await persistence.get(key)).to.be.eq(value);
expect(await persistence.get('other-key')).to.be.null;
await persistence.remove(key);
expect(await persistence.get(key)).to.be.null;
});

it('should call instantiator function if provided', async () => {
const key = 'my-super-special-user';
const value = testUser('some-uid');

expect(await persistence.get(key)).to.be.null;
await persistence.set(key, value);
const out = await persistence.get<User>(key, blob =>
testUser(`test-${blob.uid}`)
);
expect(out?.uid).to.eql('test-some-uid');
await persistence.remove(key);
expect(await persistence.get(key)).to.be.null;
});

describe('#isAvailable', () => {
it('should emit false if sessionStorage setItem throws', async () => {
sinon.stub(sessionStorage, 'setItem').throws(new Error('nope'));
expect(await persistence.isAvailable()).to.be.false;
});

it('should emit false if sessionStorage removeItem throws', async () => {
sinon.stub(sessionStorage, 'removeItem').throws(new Error('nope'));
expect(await persistence.isAvailable()).to.be.false;
});

it('should emit true if everything works properly', async () => {
expect(await persistence.isAvailable()).to.be.true;
});
});
});
});
68 changes: 68 additions & 0 deletions packages-exp/auth-exp/src/core/persistence/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* @license
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {
Persistence,
PersistenceType,
PersistenceValue,
Instantiator
} from '.';

const STORAGE_AVAILABLE_KEY_ = '__sak';

class BrowserPersistence implements Persistence {
type: PersistenceType = PersistenceType.LOCAL;

constructor(private readonly storage: Storage) {}

async isAvailable(): Promise<boolean> {
try {
if (!this.storage) {
return false;
}
this.storage.setItem(STORAGE_AVAILABLE_KEY_, '1');
this.storage.removeItem(STORAGE_AVAILABLE_KEY_);
return true;
} catch {
return false;
}
}

async set(key: string, value: PersistenceValue): Promise<void> {
this.storage.setItem(key, JSON.stringify(value));
}

async get<T extends PersistenceValue>(
key: string,
instantiator?: Instantiator<T>
): Promise<T | null> {
const json = this.storage.getItem(key);
const obj = json ? JSON.parse(json) : null;
return instantiator && obj ? instantiator(obj) : obj;
}

async remove(key: string): Promise<void> {
this.storage.removeItem(key);
}
}

export const browserLocalPersistence: Persistence = new BrowserPersistence(
localStorage
);
export const browserSessionPersistence: Persistence = new BrowserPersistence(
sessionStorage
);
51 changes: 51 additions & 0 deletions packages-exp/auth-exp/src/core/persistence/in_memory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @license
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { inMemoryPersistence as persistence } from './in_memory';
import { PersistenceType } from '.';
import { expect } from 'chai';
import { User } from '../../model/user';
import { testUser } from '../../../test/mock_auth';

describe('core/persistence/in_memory', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we standardize on how we name these?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, how would you like to standardize them? I don't really care which way we end up choosing

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

me neither, is there a pattern that the other packages follow? @Feiyang1

it('should work with persistence type', async () => {
const key = 'my-super-special-persistence-type';
const value = PersistenceType.LOCAL;
expect(await persistence.get(key)).to.be.null;
await persistence.set(key, value);
expect(await persistence.get(key)).to.be.eq(value);
expect(await persistence.get('other-key')).to.be.null;
await persistence.remove(key);
expect(await persistence.get(key)).to.be.null;
});

it('should work with user', async () => {
const key = 'my-super-special-user';
const value = testUser('uid');

expect(await persistence.get(key)).to.be.null;
await persistence.set(key, value);
expect(await persistence.get<User>(key)).to.eql(value);
expect(await persistence.get('other-key')).to.be.null;
await persistence.remove(key);
expect(await persistence.get(key)).to.be.null;
});

it('isAvailable returns true', async () => {
expect(await persistence.isAvailable()).to.be.true;
});
});
44 changes: 44 additions & 0 deletions packages-exp/auth-exp/src/core/persistence/in_memory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* @license
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Persistence, PersistenceType, PersistenceValue } from '../persistence';

class InMemoryPersistence implements Persistence {
type: PersistenceType = PersistenceType.NONE;
storage: {
[key: string]: PersistenceValue;
} = {};

async isAvailable(): Promise<boolean> {
return true;
}

async set(key: string, value: PersistenceValue): Promise<void> {
this.storage[key] = value;
}

async get<T extends PersistenceValue>(key: string): Promise<T | null> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you have a version of this one with instantiator?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above.

const value = this.storage[key];
return value === undefined ? null : (value as T);
}

async remove(key: string): Promise<void> {
delete this.storage[key];
}
}

export const inMemoryPersistence: Persistence = new InMemoryPersistence();
41 changes: 41 additions & 0 deletions packages-exp/auth-exp/src/core/persistence/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @license
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { User } from '../../model/user';

export enum PersistenceType {
SESSION = 'SESSION',
LOCAL = 'LOCAL',
NONE = 'NONE'
}

export interface Instantiator<T> {
(blob: { [key: string]: unknown }): T;
}

export type PersistenceValue = PersistenceType | User;

export interface Persistence {
type: PersistenceType;
isAvailable(): Promise<boolean>;
set(key: string, value: PersistenceValue): Promise<void>;
get<T extends PersistenceValue>(
key: string,
instantiator?: Instantiator<T>
): Promise<T | null>;
remove(key: string): Promise<void>;
}
49 changes: 49 additions & 0 deletions packages-exp/auth-exp/src/core/persistence/indexed_db.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* @license
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { PersistenceType } from '.';
import { expect } from 'chai';
import { indexedDBLocalPersistence as persistence } from './indexed_db';
import { User } from '../../model/user';
import { testUser } from '../../../test/mock_auth';

describe('core/persistence/indexed_db', () => {
it('should work with persistence type', async () => {
const key = 'my-super-special-persistence-type';
const value = PersistenceType.LOCAL;
expect(await persistence.get(key)).to.be.null;
await persistence.set(key, value);
expect(await persistence.get(key)).to.be.eq(value);
expect(await persistence.get('other-key')).to.be.null;
await persistence.remove(key);
expect(await persistence.get(key)).to.be.null;
});

it('should call instantiator function if provided', async () => {
const key = 'my-super-special-user';
const value = testUser('some-uid');

expect(await persistence.get(key)).to.be.null;
await persistence.set(key, value);
const out = await persistence.get<User>(key, blob =>
testUser(`test-${blob.uid}`)
);
expect(out?.uid).to.eql('test-some-uid');
await persistence.remove(key);
expect(await persistence.get(key)).to.be.null;
});
});
Loading