API Reference#

The package ships two entry points:

TypeScript
import { defineStore, storeToSignals, onStoreCreated } from '@sigx/store';
import { persist } from '@sigx/store/persist';

Functions#

defineStore#

Defines a store factory. Returns a factory function; calling it creates (or resolves, per the lifetime) a store instance.

TypeScript
function defineStore<TReturn extends object, TArgs extends unknown[] = []>(
    name: string,
    setup: (ctx: SetupStoreContext, ...args: TArgs) => TReturn,
    lifetime?: Lifetime
): (...args: TArgs) => UnwrapStore<TReturn>;

Parameters

  • name — the logical store name. Instance ids are ${name}#${n} (e.g. 'todos#1'); the name is also the default persistence key suffix and the prefix of the store's topic namespaces.
  • setup — builds the store via the provided SetupStoreContext and must return an object. Extra setup parameters are tuple-typed with full inference (any arity) and are forwarded when the returned factory is called.
  • lifetime'singleton' | 'scoped' | 'transient' (from @sigx/runtime-core, honored since core 0.5.0). Defaults to 'scoped'.

Returns

A factory function (commonly assigned to a useXxxStore const). Calling it yields an UnwrapStore<TReturn> — the flat store surface: returned key signals read/write as plain values, returned computeds read as plain read-only values, actions and everything else pass through, and $-meta ($id, $patch, $events, $dispose) rides on the side. Instances created inside a component dispose with it per the lifetime rules. There is no useStore export — the factory is named by you.

Assigning to a $-prefixed key on the instance throws, as does assigning to a computed key. Spread and Object.keys over the instance see only your returned keys, never $-meta.

storeToSignals#

Destructuring-safe views of a store: key signals for state keys, read-only signal views for computeds. Functions and $-meta are skipped.

TypeScript
function storeToSignals<TReturn extends object>(store: UnwrapStore<TReturn>): {
    /* state keys */    K: KeySignal<V>;
    /* computed keys */ K: { readonly value: V };
};
  • store — a store instance created by defineStore (anything else throws).
  • Returns an object of { value } views you can destructure without losing reactivity.
TypeScript
const { todos, remaining } = storeToSignals(store);
todos.value.push(item);   // still reactive
remaining.value;          // read-only

onStoreCreated#

Register a global plugin that runs for every store instance, synchronously after its setup returns. Plugins run in registration order; a throwing plugin is isolated (logged via console.error) and cannot break store creation.

TypeScript
function onStoreCreated(plugin: (ctx: StorePluginContext) => void): Subscription;
  • plugin — receives a StorePluginContext per instance.
  • Returns a Subscription whose unsubscribe() stops future invocations.
TypeScript
onStoreCreated(({ name, instanceId, instance, onDeactivated }) => {
    const sub = wireUpLogging(instanceId, instance);
    onDeactivated(() => sub.unsubscribe());
});

Interfaces#

SetupStoreContext#

The context object passed to the defineStore setup function. Extends runtime-core's SetupFactoryContext.

