-
Notifications
You must be signed in to change notification settings - Fork 949
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
Changes from 6 commits
5ad7d60
56dc53f
3c6a22f
06bdd39
e6e8819
86e7d11
b92a0ee
3c4f7ca
0467aa1
3ca8343
c4081af
619b72e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
}); | ||
}); | ||
}); | ||
}); |
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 | ||
); |
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', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we standardize on how we name these? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 () => { | ||
avolkovi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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; | ||
}); | ||
}); |
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> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do you have a version of this one with instantiator? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); |
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>; | ||
} |
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', () => { | ||
sam-gc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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; | ||
}); | ||
}); |
Uh oh!
There was an error while loading. Please reload this page.