@sqlrooms/room-store
v0.29.0
Published
Low-level state management primitives for SQLRooms, built on Zustand.
Readme
Low-level state management primitives for SQLRooms, built on Zustand.
Use this package when you want to build custom room state from scratch.
If you want DuckDB + layout + room shell out of the box, use @sqlrooms/room-shell.
Installation
npm install @sqlrooms/room-storeWhat this package provides
createRoomStore()andcreateRoomStoreCreator()- base lifecycle slice:
createBaseRoomSlice() - generic slice helper:
createSlice() - React context/hooks:
RoomStateProvider,useBaseRoomStore,useRoomStoreApi - persistence helpers:
persistSliceConfigs(),createPersistHelpers() - room-store persistence glue:
createRoomStorePersistence() - persistence controller:
createPersistenceController()
Quick start
import {
BaseRoomStoreState,
createBaseRoomSlice,
createRoomStore,
createSlice,
type StateCreator,
} from '@sqlrooms/room-store';
type CounterSliceState = {
counter: {
value: number;
increment: () => void;
};
};
function createCounterSlice(): StateCreator<CounterSliceState> {
return createSlice<CounterSliceState>((set, get) => ({
counter: {
value: 0,
increment: () =>
set((state) => ({
counter: {
...state.counter,
value: get().counter.value + 1,
},
})),
},
}));
}
type RoomState = BaseRoomStoreState & CounterSliceState;
export const {roomStore, useRoomStore} = createRoomStore<RoomState>(
(set, get, store) => ({
...createBaseRoomSlice()(set, get, store),
...createCounterSlice()(set, get, store),
}),
);React integration
import {RoomStateProvider} from '@sqlrooms/room-store';
import {roomStore} from './store';
export function App() {
return (
<RoomStateProvider roomStore={roomStore}>
<Dashboard />
</RoomStateProvider>
);
}import {useRoomStore} from './store';
import {Button} from '@sqlrooms/ui';
function Dashboard() {
const value = useRoomStore((state) => state.counter.value);
const increment = useRoomStore((state) => state.counter.increment);
return <Button onClick={increment}>Count: {value}</Button>;
}Imperative access
Use roomStore.getState() for non-reactive code (events, timers, async jobs).
import {roomStore} from './store';
export function incrementLater() {
setTimeout(() => {
roomStore.getState().counter.increment();
}, 500);
}Guarded command invocation
External and agent-facing integrations must use invokeCommandWithPolicy()
instead of calling roomStore.getState().commands.invokeCommand() directly.
The guarded helper re-checks that the command exists and is enabled immediately
before execution. It also blocks high-risk or requiresConfirmation commands
unless the caller supplies confirmation obtained from the user.
import {invokeCommandWithPolicy} from '@sqlrooms/room-store';
const result = await invokeCommandWithPolicy(
roomStore,
'workspace.refresh',
undefined,
{
surface: 'mcp',
actor: 'assistant',
traceId: requestId,
metadata: {clientName: 'Example client'},
signal: abortController.signal,
},
{confirmed: false},
);Set confirmed: true only after explicit user confirmation. Omitting it, or
passing false, fails closed with command-confirmation-required when the
command requires confirmation. createCommandCliAdapter() and
createCommandMcpAdapter() use the same guard and therefore have the same
execution semantics.
Persistence
For a Zustand room store with host-owned storage, prefer
createRoomStorePersistence(). It composes createPersistHelpers() with a
controller-backed PersistStorage, rehydrate saved-snapshot marking, optional
room-store subscription, autosave, and final flush helpers. This is the default
entry point for SQLRooms apps that persist room state to DuckDB, files, or another
project-owned store. See the
Persistence developer guide for the
full integration model, data flow, and examples.
persistSliceConfigs() defaults to browser localStorage. If browser storage
is null, cannot be accessed, or a raw storage operation fails, the room state
continues to work in memory and persistence is skipped. Malformed persisted JSON
and read failures from an explicit custom storage adapter still propagate
through Zustand's onRehydrateStorage callback so hosts can distinguish a
failed load from an empty store; custom write and removal failures are logged
and skipped.
Use the lower-level createPersistenceController() only when you need the same
persistence policy outside a room store or Zustand persist. The controller is
storage-agnostic: hosts provide load() and save() adapter functions, while
SQLRooms handles hydration state, dirty tracking, scheduled saves, final flush,
in-flight save coalescing, and observable save status.
createPersistHelpers() still only handles schema-based partialization and
rehydrate merging. Let createRoomStorePersistence() combine those helpers with
save policy unless you have a custom integration that does not fit the room-store
helper.
import {createRoomStorePersistence} from '@sqlrooms/room-store';
const persistence = createRoomStorePersistence({
partialize: (state) => ({room: state.room.config}),
autosaveDelayMs: 300,
load: async () => loadProjectSnapshot(),
save: async (snapshot, metadata) => {
await saveProjectSnapshot(snapshot, metadata?.reason);
},
});
await persistence.hydrate();
await persistence.flush('final-flush');Inside components, useRoomStoreApi() gives you the raw store API:
import {useRoomStoreApi} from '@sqlrooms/room-store';
import {Button} from '@sqlrooms/ui';
function ResetButton() {
const store = useRoomStoreApi();
return (
<Button
onClick={() => {
// Example: imperative read from store
const current = store.getState().room.initialized;
console.log('initialized', current);
}}
>
Inspect store
</Button>
);
}