From 335d6f36b76175bea50d79ac5cd87f1af9a732cf Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 29 May 2020 13:11:06 +0200 Subject: [PATCH] feat(core-api/Storage): Reworking the StorageApi to have a better interface --- .../{UserSettingsApi.ts => StorageApi.ts} | 57 ++++++----- .../core-api/src/apis/definitions/index.ts | 2 +- .../implementations/StorageApi/WebStorage.ts | 51 ++++++++++ .../apis/implementations/StorageApi/index.ts | 17 ++++ .../implementations/UserSettingsApi/index.ts | 97 ------------------- 5 files changed, 104 insertions(+), 120 deletions(-) rename packages/core-api/src/apis/definitions/{UserSettingsApi.ts => StorageApi.ts} (59%) create mode 100644 packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts create mode 100644 packages/core-api/src/apis/implementations/StorageApi/index.ts delete mode 100644 packages/core-api/src/apis/implementations/UserSettingsApi/index.ts diff --git a/packages/core-api/src/apis/definitions/UserSettingsApi.ts b/packages/core-api/src/apis/definitions/StorageApi.ts similarity index 59% rename from packages/core-api/src/apis/definitions/UserSettingsApi.ts rename to packages/core-api/src/apis/definitions/StorageApi.ts index 62045e0e51..0cb5ccc14d 100644 --- a/packages/core-api/src/apis/definitions/UserSettingsApi.ts +++ b/packages/core-api/src/apis/definitions/StorageApi.ts @@ -16,14 +16,22 @@ import { createApiRef } from '../ApiRef'; -type setValue = (value: T | null) => void; -type removeValue = () => void; -export type UserSettingsApi = { +type UnsubscribeFromStore = () => void; +type SubscribeToStoreHandler = ({ + storeName, + key, + oldValue, + newValue, +}: { + storeName: string; + key: string; + oldValue?: T; + newValue?: T; +}) => void; + +export type StorageApi = { /** * Get persistent data. - * - * TODO: Replace with something less volatile than LocalStorage - * * @param {String} storeName Name of the store. * @param {String?} key (Optional) Unique key associated with the data. * If not key is specified,the whole store is returned. @@ -33,45 +41,50 @@ export type UserSettingsApi = { getFromStore( storeName: string, key: string, - defaultValue: T | null, - ): T | null; + defaultValue?: T, + ): Promise; + /** * Remove persistent data. * - * TODO: Replace with something less volatile than LocalStorage - * * @param {String} storeName Name of the store. * @param {String} key Unique key associated with the data. */ - removeFromStore(storeName: string, key: string): void; + removeFromStore(storeName: string, key: string): Promise; /** - * Save persistent data. - * - * TODO: Replace with something less volatile than LocalStorage + * Save persiswtent data. * * @param {String} storeName Name of the store. * @param {String} key Unique key associated with the data. * @param {Object} data The data that should be stored. */ - saveToStore(storeName: string, key: string, data: any): void; + saveToStore(storeName: string, key: string, data: any): Promise; /** - * React hook that observes a single store value and provides functions for updating the value. + * Callback for Changes in the store + * @callback subscribeHandler + * @param {String} storeName Name of the store. + * @param {String} key Unique key associated with the data. + * @param {Object} oldValue The old value that was in the store. + * @param {Object} newValue The new value that has been set in the store. + */ + /** + * Subscribe to Key changes in a store and get the new and old value * * @param {String} storeName Name of the store. * @param {String} key Unique key associated with the data. - * @return {[value, setValue(value), removeValue()]} An array to be deconstructed, - * The first element is the value, the second is a function to call to update the value, - * and the third is a function to call to clear the value. + * @param {subscribeHandler} handler Handler which is called with the old value and new value in the store. + * @returns {Function} Unsubscribe to changes in the store. */ - useStoreValue( + subscribeToChange( storeName: string, key: string, - ): [T | null, setValue, removeValue]; + handler: SubscribeToStoreHandler, + ): UnsubscribeFromStore; }; -export const userSettingsApiRef = createApiRef({ +export const storageApiRef = createApiRef({ id: 'core.user.settings', description: 'Provides the ability to modify settings that are personalised to the user', diff --git a/packages/core-api/src/apis/definitions/index.ts b/packages/core-api/src/apis/definitions/index.ts index 0acef2abb4..475dba9189 100644 --- a/packages/core-api/src/apis/definitions/index.ts +++ b/packages/core-api/src/apis/definitions/index.ts @@ -27,4 +27,4 @@ export * from './AppThemeApi'; export * from './ErrorApi'; export * from './FeatureFlagsApi'; export * from './OAuthRequestApi'; -export * from './UserSettingsApi'; +export * from './StorageApi'; diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts new file mode 100644 index 0000000000..c48a1ee931 --- /dev/null +++ b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { StorageApi } from '../../definitions'; + +export class WebStorage implements StorageApi { + async getFromStore( + storeName: string, + key: string = '', + defaultValue?: T, + ): Promise { + let store; + try { + store = JSON.parse(localStorage.getItem(storeName)!) || {}; + } catch (e) { + window.console.error( + `Error when parsing JSON config from storage for: ${storeName}`, + ); + return defaultValue; + } + + if (key) { + return store[key] ?? defaultValue; + } + return store; + } + + async saveToStore(storeName: string, key: string, data: any): Promise { + const store = JSON.parse(localStorage.getItem(storeName)!) || {}; + store[key] = data; + localStorage.setItem(storeName, JSON.stringify(store)); + } + + async removeFromStore(storeName: string, key: string): Promise { + const store = JSON.parse(localStorage.getItem(storeName)!) || {}; + delete store[key]; + localStorage.setItem(storeName, JSON.stringify(store)); + } +} diff --git a/packages/core-api/src/apis/implementations/StorageApi/index.ts b/packages/core-api/src/apis/implementations/StorageApi/index.ts new file mode 100644 index 0000000000..33b0094551 --- /dev/null +++ b/packages/core-api/src/apis/implementations/StorageApi/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +export { WebStorage } from './WebStorage'; diff --git a/packages/core-api/src/apis/implementations/UserSettingsApi/index.ts b/packages/core-api/src/apis/implementations/UserSettingsApi/index.ts deleted file mode 100644 index 8d73202414..0000000000 --- a/packages/core-api/src/apis/implementations/UserSettingsApi/index.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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 { useState, useEffect } from 'react'; -import { UserSettingsApi } from '../../definitions'; - -export class UserSettings implements UserSettingsApi { - getFromStore( - storeName: string, - key: string = '', - defaultValue: T | null, - ): T | null { - let store; - try { - store = JSON.parse(localStorage.getItem(storeName)!) || {}; - } catch (e) { - window.console.error( - `Error when parsing JSON config from storage for: ${storeName}`, - ); - return defaultValue as T; - } - - if (key) { - return (store[key] ?? defaultValue) as T; - } - return store as T; - } - - saveToStore(storeName: string, key: string, data: any): void { - const store = JSON.parse(localStorage.getItem(storeName)!) || {}; - store[key] = data; - localStorage.setItem(storeName, JSON.stringify(store)); - } - - removeFromStore(storeName: string, key: string): void { - const store = JSON.parse(localStorage.getItem(storeName)!) || {}; - delete store[key]; - localStorage.setItem(storeName, JSON.stringify(store)); - } - - useStoreValue( - storeName: string, - key: string, - ): [T | null, (value: T | null) => void, () => void] { - // We hardcode the emptyValue to null, since allowing objects or arrays would make - // the useEffect params a lot more complex and it would easy to get stuck in a loop. - const [value, setValue] = useState(() => - this.getFromStore(storeName, key, null), - ); - - // Listen to storage change events from other tabs - useEffect(() => { - const onChange = (event: StorageEvent) => { - if (event.key === storeName) { - // Avoid sending unnecessary updates when only the reference changes because of JSON.parse - setValue((currentValue: T | null) => { - const newValue = this.getFromStore(storeName, key, null); - - // Fastest way to do a deep equals, since we know references will differ, - // the values are serializable, and it's ok to signal a change if key order changes. - if (JSON.stringify(currentValue) === JSON.stringify(newValue)) { - return currentValue; - } - return newValue; - }); - } - }; - - window.addEventListener('storage', onChange); - return () => window.removeEventListener('storage', onChange); - }, [storeName, key]); - - const saveValue = (newValue: T | null) => { - this.saveToStore(storeName, key, newValue); - setValue(this.getFromStore(storeName, key, null)); - }; - - const removeValue = () => { - this.removeFromStore(storeName, key); - setValue(null); - }; - - return [value, saveValue, removeValue]; - } -}