API Reference
The package ships two entry points:
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.
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 providedSetupStoreContextand 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 core0.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.
function storeToSignals<TReturn extends object>(store: UnwrapStore<TReturn>): {
/* state keys */ K: KeySignal<V>;
/* computed keys */ K: { readonly value: V };
};
store— a store instance created bydefineStore(anything else throws).- Returns an object of
{ value }views you can destructure without losing reactivity.
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.
function onStoreCreated(plugin: (ctx: StorePluginContext) => void): Subscription;
plugin— receives aStorePluginContextper instance.- Returns a
Subscriptionwhoseunsubscribe()stops future invocations.
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.
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'ssigx:<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 withpendingand dispatch lifecycle events; seeStoreAction.defineEvents<EventMap>()— a typed topic group (corecreateTopicGroup) 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>).
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.
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.
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.
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.
type KeySignal<T> = {
value: T;
};
StateKeyEvent
A per-key change event. Callbacks receive the new and previous values.
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.
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.
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:
onDispatchingfires before the action body, with the original args.onDispatchedfires after the action with(result, ...args); for async actions, after the promise resolves, with the resolved value.onFailurefires 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).
pendingis 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.
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.)
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.
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).
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.
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 fromdefineState.options— aPersistOptions<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).
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
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
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 message | When |
|---|---|
[@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 message | When |
|---|---|
[@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
- Getting Started
- Defining a Store — the conceptual hub
- Actions & Async · Events & Observability · Lifetimes & DI
- Using Stores in Components · Persistence
- Composables & Plugins · TypeScript · Patterns