TypeScript
interface SetupStoreContext extends SetupFactoryContext {
    /** The logical store name passed to defineStore (e.g. 'todos'). */
    readonly storeName: string;
    /** The friendly instance id (e.g. 'todos#1'). */
    readonly instanceId: string;
    defineState<TState extends object>(state: TState): {
        /** The deep reactive proxy — mutate freely inside the setup/actions. */
        state: TState;
        /** Spreadable per-key signals; the ones you return become public state. */
        signals: { [K in keyof TState]-?: KeySignal<TState[K]> };
        /** Per-key change events (lazy: watchers run only while subscribed). */
        events: { [K in keyof TState]-?: StateKeyEvent<TState[K]> };
        /** Atomic multi-key update; flushes reactivity once. Errors propagate. */
        patch: Patch<TState>;
    };
    defineActions<TActions extends Record<string, (...args: any[]) => any>>(
        actions: TActions
    ): StoreActions<TActions>;
    /** Typed custom events; namespaced `${id}.events`, destroyed with the store. */
    defineEvents<EventMap extends Record<string, any>>(): { [K in keyof EventMap]: Topic<EventMap[K]> };
}
  • storeName / instanceId — the logical name and the friendly per-instance id; composables use them for defaults (e.g. persist's sigx:<storeName> key).
  • defineState(state) — creates deep reactive, signal-backed state and returns { state, signals, events, patch }. Can be called multiple times for independent slices.
  • defineActions(actions) — wraps action functions with pending and dispatch lifecycle events; see StoreAction.
  • defineEvents<EventMap>() — a typed topic group (core createTopicGroup) keyed by your event map. Topics are created lazily per key on first access, namespaced ${instanceId}.events, and destroyed with the store.
  • Inherited from SetupFactoryContext: onDeactivated(fn), subscriptions, overrideDispose(fn).

StoreMeta#

The $-meta every store instance carries. $patch and $events are typed from the returned key signals only (PublicState<TReturn>).

TypeScript
type StoreMeta<TReturn extends object> = {
    /** Friendly instance id, e.g. 'todos#1'. */
    readonly $id: string;
    /** Atomic update across the store's public state keys. */
    $patch: Patch<PublicState<TReturn>>;
    /** Per-key change events for the public state keys. */
    $events: { [K in keyof PublicState<TReturn>]: StateKeyEvent<PublicState<TReturn>[K]> };
    $dispose(): void;
};

$patch throws on a state key that is not part of the public surface. $events watchers are refCounted — they run only while subscribed and never fire immediately on subscribe. The full $-meta surface per instance: $id (readonly string), $patch (object or mutator form, one flush), $events (per-public-key StateKeyEvents), $dispose() (stop state watchers, destroy every topic). Assigning to any $-prefixed key throws; spread and Object.keys over the instance never see $-meta.

StorePluginContext#

What an onStoreCreated plugin receives per store instance.

TypeScript
type StorePluginContext = {
    /** The logical store name passed to defineStore. */
    name: string;
    /** The instance id, e.g. 'todos#1'. */
    instanceId: string;
    /** The raw setup return (any shape — duck-type as needed). */
    instance: object;
    /** Tie plugin resources to the store instance's lifetime. */
    onDeactivated(fn: () => void): void;
};

Types#

UnwrapStore#

The flat store surface a factory call returns. Key signals unwrap to mutable plain values, computeds unwrap to read-only plain values, everything else passes through, intersected with StoreMeta.

TypeScript
type UnwrapStore<TReturn extends object> =
    { /* key signals  */ [K]: V }            // read/write
  & { /* computeds    */ readonly [K]: V }   // read-only
  & { /* the rest     */ [K]: TReturn[K] }   // actions, topics, helpers…
  & StoreMeta<TReturn>;

PublicState#

The keys of the setup return that are key signals — the store's public state. Drives the typing of $patch and $events.

TypeScript
type PublicState<TReturn> = {
    [K in keyof TReturn as TReturn[K] extends KeySignal<any> ? K : never]:
        TReturn[K] extends KeySignal<infer V> ? V : never;
};

KeySignal#

A signal-shaped, spreadable view over one state key (branded; produced by defineState's signals). Spread these into your setup return to make keys public.

TypeScript
type KeySignal<T> = {
    value: T;
};

StateKeyEvent#

A per-key change event. Callbacks receive the new and previous values.

TypeScript
type StateKeyEvent<T> = {
    subscribe(fn: (value: T, prev: T | undefined) => void): Subscription;
};

Patch#

The atomic update function returned by defineState (over the full slice) and exposed as $patch (over the public state). Both forms flush reactivity once.

TypeScript
type Patch<TState extends object> = {
    (partial: Partial<TState>): void;
    (mutator: (state: TState) => void): void;
};

StoreAction#

A wrapped store action: callable with the exact original signature, plus a reactive in-flight flag and per-action lifecycle event subscribers.

TypeScript
type StoreAction<F extends (...args: any[]) => any> = F & {
    /** Reactive: true while any invocation of this action is in flight. */
    readonly pending: boolean;
    onDispatching: { subscribe(fn: (...args: Parameters<F>) => void): Subscription };
    onDispatched: { subscribe(fn: (result: Awaited<ReturnType<F>>, ...args: Parameters<F>) => void): Subscription };
    onFailure: { subscribe(fn: (error: unknown, ...args: Parameters<F>) => void): Subscription };
};

Semantics:

  • onDispatching fires before the action body, with the original args.
  • onDispatched fires after the action with (result, ...args); for async actions, after the promise resolves, with the resolved value.
  • onFailure fires on a sync throw or an async rejection, with (error, ...args).
  • Sync errors re-throw to the caller; async actions return the original promise (rejections are observable by the caller, while fire-and-forget calls produce no unhandled-rejection noise).
  • pending is count-based across overlapping invocations.

Write actions as single-signature functions (union parameters instead of overloads) so these derived types stay exact.

StoreActions#

The return type of defineActions — each function wrapped as a StoreAction.

TypeScript
type StoreActions<T extends Record<string, (...args: any[]) => any>> = {
    [K in keyof T]: StoreAction<T[K]>;
};

Supporting types#

These come from @sigx/runtime-core and appear in the signatures above.

Lifetime#

The lifetime argument to defineStore. Defaults to 'scoped'. (Replaces the old InstanceLifetimes enum.)

TypeScript
type Lifetime = 'singleton' | 'scoped' | 'transient';

Topic and Subscription#

Event primitives from @sigx/runtime-core (Topic v2 — see the runtime-core API). Every subscribe() returns a Subscription.

TypeScript
interface Subscription {
    unsubscribe(): void;
}

interface Topic<T> {
    readonly namespace?: string;
    readonly name?: string;
    readonly subscriberCount: number;
    readonly hasSubscribers: boolean;
    readonly disposed: boolean;
    publish(data: T): void;
    subscribe(handler: (data: T) => void): Subscription;
    destroy(): void;
}

SetupFactoryContext#

The base of SetupStoreContext (from @sigx/runtime-core).

TypeScript
interface SetupFactoryContext {
    onDeactivated(fn: () => void): void;
    subscriptions: SubscriptionHandler;
    overrideDispose(onDispose: (fn: () => void) => void): void;
}

@sigx/store/persist#

The persistence subpath entry. See the Persistence guide for behavior and examples.

persist#

Persist a state slice to storage and rehydrate it on store creation. Call inside a store setup.

TypeScript
function persist<TState extends object>(
    ctx: SetupStoreContext,
    slice: { state: TState; patch: Patch<TState> },
    options?: PersistOptions<TState>
): PersistHandle;
  • ctx — the setup context (used for the default key and disposal wiring).
  • slice — the { state, patch } pair from defineState.
  • options — a PersistOptions<TState> bag (all fields optional).
  • Returns a PersistHandle.

Hydration is one atomic patch(); saving is a deep watch over the picked keys, debounced, and paused until hydration completes; storage failures are logged and never crash the store; everything is cleaned up on store disposal. With no storage available (SSR), persist is a no-op and hydrated is immediately true.

StorageLike#

Minimal storage contract — satisfied by localStorage, sessionStorage, and async stores like React Native's AsyncStorage (Promise-returning methods).

TypeScript
interface StorageLike {
    getItem(key: string): string | null | Promise<string | null>;
    setItem(key: string, value: string): void | Promise<void>;
    removeItem?(key: string): void | Promise<void>;
}

PersistOptions#

TypeScript
interface PersistOptions<TState extends object> {
    /** Storage key. Defaults to `sigx:<storeName>` — pass one explicitly when persisting multiple slices. */
    key?: string;
    /** Defaults to globalThis.localStorage. Absent storage (SSR) → no-op. */
    storage?: StorageLike;
    /** Persist (and hydrate) only these keys. Default: all slice keys. */
    pick?: (keyof TState)[];
    serialize?(snapshot: Partial<TState>): string;
    deserialize?(raw: string): unknown;
    /** Schema version stored alongside the data. */
    version?: number;
    /** Convert data persisted under an older version. Receives the raw deserialized payload. */
    migrate?(persisted: unknown, fromVersion: number): Partial<TState>;
    /** Debounce for writes, in ms. Default 0 (write on every change flush). */
    debounce?: number;
}

PersistHandle#

TypeScript
interface PersistHandle {
    /** Plain readonly view: true once hydration has completed. */
    hydrated: { readonly value: boolean };
    /** Resolves when hydration has completed (immediately for sync/no storage). */
    whenHydrated: Promise<void>;
    /** Remove the persisted entry from storage. */
    clear(): Promise<void>;
}

Errors#

Everything the package throws is prefixed [@sigx/store] and includes the instance id where one exists. The catalog:

Thrown messageWhen
[@sigx/store] <id>: the store setup must return an object.The defineStore setup returned null or a non-object.
[@sigx/store] <id>: "<key>" is store meta and cannot be assigned.Assignment to any $-prefixed key on the instance (store.$id = …).
[@sigx/store] <id>: "<key>" is a computed value and is read-only.Assignment to a returned computed (store.total = 0).
[@sigx/store] <id>: $patch received unknown state key "<key>".Object-form $patch with a key that is not public state.
[@sigx/store] <id>: $patch draft has no state key "<key>".Mutator-form $patch reading or writing a non-public key on the draft.
[@sigx/store] storeToSignals expects a store instance created by defineStore.storeToSignals called with anything that isn't a store instance.

Beyond these, errors thrown by your code propagate per the documented semantics: action errors re-throw (sync) or reject the returned promise (async), and errors inside a patch/$patch mutator propagate to the caller.

The following failures are logged via console.error, never thrown — they are isolation points where third-party or environmental failures must not break the store:

Logged messageWhen
[@sigx/store] <id>: error in onStoreCreated plugin: …A registered plugin threw; store creation continues.
[@sigx/store] <id>: error settling action "<name>": …A subscriber/effect threw inside the internal async settle handlers.
[@sigx/store] persist("<key>"): hydration failed, continuing with defaults: …Corrupted entry, throwing deserialize/migrate/patch during hydration.
[@sigx/store] persist("<key>"): storage read failed, continuing with defaults: …getItem threw synchronously.
[@sigx/store] persist("<key>"): hydration failed: …Async getItem rejected.
[@sigx/store] persist("<key>"): write failed: …setItem threw or rejected (quota, privacy mode, broken backend).

See also#