Releases: stainless-code/persist
Release list
v0.2.0
Minor Changes
-
#7
934af37Thanks @SutuSebastian! - Add./frameworks/angularand./frameworks/preacthydration adapters over theHydrationSignalseam../frameworks/angular(peer@angular/core >=17.0.0):useHydrated(signal)→ readonlySignal<boolean>. Call inside a component's injection context (effect()requires it)../frameworks/preact(peerpreact >=10.19.0):useHydrated(signal)→{ hydrated }viauseSyncExternalStore(preact/compat).@ts-expect-erroron the 3-arg call (Preact types omitgetServerSnapshot; runtime ignores it).
-
#7
934af37Thanks @SutuSebastian! - Add./transport/crosstabsubpath —createBroadcastCrossTab, a zero-depBroadcastChannelbridge for cross-tab sync over backends that fire nostorageevents (IndexedDB). Returns{ crossTabEventTarget, wrap, close }: pass the target ascrossTabEventTargetandwrap(storage)asstorageso writes/removes broadcast to other tabs. PostsstorageArea: nullon every event so key-only matching applies in every tab (each tab owns its own backend instance — reference equality onrawwould fail across tabs). GuardsBroadcastChannelavailability (SSR, Node <18) and posts after the write settles so receivers rehydrate into committed state. -
#7
934af37Thanks @SutuSebastian! - Add two zero-dep storage wrappers over theStateStorageseam — both async web-global adapters (no peer dep), composing withcreateStorage(backend, codec):./backends/encrypted—createEncryptedStorage(getStorage, { key }): AES-GCM via WebCrypto (crypto.subtle). Each stored value isbase64(iv).base64(ciphertext); the AES-GCM auth tag means a wrong key or tampered ciphertext throws on decrypt → the backend's asyncgetItemrejects → persist-core reports it viaonErrorphase"hydrate"(NOT the codec'sclearCorruptOnFailurepath, which only fires when the codec throws parsing a raw value). Returnsundefinedwhencrypto.subtleis unavailable;createStoragethen returnsundefined(the consumer can fall back, orpersistSourceno-ops if no storage resolves)../backends/compressed—createCompressedStorage(getStorage, { format? }): nativeCompressionStream/DecompressionStream(gzip|deflate|deflate-raw, defaultgzip); output is base64 so it stays string-wire. Returnsundefinedwhen the stream APIs are unavailable. Stacks withcreateEncryptedStorage(compress-then-encrypt is the standard order).
Design note: encryption + compression are backend wrappers, not sync
StorageCodecs, becausecrypto.subtleand the stream APIs are async and theStorageCodecseam is sync. The codec serializes the envelope (sync); the wrapper encrypts/compresses the serialized string (async).Also adds a README comparison table vs zustand-persist / redux-persist / @tanstack/query-persist-client / pinia-persist, and a migration guide with option-mapping tables + port snippets for each incumbent.
-
#7
934af37Thanks @SutuSebastian! - AddcreateMigrationChain— a zero-dep core helper that builds amigratecallback from a per-version step chain. Plug intoPersistOptions.migrate.steps[N]takes vN → v(N+1); the chain walks from the stored version toversion, awaiting each.onNewer(default"throw"— a downgrade is a bug) /onOlder(default"discard"— dropped support for that version).- Eager construction validation: a gap in the covered range, an out-of-range key, or a non-integer version throws now.
- Beyond TanStack
buster(discards on mismatch) — transforms instead.
-
#7
934af37Thanks @SutuSebastian! - Add./backends/node-fs—nodeFsStateStorage({ dir }), an asyncStateStorage<string>over Nodefs.promises(one file per key underdir). No peer dep (node:fsis a Node built-in). Keys are sanitized to filename-safe segments with a short hash suffix (app:prefs:v1→app_prefs_v1.<hash>) so distinct keys that sanitize to the same segment don't collide on one file;.././empty keys are refused; missing files map tonull(no throw); the dir is created lazily on first write. Compose withcreateStorage(() => nodeFsStateStorage({ dir }), codec). Unblocks server / SSR / CLI persistence.Also adds export/pack validation (
check:pack:@arethetypeswrong/cli+publint+knip) and README storage + codec decision matrices. -
#7
934af37Thanks @SutuSebastian! - Add three React Native storage subpaths over theStateStorageseam, mirroring the./backends/idbtemplate (own subpath, optional peer, no cross-entry value imports):./backends/async-storage(peer@react-native-async-storage/async-storage >=1.0.0) —asyncStorageStateStorage/createAsyncStorage. Fully async, string-wire;useHydratedgating mandatory. Accepts a customAsyncStorageinstance (e.g.getLegacyStorage()) to namespace../backends/mmkv(peerreact-native-mmkv >=4.0.0) —mmkvStateStorage/createMmkvStorage({ id, path?, encryptionKey? }). Synchronous (no hydration gate needed); uses the v4createMMKVfactory +getString/set/removeAPI. PairencryptionKeyfor secrets-at-rest../backends/secure-store(peerexpo-secure-store >=12.0.0) —secureStoreStateStorage/createSecureStoreStorage. OS keychain/keystore, async, ~2KB value limit per key — for small secrets (auth tokens), not large state; pairpartializeto persist a tiny slice.
All three compose via
createJSONStorage(jsonCodec default); swap codecs withcreateStorage(backend, codec). -
#7
934af37Thanks @SutuSebastian! - AddcreateSessionStorage— a zero-dep core DX factory oversessionStorage(per-tab;crossTabis meaningless). ReturnsundefinedwhensessionStorageis unavailable (SSR / non-DOM) or defined-but-broken (Node 22+ half-built global). No new subpath — exports from the core.entry. -
#7
934af37Thanks @SutuSebastian! - Add./frameworks/solidand./frameworks/vuehydration subpaths —useHydrated(signal)over theHydrationSignalseam, mirroring the React adapter (./frameworks/react)../frameworks/solid(peersolid-js >=1.6.0): returns a SolidAccessor<boolean>viafrom; the subscription is owned by the reactive scope and cleaned up on scope dispose. Uses thefrom(producer, initialValue)overload so the accessor isAccessor<boolean>(notboolean | undefined); readsisHydrated()for the initial value (pull-model signal — no initial notification)../frameworks/vue(peervue >=3.3.0): returns a VueRef<boolean>viashallowRef; subscription cleaned up viaonScopeDispose— call insidesetup()or aneffectScope().
Both render
trueon the server (the no-opPersistApiis always-hydrated, so the signal istrueserver-side) — matching theHydrationSignaladapter contract. Each ships as its own subpath with the peer as optional, no cross-entry value imports. -
#7
934af37Thanks @SutuSebastian! - Add four source adapters over thePersistableSourceseam — shape-named (not library-named): same persistable shape → same name → same merge semantics, regardless of library; the subpath carries the library. Each is a thinpersistSourcewrapper mapping the library's store API:./sources/zustand(peerzustand >=4.0.0):persistStore(store, opts)— zustand'sgetState/setState/subscribemap directly. Same name + shallow-spread merge as./sources/tanstack-store'spersistStore(same shape); alias one if importing both../sources/jotai(peerjotai >=2.0.0):persistAtom(store, atom, opts)— wraps a writable atom + jotaiStore; replace-merge default (likepersistAtomfrom./sources/tanstack-store) so primitive atoms don't hydrate to{}../sources/valtio(peervaltio >=1.0.0):persistProxy(proxyObject, opts)—snapshotfor reads,Object.assignfor writes,subscribefor changes../sources/mobx(peermobx >=6.0.0):persistObservable(observable, opts)—toJSfor reads,Object.assignfor writes,observefor changes.
Each is its own subpath with the peer optional, no cross-entry value imports. README "Wrapping your store" recipe section shows both the shipped adapter + the underlying
persistSourcemapping for customization.
v0.1.1
Patch Changes
- #4
efc5614Thanks @SutuSebastian! -createStoragenow shape-checks the resolved backend and treats one missinggetItem/setItem/removeItemas unavailable. Fixes the Node 22+ SSR crash wherelocalStorageexists as an object (so the availability lookup doesn't throw) but its methods areundefinedwithout a valid--localstorage-filepath — previously this passed availability and threwstorage.getItem is not a functioninsidehydrate; nowpersistSource/persistStore/persistAtomcollapse to the no-opPersistApi.
v0.1.0
Minor Changes
- #2
dbf0428Thanks @SutuSebastian! - Ship consumer Agent Skills via TanStack Intent. Addsskills/tanstack-store/SKILL.md(packaged in the npm tarball), thetanstack-intentkeyword for registry discovery,intent:validate/intent:stalescripts,intent validategated inprepublishOnly, and acheck-skills.ymlCI workflow for skill validation + post-release staleness review. No runtime API change.