From d9a6f11c3504a333bb92fa6155802ed11dc94dba Mon Sep 17 00:00:00 2001 From: checktheroads Date: Tue, 6 Jul 2021 17:55:13 -0700 Subject: [PATCH] #6372: Implement basic state management with localStorage integration --- netbox/project-static/src/global.d.ts | 5 + netbox/project-static/src/state/index.ts | 160 +++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 netbox/project-static/src/state/index.ts diff --git a/netbox/project-static/src/global.d.ts b/netbox/project-static/src/global.d.ts index 4b90d68f5..a249071f2 100644 --- a/netbox/project-static/src/global.d.ts +++ b/netbox/project-static/src/global.d.ts @@ -6,6 +6,11 @@ type Dict = Record; type Nullable = T | null; +/** + * Enforce string index type (not `number` or `symbol`). + */ +type Index = K extends string ? K : never; + type APIAnswer = { count: number; next: Nullable; diff --git a/netbox/project-static/src/state/index.ts b/netbox/project-static/src/state/index.ts new file mode 100644 index 000000000..3fcdb5889 --- /dev/null +++ b/netbox/project-static/src/state/index.ts @@ -0,0 +1,160 @@ +/** + * `StateManger` configuration options. + */ +interface StateOptions { + /** + * If true, all values will be written to localStorage when calling `set()`. Additionally, when + * a new state instance is initialized, if the same localStorage state key (see `key` property) + * exists in localStorage, the value will be read and used as the initial value. + */ + persist?: boolean; +} + +/** + * Typed implementation of native `ProxyHandler`. + */ +class ProxyStateHandler implements ProxyHandler { + public set>(target: T, key: S, value: T[S]): boolean { + target[key] = value; + return true; + } + + public get>(target: T, key: G): T[G] { + return target[key]; + } + public has(target: T, key: string): boolean { + return key in target; + } +} + +/** + * Manage runtime and/or locally stored (via localStorage) state. + */ +export class StateManager { + /** + * implemented `ProxyHandler` for the underlying `Proxy` object. + */ + private handlers: ProxyStateHandler; + /** + * Underlying `Proxy` object for this instance. + */ + private proxy: T; + /** + * Options for this instance. + */ + private options: StateOptions; + /** + * localStorage key for this instance. + */ + private key: string = ''; + + constructor(raw: T, options: StateOptions) { + this.key = this.generateStateKey(raw); + + this.options = options; + + if (this.options.persist) { + const saved = this.retrieve(); + if (saved !== null) { + raw = { ...raw, ...saved }; + } + } + + this.handlers = new ProxyStateHandler(); + this.proxy = new Proxy(raw, this.handlers); + + if (this.options.persist) { + this.save(); + } + } + + /** + * Generate a semi-unique localStorage key for this instance. + */ + private generateStateKey(obj: T): string { + const encoded = window.btoa(Object.keys(obj).join('---')); + return `netbox-${encoded}`; + } + + /** + * Get the current value of `key`. + * + * @param key Object key name. + * @returns Object value. + */ + public get>(key: G): T[G] { + return this.handlers.get(this.proxy, key); + } + + /** + * Set a new value for `key`. + * + * @param key Object key name. + * @param value New value. + */ + public set>(key: G, value: T[G]): void { + this.handlers.set(this.proxy, key, value); + if (this.options.persist) { + this.save(); + } + } + + /** + * Access the full instance. + * + * @returns StateManager instance. + */ + public all(): T { + return this.proxy; + } + + /** + * Access all state keys. + */ + public keys(): K[] { + return Object.keys(this.proxy) as K[]; + } + + /** + * Access all state values. + */ + public values(): T[K][] { + return Object.values(this.proxy) as T[K][]; + } + + /** + * Serialize and save the current state to localStorage. + */ + private save(): void { + const value = JSON.stringify(this.proxy); + localStorage.setItem(this.key, value); + } + + /** + * Retrieve the serialized state object from localStorage. + * + * @returns Parsed state object. + */ + private retrieve(): T | null { + const raw = localStorage.getItem(this.key); + if (raw !== null) { + const data = JSON.parse(raw) as T; + return data; + } + return null; + } +} + +/** + * Create a new state object. Only one instance should exist at runtime for a given state. + * + * @param initial State's initial value. + * @param options State management instance options. + * @returns State management instance. + */ +export function createState( + initial: T, + options: StateOptions = {}, +): StateManager { + return new StateManager(initial, options); +}