Преглед изворни кода

#6372: Implement basic state management with localStorage integration

checktheroads пре 4 година
родитељ
комит
d9a6f11c35
2 измењених фајлова са 165 додато и 0 уклоњено
  1. 5 0
      netbox/project-static/src/global.d.ts
  2. 160 0
      netbox/project-static/src/state/index.ts

+ 5 - 0
netbox/project-static/src/global.d.ts

@@ -6,6 +6,11 @@ type Dict<T extends unknown = unknown> = Record<string, T>;
 
 
 type Nullable<T> = T | null;
 type Nullable<T> = T | null;
 
 
+/**
+ * Enforce string index type (not `number` or `symbol`).
+ */
+type Index<O extends Dict, K extends keyof O> = K extends string ? K : never;
+
 type APIAnswer<T> = {
 type APIAnswer<T> = {
   count: number;
   count: number;
   next: Nullable<string>;
   next: Nullable<string>;

+ 160 - 0
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<T extends Dict, K extends keyof T = keyof T> implements ProxyHandler<T> {
+  public set<S extends Index<T, K>>(target: T, key: S, value: T[S]): boolean {
+    target[key] = value;
+    return true;
+  }
+
+  public get<G extends Index<T, K>>(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<T extends Dict, K extends keyof T = keyof T> {
+  /**
+   * implemented `ProxyHandler` for the underlying `Proxy` object.
+   */
+  private handlers: ProxyStateHandler<T>;
+  /**
+   * 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<T>();
+    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<G extends Index<T, K>>(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<G extends Index<T, K>>(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<T extends Dict>(
+  initial: T,
+  options: StateOptions = {},
+): StateManager<T> {
+  return new StateManager<T>(initial, options);
+}