UNPKG

crann

Version:

Effortless State Synchronization for Web Extensions

198 lines (197 loc) 7.29 kB
/** * Crann v2 Store * * The central state hub that runs in the service worker. * This is NOT a singleton - each call to createStore() returns a new instance. */ import type { BrowserLocation } from "../transport"; import { ConfigSchema, ValidatedConfig, StoreOptions, DerivedState, DerivedSharedState, DerivedAgentState, StateChangeListener, AgentConnectionInfo } from "./types"; export declare class Store<TConfig extends ConfigSchema> { private readonly config; private readonly options; private readonly porter; private readonly stateManager; private readonly persistence; private readonly agentRegistry; private readonly actionExecutor; private readonly stateChangeListeners; private readonly agentConnectListeners; private readonly agentDisconnectListeners; private isDestroyed; constructor(config: ValidatedConfig<TConfig>, options?: StoreOptions); /** * Get the current shared state (state visible to all agents). * * @returns A snapshot of the current shared state * @throws {LifecycleError} If the store has been destroyed * * @example * const state = store.getState(); * console.log(state.count); */ getState(): DerivedState<TConfig>; /** * Get agent-scoped state for a specific agent. * * @param agentId - The unique identifier of the agent * @returns The agent-scoped state for the specified agent * @throws {LifecycleError} If the store has been destroyed * * @example * store.onAgentConnect((agent) => { * const agentState = store.getAgentState(agent.id); * }); */ getAgentState(agentId: string): DerivedAgentState<TConfig>; /** * Update shared state. Changes are persisted and broadcast to all agents. * * @param state - Partial state to merge with current shared state * @throws {LifecycleError} If the store has been destroyed * * @example * await store.setState({ count: 5 }); */ setState(state: Partial<DerivedSharedState<TConfig>>): Promise<void>; /** * Update state for a specific agent. Can include both shared and agent-scoped state. * * @param state - Partial state to merge * @param agentId - The agent to update (for agent-scoped state) * @throws {LifecycleError} If the store has been destroyed * * @example * await store.setState({ count: 5, agentData: 'custom' }, agentId); */ setState(state: Partial<DerivedState<TConfig>>, agentId: string): Promise<void>; /** * Update agent-scoped state for a specific agent. * * @param agentId - The unique identifier of the agent * @param state - Partial agent-scoped state to merge * @throws {LifecycleError} If the store has been destroyed * * @example * await store.setAgentState(agentId, { agentData: 'custom' }); */ setAgentState(agentId: string, state: Partial<DerivedAgentState<TConfig>>): Promise<void>; /** * Clear all state back to defaults and remove persisted data. * * @throws {LifecycleError} If the store has been destroyed * * @example * await store.clear(); */ clear(): Promise<void>; /** * Subscribe to state changes. Called whenever state is updated. * * Can subscribe to all changes or filter by specific keys. * * @param callbackOrKeys - Either a callback for all changes, or an array of keys to filter by * @param maybeCallback - Callback when first arg is keys array * @returns Unsubscribe function * @throws {LifecycleError} If the store has been destroyed * * @example * // Subscribe to all changes * const unsubscribe = store.subscribe((state, changes, agent) => { * console.log('State changed:', changes); * }); * * // Subscribe to specific keys only * const unsubscribe = store.subscribe(['active', 'initialized'], (state, changes, agent) => { * // Only called when 'active' or 'initialized' change * console.log('Relevant state changed:', changes); * }); */ subscribe(callbackOrKeys: StateChangeListener<TConfig> | Array<keyof DerivedState<TConfig>>, maybeCallback?: StateChangeListener<TConfig>): () => void; /** * Register callback for when an agent connects. * * @param callback - Called with agent connection info * @returns Unsubscribe function * @throws {LifecycleError} If the store has been destroyed * * @example * store.onAgentConnect((agent) => { * console.log(`Agent ${agent.id} connected from tab ${agent.tabId}`); * }); */ onAgentConnect(callback: (agent: AgentConnectionInfo) => void): () => void; /** * Register callback for when an agent disconnects. * * @param callback - Called with agent connection info * @returns Unsubscribe function * @throws {LifecycleError} If the store has been destroyed * * @example * store.onAgentDisconnect((agent) => { * console.log(`Agent ${agent.id} disconnected`); * }); */ onAgentDisconnect(callback: (agent: AgentConnectionInfo) => void): () => void; /** * Get all connected agents, optionally filtered by location. * * @param query - Optional filter by context, tabId, frameId * @returns Array of connected agent info * @throws {LifecycleError} If the store has been destroyed * * @example * const allAgents = store.getAgents(); * const contentScripts = store.getAgents({ context: 'contentscript' }); * const tab42Agents = store.getAgents({ tabId: 42 }); */ getAgents(query?: Partial<BrowserLocation>): AgentConnectionInfo[]; /** * Destroy the store and clean up all resources. * After calling this, the store cannot be used. Use for testing or HMR cleanup. * * @param options.clearPersisted - If true, also clears persisted storage data * * @example * // In tests * afterEach(() => { * store.destroy({ clearPersisted: true }); * }); */ destroy(options?: { clearPersisted?: boolean; }): void; private setupMessageHandlers; private setupRpcHandlers; private hydrate; private notifyStateChange; private assertNotDestroyed; } /** * Create a new Crann store instance. * * This should be called in your service worker/background script. * Each call creates a new, independent store instance (no singleton). * * @param config - Validated config from createConfig() * @param options - Optional store options * @param options.debug - Enable debug logging * @param options.migrate - Migration function for schema version changes * @returns A new Store instance * * @example * // background.ts * import { createConfig, createStore } from 'crann'; * * const config = createConfig({ * name: 'myFeature', * count: { default: 0, persist: 'local' }, * }); * * const store = createStore(config, { debug: true }); * * store.subscribe((state, changes) => { * console.log('State changed:', changes); * }); */ export declare function createStore<TConfig extends ConfigSchema>(config: ValidatedConfig<TConfig>, options?: StoreOptions): Store<TConfig>;