Plugins and orchestration
Plugins are opt-in. Attach them to a manager with createStateManager().use(...), then opt stores into the relevant feature through their definition options.
How the plugin model fits together
import {
createAsyncPlugin,
createHistoryPlugin,
createPersistencePlugin,
createStateManager,
createSyncPlugin
} from '@selfagency/stately';
const manager = createStateManager()
.use(createPersistencePlugin())
.use(createHistoryPlugin())
.use(createSyncPlugin())
.use(createAsyncPlugin());Each plugin augments stores only when the store definition opts into the matching feature.
createPersistencePlugin()
The persistence plugin hydrates state from storage and writes snapshots back through the store’s mutation pipeline.
Use it when a store should survive reloads, sessions, or app restarts.
Key behavior:
- requires a
persistoption with aversionand aPersistenceAdapter - exposes
$persist.ready,$persist.flush(),$persist.rehydrate(),$persist.clear(),$persist.pause(), and$persist.resume() - queues writes so older snapshots do not overwrite newer ones
- supports optional compression and custom serialize/deserialize hooks
- suppresses writes while replaying history or during explicit pause/rehydrate flows
import {
createLocalStorageAdapter,
createPersistencePlugin,
createStateManager,
defineStore
} from '@selfagency/stately';
const manager = createStateManager().use(createPersistencePlugin());
export const useSessionStore = defineStore('session', {
state: () => ({ theme: 'dark' }),
persist: {
adapter: createLocalStorageAdapter(),
key: 'stately:session',
version: 1
}
});See Persistence helpers for adapter and compression options.
createHistoryPlugin()
The history plugin records snapshots and adds undo/redo/time-travel helpers.
Use it for draft editing, debugging, or user-facing time travel.
Key behavior:
- requires a
historyoption on the store definition - exposes
$historyand$timeTravel - supports
undo(),redo(),goTo(index),record(snapshot),startBatch(), andendBatch()through the history controller - replays snapshots without re-triggering history recording
- avoids persistence and sync feedback loops during time travel
import { createHistoryPlugin, createStateManager, defineStore } from '@selfagency/stately';
const manager = createStateManager().use(createHistoryPlugin());
export const useDraftStore = defineStore('draft', {
state: () => ({ body: '' }),
history: { limit: 25 }
});createSyncPlugin(options?)
The sync plugin publishes store snapshots across tabs or embedded environments and applies validated inbound updates to matching stores.
Use it when multiple browser contexts should stay in sync.
Key behavior:
- ignores self-originated messages
- rejects mismatched versions
- only patches known state keys
- cleans up transports during
$dispose()
Important options:
originto identify the current tab or instanceversionto reject incompatible payloadschannelName/storageKeyto customize the default transport stacktransportsto supply your own publish/subscribe bridgecreateIdandcreateTimestampfor deterministic tests
import { createStateManager, createSyncPlugin, defineStore } from '@selfagency/stately';
const manager = createStateManager().use(createSyncPlugin({ origin: 'local-tab' }));createAsyncPlugin(options?)
The async plugin tracks action state and wraps matching actions with concurrency control.
Use it when actions can overlap, be cancelled, or need loading/error metadata.
Key behavior:
- adds a
$asyncregistry keyed by action name - wraps matching actions and keeps the action hook semantics intact
- supports
includeto limit which actions are tracked - supports
policiesand a sharedpolicyoverride for concurrency control - can inject an
AbortSignalinto actions for cancellation flows
Supported policies:
parallelrestartabledropenqueuededupe
import { createAsyncPlugin, createStateManager, defineStore } from '@selfagency/stately';
const manager = createStateManager().use(
createAsyncPlugin({
include: ['loadCount'],
policies: { loadCount: 'restartable' },
injectSignal(signal, args) {
return [signal, ...args];
}
})
);Working with plugin cleanup
Plugins commonly extend $dispose() to clean up subscriptions, transports, or other external resources. That means you should treat $dispose() as the store’s teardown point, not just a convenience method.