crann
Version:
Effortless State Synchronization for Web Extensions
198 lines (197 loc) • 7.29 kB
TypeScript
/**
* 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>;