init
This commit is contained in:
2
src/index.ts
Normal file
2
src/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './persist';
|
||||
export * from './memory-storage';
|
22
src/memory-storage.ts
Normal file
22
src/memory-storage.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Example storage backend. By default, persist will use localStorage however,
|
||||
* by implementing the Storage interface you can provide a custom backend to
|
||||
* store data anywhere you desire such as an external database or redis.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
export class MemoryStorage implements Storage {
|
||||
[name: string]: any;
|
||||
|
||||
get length() { return Object.keys(this).length; }
|
||||
|
||||
clear(): void { Object.keys(this).forEach(k => this.removeItem(k)); }
|
||||
|
||||
getItem(key: string): string | null { return this[key]; }
|
||||
|
||||
key(index: number): string | null { return Object.keys(this)[index]; }
|
||||
|
||||
removeItem(key: string): void { delete this[key]; }
|
||||
|
||||
setItem(key: string, value: string): void { this[key] = value; }
|
||||
}
|
144
src/persist.ts
Normal file
144
src/persist.ts
Normal file
@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Configurations persistence behaviour
|
||||
*
|
||||
* @group Options
|
||||
*/
|
||||
export type PersistOptions<T> = {
|
||||
/** Default/Initial value if undefined */
|
||||
default?: T,
|
||||
/** Storage implementation, defaults to LocalStorage */
|
||||
storage?: Storage,
|
||||
/** Force value to have prototype */
|
||||
type?: any,
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync variable's value with persistent storage (LocalStorage by default)
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const theme = new Persist('theme.current', {default: 'os'});
|
||||
* console.log(theme.value); // Output: os
|
||||
*
|
||||
* theme.value = 'light'; // Any changes to `.value` will automatically sync with localStorage
|
||||
*
|
||||
* location.reload(); // Simulate refresh
|
||||
* console.log(theme.value); // Output: light
|
||||
* ```
|
||||
*/
|
||||
export class Persist<T> {
|
||||
/** Private value field */
|
||||
private _value!: T;
|
||||
|
||||
/** Current value or default if undefined */
|
||||
get value(): T { return this._value !== undefined ? this._value : <T>this.options?.default; }
|
||||
|
||||
/** Set value with proxy object wrapper to sync future changes */
|
||||
set value(v: T | undefined) {
|
||||
if(v == null || typeof v != 'object') this._value = <T>v;
|
||||
// @ts-ignore
|
||||
else this._value = new Proxy<T>(v, {
|
||||
get: (target: T, p: string | symbol): any => {
|
||||
const f = typeof (<any>target)[p] == 'function';
|
||||
if(!f) return (<any>target)[p];
|
||||
return (...args: any[]) => {
|
||||
const value = (<any>target)[p](...args);
|
||||
this.save();
|
||||
return value;
|
||||
};
|
||||
},
|
||||
set: (target: T, p: string | symbol, newValue: any): boolean => {
|
||||
(<any>target)[p] = newValue;
|
||||
this.save();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
this.save();
|
||||
}
|
||||
|
||||
/** Where data gets physically stored */
|
||||
private readonly storage!: Storage;
|
||||
/** Listeners which should be notified on changes */
|
||||
private watches: { [key: string]: Function } = {};
|
||||
|
||||
/**
|
||||
* @param {string} key Unique key value will be stored under
|
||||
* @param {PersistOptions<T>} options Configure using {@link PersistOptions}
|
||||
*/
|
||||
constructor(public readonly key: string, public options: PersistOptions<T> = {}) {
|
||||
this.storage = options.storage || localStorage;
|
||||
this.load();
|
||||
}
|
||||
|
||||
/** Delete value from storage */
|
||||
clear() {
|
||||
this.storage.removeItem(this.key);
|
||||
}
|
||||
|
||||
/** Save current value to storage */
|
||||
save() {
|
||||
if(this._value === undefined) this.clear();
|
||||
else this.storage.setItem(this.key, JSON.stringify(this._value));
|
||||
this.notify(this.value);
|
||||
}
|
||||
|
||||
/** Load value from storage */
|
||||
load() {
|
||||
if(this.storage[this.key] != undefined) {
|
||||
let value = JSON.parse(<string>this.storage.getItem(this.key));
|
||||
if(value != null && typeof value == 'object' && this.options.type) value.__proto__ = this.options.type.prototype;
|
||||
this.value = value;
|
||||
} else this.value = this.options.default || <T>undefined;
|
||||
}
|
||||
|
||||
/** Callback to listen for changes */
|
||||
watch(fn: (value: T) => any): () => void {
|
||||
const index = Object.keys(this.watches).length;
|
||||
this.watches[index] = fn;
|
||||
return () => { delete this.watches[index]; };
|
||||
}
|
||||
|
||||
/** Return value as JSON string */
|
||||
toString() { return JSON.stringify(this.value); }
|
||||
|
||||
/** Return raw value */
|
||||
valueOf() { return this.value?.valueOf(); }
|
||||
|
||||
/** Notify listeners of change */
|
||||
private notify(value: T) {
|
||||
Object.values(this.watches).forEach(watch => watch(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync class property with persistent storage (LocalStorage by default)
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* class ThemeEngine {
|
||||
* @persist({default: 'os'}) current!: string;
|
||||
* }
|
||||
*
|
||||
* const theme = new ThemeEngine();
|
||||
* console.log(theme.current) // Output: os
|
||||
*
|
||||
* theme.current = 'light'; //Any changes will be automatically saved to localStorage
|
||||
*
|
||||
* location.reload(); // Simulate refresh
|
||||
* console.log(theme.current) // Output: light
|
||||
* ```
|
||||
*
|
||||
* @group Decorators
|
||||
* @param {PersistOptions<T> & {key?: string}} options Configure using {@link PersistOptions}
|
||||
* @returns Decorator function
|
||||
*/
|
||||
export function persist<T>(options?: PersistOptions<T> & { key?: string }) {
|
||||
return (target: any, prop: any) => {
|
||||
const key = options?.key || `${target.constructor.name}.${prop.toString()}`;
|
||||
const wrapper = new Persist(key, options);
|
||||
Object.defineProperty(target, prop, {
|
||||
get: function() { return wrapper.value; },
|
||||
set: function(v) { wrapper.value = v; }
|
||||
});
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user