1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

persist local-storage state in external json-file

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2021-03-04 15:18:35 +02:00
parent 852aa1147f
commit 597370ae07
2 changed files with 182 additions and 45 deletions

View File

@ -1,7 +1,7 @@
import { useState } from "react";
import { createStorage, IStorageHelperOptions } from "../utils";
import { createStorage, StorageHelperOptions } from "../utils";
export function useStorage<T>(key: string, initialValue?: T, options?: IStorageHelperOptions) {
export function useStorage<T>(key: string, initialValue?: T, options?: StorageHelperOptions<T>) {
const storage = createStorage(key, initialValue, options);
const [storageValue, setStorageValue] = useState(storage.get());
const setValue = (value: T) => {

View File

@ -1,73 +1,210 @@
// Helper to work with browser's local/session storage api
// Helper for work with persistent local storage (default: window.localStorage)
// TODO: write unit/integration tests
export interface IStorageHelperOptions {
addKeyPrefix?: boolean;
useSession?: boolean; // use `sessionStorage` instead of `localStorage`
}
import { app, remote } from "electron";
import path from "path";
import { ensureDirSync, readJsonSync, writeJson } from "fs-extra";
import type { CreateObservableOptions } from "mobx/lib/api/observable";
import { action, comparer, observable, reaction, toJS, when } from "mobx";
import produce, { Draft, setAutoFreeze } from "immer";
import { isEqual, isFunction, isObject } from "lodash";
export function createStorage<T>(key: string, defaultValue?: T, options?: IStorageHelperOptions) {
setAutoFreeze(false); // allow to merge observables
export function createStorage<T>(key: string, defaultValue?: T, options?: StorageHelperOptions<T>) {
return new StorageHelper(key, defaultValue, options);
}
export class StorageHelper<T> {
static keyPrefix = "lens_";
export interface StorageHelperOptions<T> extends StorageConfiguration<T> {
autoInit?: boolean; // default: true
}
static defaultOptions: IStorageHelperOptions = {
addKeyPrefix: true,
useSession: false,
export interface StorageConfiguration<T> {
storage?: StorageAdapter<T>;
observable?: CreateObservableOptions;
}
export interface StorageAdapter<T> {
[metadata: string]: any;
getItem(key: string): T; // import
setItem(key: string, value: T): void; // export
removeItem?(key: string): void; // if not provided setItem(key,undefined) will be used
onChange?(data: { key: string, value: T, oldValue?: T }): void;
}
export class StorageHelper<T> {
static defaultOptions: StorageHelperOptions<any> = {
autoInit: true,
get storage() {
return jsonFileSyncStorageAdapter;
},
observable: {
deep: true,
equals: comparer.shallow,
}
};
constructor(protected key: string, protected defaultValue?: T, protected options?: IStorageHelperOptions) {
this.options = Object.assign({}, StorageHelper.defaultOptions, options);
private data = observable.box<T>();
@observable.ref storage: StorageAdapter<T>;
@observable initialized = false;
whenReady = when(() => this.initialized);
if (this.options.addKeyPrefix) {
this.key = StorageHelper.keyPrefix + key;
constructor(readonly key: string, readonly defaultValue?: T, readonly options: StorageHelperOptions<T> = {}) {
this.options = { ...StorageHelper.defaultOptions, ...options };
this.configure();
this.reset();
if (this.options.autoInit) {
this.init();
}
}
protected get storage() {
if (this.options.useSession) return window.sessionStorage;
@action
init() {
if (this.initialized) return;
return window.localStorage;
try {
const value = this.load();
const notEmpty = value != null;
const notDefault = !this.isDefaultValue(value);
if (notEmpty && notDefault) {
this.merge(value);
}
this.initialized = true;
} catch (error) {
console.error(`[init]: ${error}`, this);
}
}
isDefaultValue(value: T): boolean {
return isEqual(value, this.defaultValue);
}
@action
private configure({ storage, observable }: StorageConfiguration<T> = this.options): this {
if (storage) this.storage = storage;
if (observable) this.configureObservable(observable);
return this;
}
@action
private configureObservable(options: CreateObservableOptions = {}) {
this.data = observable.box<T>(this.data.get(), {
...StorageHelper.defaultOptions.observable, // inherit default observability options
...options,
});
this.data.observe(change => {
const { newValue, oldValue } = toJS(change, { recurseEverything: true });
this.onChange(newValue, oldValue);
});
}
protected onChange(value: T, oldValue?: T) {
if (!this.initialized) return;
this.storage.onChange?.({ value, oldValue, key: this.key });
this.storage.setItem(this.key, value);
}
load(): T {
return this.storage.getItem(this.key);
}
get(): T {
const strValue = this.storage.getItem(this.key);
if (strValue != null) {
try {
return JSON.parse(strValue);
} catch (e) {
console.error(`Parsing json failed for pair: ${this.key}=${strValue}`);
}
}
return this.defaultValue;
return this.data.get();
}
set(value: T) {
this.storage.setItem(this.key, JSON.stringify(value));
return this;
this.data.set(value);
}
merge(value: Partial<T>) {
const currentValue = this.get();
return this.set(Object.assign(currentValue, value));
reset() {
this.set(this.defaultValue);
}
clear() {
this.storage.removeItem(this.key);
return this;
this.data.set(null);
}
getDefaultValue() {
return this.defaultValue;
merge(value: Partial<T> | ((draft: Draft<T>) => Partial<T> | void)) {
const updater = isFunction(value) ? value : (state: Draft<T>) => {
if (isObject(state)) return Object.assign(state, value);
return value;
};
const currentValue = this.toJS();
const nextValue = produce(currentValue, updater) as T;
this.set(nextValue);
}
restoreDefaultValue() {
return this.set(this.defaultValue);
toJS() {
return toJS(this.get(), { recurseEverything: true });
}
}
export const localStorageAdapter: StorageAdapter<object> = {
getItem(key: string) {
return JSON.parse(localStorage.getItem(key));
},
setItem(key: string, value: any) {
localStorage.setItem(key, JSON.stringify(value));
},
removeItem(key: string) {
localStorage.removeItem(key);
}
};
// TODO: remove after merge https://github.com/lensapp/lens/pull/2269
export const jsonFileSyncStorageAdapter: StorageAdapter<object> = {
cwd: path.resolve((app || remote.app).getPath("userData"), "lens-local-storage"),
data: observable.map<string, any>([], { deep: false }),
initialized: false,
get filePath() {
const clusterId = location.hostname.split(".").slice(-2, -1)[0];
return path.resolve(this.cwd, `${clusterId ?? "app"}.json`);
},
init() {
if (this.initialized) return;
try {
ensureDirSync(this.cwd, { mode: 0o755 });
const data = readJsonSync(this.filePath, { throws: false }) || {};
this.data.replace(data);
this.bindAutoSave();
} catch (error) {
console.error(`[init]: ${this.filePath} failed: ${error}`, this);
} finally {
this.initialized = true;
}
},
bindAutoSave() {
return reaction(() => this.data.toJSON(), async (data) => {
try {
await writeJson(this.filePath, data, { spaces: 2 });
} catch (error) {
console.error(`[save]: ${this.filePath} failed: ${error}`, this);
}
});
},
getItem(key: string) {
this.init();
return this.data.get(key);
},
setItem(key: string, value: any) {
this.data.set(key, value);
},
removeItem(key: string) {
this.data.delete(key);
}
};