nusm
v0.1.0
Published
Non Uniform State Manager nusm (pronounced "noose em") built on @tanstack/store with optional persisted adapters.
Readme
nusm
Non Uniform State Manager (nusm) > Pronounced noose em (/ˈnuːs əm/) is a persistence-ready wrapper around @tanstack/store with adapter-based storage
Features
- Same store semantics as @tanstack/store
- Optional persistence via adapters
- Entire-store or slice-based persistence
- Async hydration with deep merge
- Debounced persistence via @tanstack/pacer
- Adapter events for cross-tab or external updates
- React hooks (via
nusm/react) - @tanstack/devtools event support (panel coming soon)
Install
bun install `nusm`Quick Start
import { createNusmStore, createLocalStorageAdapter } from 'nusm'
const store = createNusmStore(
{ count: 0 },
{
storeId: 'counter',
adapter: createLocalStorageAdapter(),
persist: { strategy: 'entire' },
},
)
await store.ready
store.setState((state) => ({ count: state.count + 1 }))API
createNusmStore(initialState, options?)
Creates a nusm-backed store.
import { createNusmStore } from 'nusm'
const nusm = createNusmStore(initialState, {
storeId: 'settings',
adapter,
persist: {
strategy: 'entire',
},
})Return value:
- A @tanstack/store instance extended with
ready(resolves when hydration completes).
Persistence strategies
Entire store
persist: { strategy: 'entire' }Slices
persist: {
strategy: 'slices',
slices: [
{
key: 'todos',
select: (state) => state.todos,
apply: (state, sliceValue) => ({ ...state, todos: sliceValue }),
},
],
}Hydration configuration
persist: {
strategy: 'entire',
hydrate: {
discardPersisted: false,
validate: (persisted) => ({ ok: true, value: persisted }),
merge: ({ initial, persisted }) => ({ ...initial, ...persisted }),
},
}Adapters
Adapters control persistence. They define how nusm reads/writes state and how external changes (for example, another tab) are observed.
Adapter interface
type NusmAdapter = {
name: string
getItem(key: string): unknown | null | Promise<unknown | null>
setItem(key: string, value: unknown): void | Promise<void>
removeItem(key: string): void | Promise<void>
getAllKeys?(): string[] | Promise<string[]>
clear?(): void | Promise<void>
subscribe?(listener: (event: { type: 'set' | 'remove' | 'clear'; key?: string }) => void): () => void
resolveKey?(params: { storeId: string; sliceKey?: string; kind: 'entire' | 'slice' }): string
pacer?: false | { wait?: number; maxWait?: number; leading?: boolean; trailing?: boolean }
}Notes:
getAllKeysenables more complete persisted snapshots.resolveKeylets you control key layout. When omitted, nusm usesnusm:<storeId>:entireandnusm:<storeId>:slice:<sliceKey>.subscribeshould emit adapter events for cross-tab or external updates.pacercontrols debouncing of writes. Usefalseto write immediately.
Creating a custom adapter
const memoryAdapter: NusmAdapter = {
name: 'memory',
getItem: (key) => store.get(key) ?? null,
setItem: (key, value) => store.set(key, value),
removeItem: (key) => store.delete(key),
getAllKeys: () => Array.from(store.keys()),
resolveKey: ({ storeId, kind, sliceKey }) =>
kind === 'entire'
? `nusm:${storeId}:entire`
: `nusm:${storeId}:slice:${sliceKey}`,
}Local Storage
import { createLocalStorageAdapter } from 'nusm'
const adapter = createLocalStorageAdapter()Options:
storage: aStorage-like implementation (defaults towindow.localStorage).prefix: key prefix (default:nusm).serialize: custom serializer (default:superjson.stringify).deserialize: custom deserializer (default:superjson.parse).pacer: persistence debouncer configuration.
Session Storage
import { createSessionStorageAdapter } from 'nusm'
const adapter = createSessionStorageAdapter()Options:
storage: aStorage-like implementation (defaults towindow.sessionStorage).prefix: key prefix (default:nusm).serialize: custom serializer (default:superjson.stringify).deserialize: custom deserializer (default:superjson.parse).pacer: persistence debouncer configuration.
IndexDB
import { createIndexDbAdapter } from 'nusm'
const adapter = createIndexDbAdapter({ dbName: 'my-db' })Options:
dbName: database name (default:nusm).storeName: object store name (default:nusm).version: database version (default:1).serialize: custom serializer (default:superjson.stringify).deserialize: custom deserializer (default:superjson.parse).pacer: persistence debouncer configuration (default: trailing, 100ms).
React Hooks
import { useStore } from 'nusm/react'useStore uses React 19's useSyncExternalStore and supports selectors and
configurable equality checks.
useStore(store, selector?, options?)
Arguments:
store: a Nusm store instance returned bycreateNusmStore.selector(optional): function that receives the full state and returns the selected slice. Defaults to identity (returns full state).options(optional): configuration object with:equal: whentrue, uses deep equality (fast-equalsdeepEqual). Whenfalseor omitted, uses shallow equality (fast-equalsshallowEqual).
Example:
const store = createNusmStore({ user: { name: 'Ada' } })
const name = useStore(store, (state) => state.user.name)
const user = useStore(store, (state) => state.user, { equal: true })Tests
bun testBuild
bun run buildLicense
MIT. See LICENSE.
