crann
Version:
Effortless State Synchronization for Web Extensions
177 lines (176 loc) • 6.04 kB
TypeScript
/**
* Crann v2 Type Definitions
*
* This file defines the new config schema and derived types for v2.
* Key changes from v1:
* - `name` and `version` are part of config (single source of truth)
* - `scope: 'shared' | 'agent'` replaces `partition: 'service' | 'instance'`
* - Actions are nested under `actions:` key
* - Action handlers receive a context object
*/
import { AgentInfo, BrowserLocation } from "../transport";
export declare const Scope: {
Shared: "shared";
Agent: "agent";
};
export type ScopeType = (typeof Scope)[keyof typeof Scope];
export declare const Persist: {
Local: "local";
Session: "session";
None: "none";
};
export type PersistType = (typeof Persist)[keyof typeof Persist];
export type StateItemDefinition<T = unknown> = {
default: T;
scope?: ScopeType;
persist?: PersistType;
};
/**
* Context object passed to action handlers.
* Provides access to state and utilities without positional parameters.
*/
export type ActionContext<TState> = {
/** Current state snapshot */
state: TState;
/** Update state (async, awaitable) */
setState: (partial: Partial<TState>) => Promise<void>;
/** ID of the agent that called this action */
agentId: string;
/** Location info of the calling agent */
agentLocation: BrowserLocation;
};
/**
* Action handler function signature.
*/
export type ActionHandler<TState, TArgs extends unknown[], TResult> = (ctx: ActionContext<TState>, ...args: TArgs) => Promise<TResult>;
/**
* Action definition with handler and optional validation.
*/
export type ActionDefinition<TState = unknown, TArgs extends unknown[] = unknown[], TResult = unknown> = {
handler: ActionHandler<TState, TArgs, TResult>;
validate?: (...args: TArgs) => void;
};
/**
* Actions configuration object.
*/
export type ActionsDefinition<TState> = {
[actionName: string]: ActionDefinition<TState, any[], any>;
};
/**
* Base config structure that users provide to createConfig().
*/
export type ConfigSchema = {
/** Store name - used for storage keys and agent connection routing */
name: string;
/** Schema version for migrations (default: 1) */
version?: number;
/** Action definitions */
actions?: ActionsDefinition<any>;
/** State item definitions (all other keys) */
[key: string]: string | number | StateItemDefinition<any> | ActionsDefinition<any> | undefined;
};
/**
* Validated config with required fields filled in.
*/
export type ValidatedConfig<T extends ConfigSchema> = T & {
name: string;
version: number;
};
/**
* Extract state item keys from config (excludes name, version, actions).
*/
type StateKeys<T extends ConfigSchema> = {
[K in keyof T]: K extends "name" | "version" | "actions" ? never : T[K] extends StateItemDefinition<any> ? K : never;
}[keyof T];
/**
* Remove never properties from a type.
*/
type OmitNever<T> = {
[K in keyof T as T[K] extends never ? never : K]: T[K];
};
/**
* Full derived state from config (all state items).
*/
export type DerivedState<T extends ConfigSchema> = OmitNever<{
[K in StateKeys<T>]: T[K] extends StateItemDefinition<infer D> ? D : never;
}>;
/**
* Shared state (scope: 'shared' or unspecified).
*/
export type DerivedSharedState<T extends ConfigSchema> = OmitNever<{
[K in StateKeys<T>]: T[K] extends StateItemDefinition<infer D> ? T[K] extends {
scope: "agent";
} ? never : D : never;
}>;
/**
* Agent-scoped state (scope: 'agent').
*/
export type DerivedAgentState<T extends ConfigSchema> = OmitNever<{
[K in StateKeys<T>]: T[K] extends StateItemDefinition<infer D> & {
scope: "agent";
} ? D : never;
}>;
/**
* Extract the typed actions interface from config.
*/
export type DerivedActions<T extends ConfigSchema> = T["actions"] extends ActionsDefinition<any> ? {
[K in keyof T["actions"]]: T["actions"][K] extends ActionDefinition<any, infer TArgs, infer TResult> ? (...args: TArgs) => Promise<TResult> : never;
} : Record<string, never>;
export type StoreOptions = {
/** Enable debug logging */
debug?: boolean;
/** Migration function for schema version changes */
migrate?: (oldState: unknown, oldVersion: number, newVersion: number) => unknown;
};
export type AgentOptions = {
/** Enable debug logging */
debug?: boolean;
};
export type ConnectionStatus = {
connected: boolean;
agent?: AgentInfo;
};
export type StateChanges<T extends ConfigSchema> = Partial<DerivedState<T>>;
export type StateChangeListener<T extends ConfigSchema> = (state: DerivedState<T>, changes: StateChanges<T>, agent?: AgentInfo) => void;
/**
* Internal subscriber type for Store with optional key filtering.
*/
export type StateSubscriber<T extends ConfigSchema> = {
keys?: Array<keyof DerivedState<T>>;
callback: StateChangeListener<T>;
};
export type AgentConnectionInfo = {
id: string;
tabId: number;
frameId: number;
context: string;
connectedAt: number;
};
export declare function isStateItem(item: unknown): item is StateItemDefinition<unknown>;
export declare function isActionDefinition(item: unknown): item is ActionDefinition<any, any[], any>;
/**
* Creates a validated config object with proper type inference.
* This is the single source of truth for store identity and shape.
*
* @param config - The store configuration object
* @returns A validated config with defaults filled in
* @throws {ConfigError} If the config is invalid
*
* @example
* const config = createConfig({
* name: 'myFeature',
* version: 1,
* count: { default: 0, scope: 'shared', persist: 'local' },
* agentData: { default: null as AgentData | null, scope: 'agent' },
* actions: {
* increment: {
* handler: async (ctx) => {
* await ctx.setState({ count: ctx.state.count + 1 });
* return ctx.state.count + 1;
* },
* },
* },
* });
*/
export declare function createConfig<T extends ConfigSchema>(config: T): ValidatedConfig<T>;
export type { AgentInfo, BrowserLocation };