feat(core-api/Storage): Reworking the StorageApi to have a better interface

This commit is contained in:
blam
2020-05-29 13:11:06 +02:00
parent 9437832616
commit 335d6f36b7
5 changed files with 104 additions and 120 deletions
@@ -16,14 +16,22 @@
import { createApiRef } from '../ApiRef';
type setValue<T> = (value: T | null) => void;
type removeValue = () => void;
export type UserSettingsApi = {
type UnsubscribeFromStore = () => void;
type SubscribeToStoreHandler<T> = ({
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<T>(
storeName: string,
key: string,
defaultValue: T | null,
): T | null;
defaultValue?: T,
): Promise<T | undefined>;
/**
* 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<void>;
/**
* 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<void>;
/**
* 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<T>(
subscribeToChange<T>(
storeName: string,
key: string,
): [T | null, setValue<T | null>, removeValue];
handler: SubscribeToStoreHandler<T>,
): UnsubscribeFromStore;
};
export const userSettingsApiRef = createApiRef<UserSettingsApi>({
export const storageApiRef = createApiRef<StorageApi>({
id: 'core.user.settings',
description:
'Provides the ability to modify settings that are personalised to the user',
@@ -27,4 +27,4 @@ export * from './AppThemeApi';
export * from './ErrorApi';
export * from './FeatureFlagsApi';
export * from './OAuthRequestApi';
export * from './UserSettingsApi';
export * from './StorageApi';
@@ -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<T>(
storeName: string,
key: string = '',
defaultValue?: T,
): Promise<T | undefined> {
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<void> {
const store = JSON.parse(localStorage.getItem(storeName)!) || {};
store[key] = data;
localStorage.setItem(storeName, JSON.stringify(store));
}
async removeFromStore(storeName: string, key: string): Promise<void> {
const store = JSON.parse(localStorage.getItem(storeName)!) || {};
delete store[key];
localStorage.setItem(storeName, JSON.stringify(store));
}
}
@@ -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';
@@ -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<T>(
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<T>(
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<T | null>(() =>
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];
}
}