UNPKG

@chromahq/store

Version:

Centralized, persistent store for Chrome extensions using zustand, accessible from service workers and React, with chrome.storage.local persistence.

244 lines (234 loc) 10.2 kB
import { StateCreator, StoreApi } from 'zustand'; import * as React from 'react'; import { ReactNode } from 'react'; import { StateCreator as StateCreator$1 } from 'zustand/vanilla'; type PersistOptions = { name: string; version?: number; migrate?: (state: any, version: number) => any; }; interface StoreDefinition { name: string; slices?: StateCreator<any, [], [], any>[]; persistence?: PersistOptions; config?: Record<string, any>; } type ExtractSliceState<T> = T extends StateCreator<infer State, any, any, any> ? State : never; type SliceCreator<T> = StateCreator<T, [], [], T>; interface StoreConfig<T> { slices: readonly StateCreator<any, [], [], any>[]; persist?: PersistOptions; } type MergeSlices<Slices extends readonly StateCreator<any, [], [], any>[]> = Slices extends readonly [infer First, ...infer Rest] ? First extends StateCreator<any, [], [], infer FirstState> ? Rest extends readonly StateCreator<any, [], [], any>[] ? FirstState & MergeSlices<Rest> : FirstState : {} : {}; interface CentralStore<T> extends StoreApi<T> { isReady: () => boolean; onReady: (callback: () => void) => () => void; reset: () => void; } declare function chromeStoragePersist<S>(options?: PersistOptions & { onReady?: () => void; }): (config: StateCreator<S>) => StateCreator<S>; declare function useCentralStore<T, U = T>(store: CentralStore<T>, selector: (state: T) => U): U; declare function useCentralDispatch<T>(store: CentralStore<T>): { (partial: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: false): void; (state: T | ((state: T) => T), replace: true): void; }; /** * React hook to check if the store is ready (fully loaded from persistence/bridge) */ declare function useStoreReady<T>(store: CentralStore<T>): boolean; /** * React hook to get the reset function for a store */ declare function useStoreReset<T>(store: CentralStore<T>): () => void; interface Bridge { send: <Req = unknown, Res = unknown>(key: string, payload?: Req, timeoutDuration?: number) => Promise<Res>; isConnected: boolean; } interface BridgeWithEvents extends Bridge { on?: (key: string, handler: (payload: any) => void) => void; off?: (key: string, handler: (payload: any) => void) => void; } interface BridgeWithHandlers extends Bridge { register: (key: string, handler: (payload?: any) => any) => void; broadcast: (key: string, payload: any) => void; on?: (key: string, handler: (payload: any) => void) => void; off?: (key: string, handler: (payload: any) => void) => void; } declare class BridgeStore<T> implements CentralStore<T> { private bridge; private listeners; private currentState; private previousState; private initialState; private storeName; private ready; private readyCallbacks; private initializationAttempts; private readonly maxInitializationAttempts; private initializationTimer; private isInitializing; private reconnectHandler; private disconnectHandler; private stateChangedHandler; private stateSyncDebounceTimer; private readonly stateSyncDebounceMs; private reconnectDelayTimer; private visibilityHandler; private lastVisibleAt; private readonly staleThresholdMs; constructor(bridge: BridgeWithEvents, initialState?: T, storeName?: string, readyCallbacks?: Set<() => void>); private setupReconnectListener; /** * Re-register all event listeners on the bridge * Called after reconnection because React StrictMode may have created a new eventListenersRef * IMPORTANT: Remove existing listeners first to prevent duplicate handlers */ private reregisterEventListeners; private setupVisibilityListener; initialize: () => Promise<void>; private stateSyncSequence; private pendingStateSync; /** * Apply state directly from broadcast payload (no round-trip) */ private applyBroadcastState; /** * Fetch state from SW (fallback when broadcast doesn't include payload) */ private fetchAndApplyState; private setupStateSync; private notifyListeners; getState: () => T; setState(partial: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: false): void; setState(state: T | ((state: T) => T), replace: true): void; private applyOptimisticUpdate; subscribe: (listener: (state: T, prevState: T) => void) => (() => void); destroy: () => void; getInitialState: () => T; isReady: () => boolean; onReady: (callback: () => void) => (() => void); reset: () => void; private notifyReady; /** * Force re-initialization of the store (useful for debugging or after reconnection) */ forceInitialize: () => Promise<void>; /** * Get debug information about the store state */ getDebugInfo: () => { storeName: string; ready: boolean; isInitializing: boolean; bridgeConnected: boolean; hasCurrentState: boolean; hasInitialState: boolean; readyCallbacksCount: number; initializationAttempts: number; maxInitializationAttempts: number; }; /** * Update the bridge reference and re-register all event listeners. * Called when createBridgeStore receives a new bridge object (e.g., after React remount). * This is critical for React StrictMode which causes double-mounting. */ updateBridge: (newBridge: BridgeWithEvents) => void; } declare function createBridgeStore<T>(bridge: BridgeWithEvents, initialState?: T, storeName?: string, readyCallbacks?: Set<() => void>): CentralStore<T>; declare function clearStoreCache(): void; /** * Destroy a specific store and remove it from cache. * Call this when a store is no longer needed to free memory. * @param storeName - The name of the store to destroy */ declare function destroyStore(storeName: string): void; /** * Generic action hook factory for any store instance. * Usage: * const store = createStore(mergeSlices(sliceA, sliceB)); * <StoreProvider store={store}> ... </StoreProvider> * export const useWalletActions = createActionHookForStore(store, walletActions); * export const useCounterActions = createActionHookForStore(store, counterActions); * All hooks and actions share the same StoreProvider/context. */ declare function createActionHookForStore<S extends Record<string, any>, ActionMap extends Record<string, (...args: any[]) => void>>(store: CentralStore<S>, actionsFactory: (actions: ReturnType<typeof useStoreActions<S>>) => ActionMap): () => ActionMap; /** * Store actions helper: exposes update, updateWith, replace, setState * Actions should be defined in your slice and accessed via useActions. */ declare function useStoreActions<T extends Record<string, any>>(store: CentralStore<T>): { update: (partial: Partial<T>) => void; updateWith: (updater: (state: T) => Partial<T>) => void; replace: (newState: T) => void; setState: { (partial: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: false): void; (state: T | ((state: T) => T), replace: true): void; }; }; declare function createStoreHooks<T extends Record<string, any>>(): { createActionHook: <ActionMap extends Record<string, (...args: any[]) => void>>(actionsFactory: (actions: ReturnType<typeof useStoreActions<T>>) => ActionMap) => () => ActionMap; StoreProvider: ({ store, children }: { store: CentralStore<T>; children: ReactNode; }) => React.FunctionComponentElement<React.ProviderProps<CentralStore<T> | null>>; useStore: <U>(selector: (state: T) => U) => U; useStoreInstance: () => CentralStore<T>; useActions: () => { update: (partial: Partial<T>) => void; updateWith: (updater: (state: T) => Partial<T>) => void; replace: (newState: T) => void; setState: { (partial: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: false): void; (state: T | ((state: T) => T), replace: true): void; }; }; useAction: <K extends { [K_1 in keyof T]: T[K_1] extends (...args: any[]) => any ? K_1 : never; }[keyof T]>(actionKey: K) => T[K]; }; /** * Core store builder with fluent API */ declare class StoreBuilder<T = any> { private config; private onReadyCallbacks; constructor(name?: string); /** * Add state slices to the store */ withSlices(...slices: StateCreator$1<any, [], [], any>[]): this; onReady(callback: () => void): this; /** * Attach a bridge for cross-context communication */ withBridge(bridge?: BridgeWithEvents): this; /** * Enable persistence with Chrome storage */ withPersistence(options?: PersistOptions): this; /** * Create the store */ create(): Promise<CentralStore<T>>; private createBaseStore; private createServiceWorkerStore; } /** * Create a new store builder */ declare function createStore<T = any>(name?: string): StoreBuilder<T>; /** * Create a service worker store directly (convenience function) * This creates a store optimized for service worker context with persistence */ declare function createServiceWorkerStore<T = any>(slices: StateCreator$1<T, [], [], T>[] | StateCreator$1<T, [], [], T>, name?: string, persistOptions?: PersistOptions): Promise<CentralStore<T>>; /** * Create a bridge store directly (convenience function) * This creates a store optimized for UI context that connects to service worker */ declare function createUIStore<T = any>(bridge: BridgeWithEvents, initialState?: T, name?: string): CentralStore<T>; /** * Initialize a store from a store definition */ declare function init(storeDefinition: StoreDefinition): Promise<any>; export { BridgeStore, StoreBuilder, chromeStoragePersist, clearStoreCache, createActionHookForStore, createBridgeStore, createServiceWorkerStore, createStore, createStoreHooks, createUIStore, destroyStore, init, useCentralDispatch, useCentralStore, useStoreReady, useStoreReset }; export type { Bridge, BridgeWithEvents, BridgeWithHandlers, CentralStore, ExtractSliceState, MergeSlices, PersistOptions, SliceCreator, StoreConfig, StoreDefinition };