/** * Base store classes and utilities for consistent store patterns * * This module provides: * - BaseServerStore: For stores that fetch data from APIs * - BaseClientStore: For stores that manage UI/client state * - Common patterns for all stores */ import { makeObservable, action } from "mobx"; import { RequestDeduplicator, StoreError } from "./store-utils"; /** * Base interface for all store states * Ensures all stores have a consistent setPartial method */ export interface BaseState { setPartial(partial: Partial): void; } /** * Base class for server state stores (data fetching) * * Server stores: * - Fetch data from APIs * - Cache responses in memory * - Handle errors with StoreError * - Support request deduplication * * @example * class MemoState implements BaseState { * memoMapByName: Record = {}; * constructor() { makeAutoObservable(this); } * setPartial(partial: Partial) { Object.assign(this, partial); } * } * * const store = createServerStore(new MemoState()); */ export interface ServerStoreConfig { /** * Enable request deduplication * Prevents multiple identical requests from running simultaneously */ enableDeduplication?: boolean; /** * Store name for debugging and error messages */ name: string; } /** * Create a server store with built-in utilities */ export function createServerStore(state: TState, config: ServerStoreConfig) { const deduplicator = config.enableDeduplication !== false ? new RequestDeduplicator() : null; return { state, deduplicator, name: config.name, /** * Wrap an async operation with error handling and optional deduplication */ async executeRequest(key: string, operation: () => Promise, errorCode?: string): Promise { try { if (deduplicator && key) { return await deduplicator.execute(key, operation); } return await operation(); } catch (error) { if (StoreError.isAbortError(error)) { throw error; // Re-throw abort errors as-is } throw StoreError.wrap(errorCode || `${config.name.toUpperCase()}_OPERATION_FAILED`, error); } }, }; } /** * Base class for client state stores (UI state) * * Client stores: * - Manage UI preferences and transient state * - May persist to localStorage or URL * - No API calls * - Instant updates * * @example * class ViewState implements BaseState { * orderByTimeAsc = false; * layout: "LIST" | "MASONRY" = "LIST"; * constructor() { makeAutoObservable(this); } * setPartial(partial: Partial) { * Object.assign(this, partial); * localStorage.setItem("view", JSON.stringify(this)); * } * } */ export interface ClientStoreConfig { /** * Store name for debugging */ name: string; /** * Enable localStorage persistence */ persistence?: { key: string; serialize?: (state: any) => string; deserialize?: (data: string) => any; }; } /** * Create a client store with optional persistence */ export function createClientStore(state: TState, config: ClientStoreConfig) { // Load from localStorage if enabled if (config.persistence) { try { const cached = localStorage.getItem(config.persistence.key); if (cached) { const data = config.persistence.deserialize ? config.persistence.deserialize(cached) : JSON.parse(cached); Object.assign(state, data); } } catch (error) { console.warn(`Failed to load ${config.name} from localStorage:`, error); } } return { state, name: config.name, /** * Save state to localStorage if persistence is enabled */ persist(): void { if (config.persistence) { try { const data = config.persistence.serialize ? config.persistence.serialize(state) : JSON.stringify(state); localStorage.setItem(config.persistence.key, data); } catch (error) { console.warn(`Failed to persist ${config.name}:`, error); } } }, /** * Clear persisted state */ clearPersistence(): void { if (config.persistence) { localStorage.removeItem(config.persistence.key); } }, }; } /** * Standard state class implementation * Use this as a base for your state classes */ export abstract class StandardState implements BaseState { constructor() { makeObservable(this, { setPartial: action, }); } setPartial(partial: Partial): void { Object.assign(this, partial); } }