From ebb529df1aadff9b6688cb0df2f44722c1fece9d Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Wed, 22 Jul 2026 23:49:30 +0300 Subject: [PATCH 01/10] chore(skills): rename Intent leaves to persist-* and add zustand Composition naming avoids clashing with upstream library skill leaves; ship persist-zustand as the next source tracer and record the rest in skills/_artifacts/skill_tree.yaml. --- .changeset/intent-skills-rename.md | 5 + .github/workflows/check-skills.yml | 4 +- package.json | 1 + scripts/sync-skill-versions.ts | 19 ++-- skills/_artifacts/skill_tree.yaml | 61 +++++++++++ .../SKILL.md | 16 +-- skills/persist-zustand/SKILL.md | 100 ++++++++++++++++++ 7 files changed, 191 insertions(+), 15 deletions(-) create mode 100644 .changeset/intent-skills-rename.md create mode 100644 skills/_artifacts/skill_tree.yaml rename skills/{tanstack-store => persist-tanstack-store}/SKILL.md (94%) create mode 100644 skills/persist-zustand/SKILL.md diff --git a/.changeset/intent-skills-rename.md b/.changeset/intent-skills-rename.md new file mode 100644 index 0000000..2846112 --- /dev/null +++ b/.changeset/intent-skills-rename.md @@ -0,0 +1,5 @@ +--- +"@stainless-code/persist": patch +--- + +Rename the TanStack Store Intent skill to `persist-tanstack-store` (composition leaf; avoids clashing with upstream skill names) and add `persist-zustand`. Re-run `npx @tanstack/intent@latest install` if an agent config still points at `skills/tanstack-store`. diff --git a/.github/workflows/check-skills.yml b/.github/workflows/check-skills.yml index 324751c..ab1e4a9 100644 --- a/.github/workflows/check-skills.yml +++ b/.github/workflows/check-skills.yml @@ -37,7 +37,7 @@ jobs: node-version: 20 - name: Install intent - run: npm install -g @tanstack/intent@0.3.4 + run: npm install -g @tanstack/intent@0.3.5 - name: Validate skills run: intent validate --github-summary @@ -61,7 +61,7 @@ jobs: node-version: 20 - name: Install intent - run: npm install -g @tanstack/intent@0.3.4 + run: npm install -g @tanstack/intent@0.3.5 - name: Check skills id: stale diff --git a/package.json b/package.json index cbb7f8a..93b2a2a 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "store", "svelte", "tanstack", + "tanstack-intent", "tanstack-store", "typescript", "valtio", diff --git a/scripts/sync-skill-versions.ts b/scripts/sync-skill-versions.ts index f80f2fe..6dbafa2 100755 --- a/scripts/sync-skill-versions.ts +++ b/scripts/sync-skill-versions.ts @@ -1,19 +1,26 @@ #!/usr/bin/env bun -// Stamp `library_version` in every skills/*/SKILL.md frontmatter to the +// Stamp `library_version` in every skills/**/SKILL.md frontmatter to the // current package version. Wired into the `version` script so the changesets // "Version packages" PR carries the skill bump alongside the package bump — // keeps `intent stale` quiet without a manual review PR per release. -import { readFileSync, readdirSync, writeFileSync, existsSync } from "node:fs"; +import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; const { version } = JSON.parse(readFileSync("package.json", "utf8")); const FIELD = /^( *library_version:\s*")([^"]*)(")/m; +function* walkSkillFiles(dir: string): Generator { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (!entry.isDirectory() || entry.name === "_artifacts") continue; + const nested = join(dir, entry.name); + const skill = join(nested, "SKILL.md"); + if (existsSync(skill)) yield skill; + yield* walkSkillFiles(nested); + } +} + let touched = 0; -for (const entry of readdirSync("skills", { withFileTypes: true })) { - if (!entry.isDirectory()) continue; - const file = join("skills", entry.name, "SKILL.md"); - if (!existsSync(file)) continue; +for (const file of walkSkillFiles("skills")) { const before = readFileSync(file, "utf8"); const after = before.replace(FIELD, `$1${version}$3`); if (after !== before) { diff --git a/skills/_artifacts/skill_tree.yaml b/skills/_artifacts/skill_tree.yaml new file mode 100644 index 0000000..33b6d12 --- /dev/null +++ b/skills/_artifacts/skill_tree.yaml @@ -0,0 +1,61 @@ +# Intent skill tree — planned vs shipped composition skills for +# @stainless-code/persist. `_artifacts/` is excluded from the npm tarball. +library: + name: "@stainless-code/persist" + version: "0.4.0" + repository: "https://github.com/stainless-code/persist" + description: "Hydration-aware persistence for any reactive store" +generated_at: "2026-07-22" + +# Flat composition leaves (`persist-`) avoid clashing with upstream +# library skill names (e.g. a future `@tanstack/store` skill named +# `tanstack-store`). Intent load stays `@stainless-code/persist#`. +skills: + - name: Persist TanStack Store + slug: persist-tanstack-store + type: composition + path: skills/persist-tanstack-store/SKILL.md + status: shipped + description: persistStore/persistAtom over @tanstack/store Store and Atom. + + - name: Persist zustand + slug: persist-zustand + type: composition + path: skills/persist-zustand/SKILL.md + status: shipped + description: persistStore over zustand StoreApi. + + # Remaining source adapters (same composition pattern). + - name: Persist Jotai + slug: persist-jotai + type: composition + path: skills/persist-jotai/SKILL.md + status: planned + - name: Persist Valtio + slug: persist-valtio + type: composition + path: skills/persist-valtio/SKILL.md + status: planned + - name: Persist MobX + slug: persist-mobx + type: composition + path: skills/persist-mobx/SKILL.md + status: planned + - name: Persist Pinia + slug: persist-pinia + type: composition + path: skills/persist-pinia/SKILL.md + status: planned + - name: Persist Redux + slug: persist-redux + type: composition + path: skills/persist-redux/SKILL.md + status: planned + + # Framework hydration adapters (type: framework) — after sources. + - name: Persist React hydration + slug: persist-react + type: framework + path: skills/persist-react/SKILL.md + status: planned + note: useHydrated over HydrationSignal; other frameworks follow. diff --git a/skills/tanstack-store/SKILL.md b/skills/persist-tanstack-store/SKILL.md similarity index 94% rename from skills/tanstack-store/SKILL.md rename to skills/persist-tanstack-store/SKILL.md index 6b8ebaf..61f4d72 100644 --- a/skills/tanstack-store/SKILL.md +++ b/skills/persist-tanstack-store/SKILL.md @@ -1,14 +1,16 @@ --- -name: tanstack-store +name: persist-tanstack-store description: Persist a @tanstack/store Store or writable Atom with @stainless-code/persist (persistStore/persistAtom). Use when persisting TanStack Store state to localStorage/sessionStorage/IndexedDB, gating UI on hydration for an async backend, or deciding persistStore vs persistSource. license: MIT metadata: + type: composition library: "@stainless-code/persist" library_version: "0.4.0" - framework: "tanstack-store" + framework: "@tanstack/store" sources: - - README.md - - docs/architecture.md + - stainless-code/persist:README.md + - stainless-code/persist:docs/architecture.md + - stainless-code/persist:src/adapters/sources/tanstack-store.ts --- # Persisting TanStack Store @@ -21,7 +23,7 @@ sources: - You need a hydration signal to gate UI flash on async backends (IndexedDB). - You're deciding between `persistStore` and dropping to `persistSource`. -If you're persisting zustand / Redux / a hand-rolled atom instead, skip to `persistSource` — the adapters here only earn their keep for `@tanstack/store` shapes. +Zustand → `persist-zustand`. Other libraries with a first-party adapter → that `persist-*` skill. Hand-rolled shapes → `persistSource`. ## Install @@ -132,11 +134,11 @@ persistStore(store, { Use `persistSource({ getState, setState, subscribe }, opts)` directly when: -- The store isn't `@tanstack/store` (zustand, Redux, hand-rolled atom). +- There is no first-party `./sources/*` adapter for the store (hand-rolled atom). - You want to control subscription timing without the adapter's opinion. - You're building a framework adapter (the React `useHydrated` hook is the template — a thin layer over `HydrationSignal`; see its JSDoc for the subscribe contract). -The TanStack adapters exist because `Store`/`Atom` have a known shape; anything else is `persistSource`. +See also: `persist-zustand` — same core options over zustand's `StoreApi`. ## Common mistakes diff --git a/skills/persist-zustand/SKILL.md b/skills/persist-zustand/SKILL.md new file mode 100644 index 0000000..9561330 --- /dev/null +++ b/skills/persist-zustand/SKILL.md @@ -0,0 +1,100 @@ +--- +name: persist-zustand +description: Persist a zustand StoreApi with @stainless-code/persist (persistStore). Use when wiring zustand to localStorage/sessionStorage/IndexedDB, replacing zustand/middleware persist, or choosing between persistStore and persistSource for a zustand store. +license: MIT +metadata: + type: composition + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "zustand" +sources: + - stainless-code/persist:README.md + - stainless-code/persist:docs/architecture.md + - stainless-code/persist:src/adapters/sources/zustand.ts +--- + +# Persisting zustand + +`@stainless-code/persist/sources/zustand` maps zustand's `StoreApi` (`getState` / `setState` / `subscribe`) onto `persistSource`. Prefer this over zustand's built-in `persist` middleware when you want Persist's hydration gate, trailing throttle, cross-tab helpers, or the same storage/codec stack as other Persist adapters. + +## When to use this skill + +- You have a zustand store (`create(...)` / `StoreApi`) and want it to survive reload. +- You're migrating off `zustand/middleware`'s `persist`. +- You need async-backend hydration gating (`useHydrated`) shared with other Persist sources. + +TanStack Store / Atom → `persist-tanstack-store`. Hand-rolled shapes → `persistSource`. + +## Install + +```bash +bun add @stainless-code/persist zustand +# only when you use a codec that needs it: +bun add seroval +``` + +`zustand` is an optional peer of the `/sources/zustand` subpath — importing the subpath is the dep opt-in. + +## Minimal wiring + +```ts +import { create } from "zustand"; +import { createJSONStorage } from "@stainless-code/persist"; +import { persistStore } from "@stainless-code/persist/sources/zustand"; + +const usePrefs = create(() => ({ theme: "light" as const })); +const persist = persistStore(usePrefs, { + name: "app:prefs:v1", + storage: createJSONStorage(() => localStorage), +}); +``` + +Pass the store API (`usePrefs`), not a hook call. `persist` is a `PersistApi` — keep it for `rehydrate()` / `destroy()` / `onHydrate` / `clearStorage()`. + +## vs zustand/middleware `persist` + +| Concern | zustand `persist` | `@stainless-code/persist` | +| ------------------ | -------------------- | ---------------------------------------------------- | +| Hydration gate | library-specific | built-in; pair with `useHydrated` for async backends | +| Storage / codecs | JSON-centric helpers | `createStorage` + json / seroval / identity / custom | +| Cross-tab | optional listeners | `crossTab` + `crossTabEventTarget` for IDB | +| Multi-library apps | zustand-only | same Options / `PersistApi` across source adapters | + +Keep zustand middleware for zustand-only apps that already depend on its persist API. Switch when you want one persist stack across stores. + +## Hydration gate + +Writes are gated until hydration settles (same as other Persist sources). Sync backends settle in a microtask; IndexedDB needs a UI gate: + +```ts +import { toHydrationSignal } from "@stainless-code/persist"; +import { useHydrated } from "@stainless-code/persist/frameworks/react"; + +export const prefsHydration = toHydrationSignal(persist); +const { hydrated } = useHydrated(prefsHydration); +``` + +## Teardown — required for non-singletons + +`persistStore` subscribes to the store. For route/component-scoped stores, `destroy()` on unmount. + +```ts +useEffect(() => { + const persist = persistStore(store, opts); + return () => persist.destroy(); +}, [store]); +``` + +## Common mistakes + +- **Passing `usePrefs()` (state) instead of `usePrefs` (API).** The adapter needs `getState` / `setState` / `subscribe`. +- **Stacking zustand `persist` middleware and this adapter.** Pick one write path. +- **Gating writes manually before hydration.** The gate is built in. +- **`identityCodec` with string-only backends.** Use `jsonCodec` / `createJSONStorage` for `localStorage`; `identityCodec` is for structured-clone IDB. + +## API surface for this skill + +- `persistStore(store, options) → PersistApi` — `store: StoreApi` +- Options / `PersistApi`: same as `persistSource` (see JSDoc on `@stainless-code/persist` and `persist-tanstack-store` for the full list) + +See also: `persist-tanstack-store` — shared Options semantics (`throttleMs`, `crossTab`, `migrate`, `maxAge`) with TanStack Store examples. From ed986afe81361c7219b212f0a9e6bc1f37af3734 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Thu, 23 Jul 2026 08:51:38 +0300 Subject: [PATCH 02/10] chore(skills): ship remaining source Intent skills + persist-react Add persist-core (required by framework skills), jotai/valtio/mobx/pinia/redux composition leaves, and persist-react useHydrated. Remaining frameworks stay planned in skills/_artifacts/skill_tree.yaml. --- .changeset/intent-skills-rename.md | 2 +- skills/_artifacts/skill_tree.yaml | 87 ++++++++++++++++++------------ skills/persist-core/SKILL.md | 80 +++++++++++++++++++++++++++ skills/persist-jotai/SKILL.md | 66 +++++++++++++++++++++++ skills/persist-mobx/SKILL.md | 61 +++++++++++++++++++++ skills/persist-pinia/SKILL.md | 62 +++++++++++++++++++++ skills/persist-react/SKILL.md | 76 ++++++++++++++++++++++++++ skills/persist-redux/SKILL.md | 73 +++++++++++++++++++++++++ skills/persist-valtio/SKILL.md | 61 +++++++++++++++++++++ skills/persist-zustand/SKILL.md | 2 +- 10 files changed, 533 insertions(+), 37 deletions(-) create mode 100644 skills/persist-core/SKILL.md create mode 100644 skills/persist-jotai/SKILL.md create mode 100644 skills/persist-mobx/SKILL.md create mode 100644 skills/persist-pinia/SKILL.md create mode 100644 skills/persist-react/SKILL.md create mode 100644 skills/persist-redux/SKILL.md create mode 100644 skills/persist-valtio/SKILL.md diff --git a/.changeset/intent-skills-rename.md b/.changeset/intent-skills-rename.md index 2846112..a6bc2c7 100644 --- a/.changeset/intent-skills-rename.md +++ b/.changeset/intent-skills-rename.md @@ -2,4 +2,4 @@ "@stainless-code/persist": patch --- -Rename the TanStack Store Intent skill to `persist-tanstack-store` (composition leaf; avoids clashing with upstream skill names) and add `persist-zustand`. Re-run `npx @tanstack/intent@latest install` if an agent config still points at `skills/tanstack-store`. +Ship Intent skills for persist core, every source adapter (`persist-tanstack-store` … `persist-redux`), and `persist-react` (`useHydrated`). Renames the old `tanstack-store` leaf to `persist-tanstack-store` — re-run `npx @tanstack/intent@latest install` if an agent config still points at `skills/tanstack-store`. diff --git a/skills/_artifacts/skill_tree.yaml b/skills/_artifacts/skill_tree.yaml index 33b6d12..fde99b2 100644 --- a/skills/_artifacts/skill_tree.yaml +++ b/skills/_artifacts/skill_tree.yaml @@ -1,61 +1,78 @@ -# Intent skill tree — planned vs shipped composition skills for -# @stainless-code/persist. `_artifacts/` is excluded from the npm tarball. +# Intent skill tree — planned vs shipped skills for @stainless-code/persist. +# `_artifacts/` is excluded from the npm tarball. library: name: "@stainless-code/persist" version: "0.4.0" repository: "https://github.com/stainless-code/persist" description: "Hydration-aware persistence for any reactive store" -generated_at: "2026-07-22" +generated_at: "2026-07-23" -# Flat composition leaves (`persist-`) avoid clashing with upstream -# library skill names (e.g. a future `@tanstack/store` skill named -# `tanstack-store`). Intent load stays `@stainless-code/persist#`. +# Flat leaves (`persist-`) avoid clashing with upstream library skill names. +# Intent load: `@stainless-code/persist#`. skills: - - name: Persist TanStack Store - slug: persist-tanstack-store + - slug: persist-core + type: core + path: skills/persist-core/SKILL.md + status: shipped + - slug: persist-tanstack-store type: composition path: skills/persist-tanstack-store/SKILL.md status: shipped - description: persistStore/persistAtom over @tanstack/store Store and Atom. - - - name: Persist zustand - slug: persist-zustand + - slug: persist-zustand type: composition path: skills/persist-zustand/SKILL.md status: shipped - description: persistStore over zustand StoreApi. - - # Remaining source adapters (same composition pattern). - - name: Persist Jotai - slug: persist-jotai + - slug: persist-jotai type: composition path: skills/persist-jotai/SKILL.md - status: planned - - name: Persist Valtio - slug: persist-valtio + status: shipped + - slug: persist-valtio type: composition path: skills/persist-valtio/SKILL.md - status: planned - - name: Persist MobX - slug: persist-mobx + status: shipped + - slug: persist-mobx type: composition path: skills/persist-mobx/SKILL.md - status: planned - - name: Persist Pinia - slug: persist-pinia + status: shipped + - slug: persist-pinia type: composition path: skills/persist-pinia/SKILL.md - status: planned - - name: Persist Redux - slug: persist-redux + status: shipped + - slug: persist-redux type: composition path: skills/persist-redux/SKILL.md - status: planned - - # Framework hydration adapters (type: framework) — after sources. - - name: Persist React hydration - slug: persist-react + status: shipped + - slug: persist-react type: framework path: skills/persist-react/SKILL.md + status: shipped + + # Remaining framework hydration adapters. + - slug: persist-vue + type: framework + path: skills/persist-vue/SKILL.md + status: planned + - slug: persist-solid + type: framework + path: skills/persist-solid/SKILL.md + status: planned + - slug: persist-svelte + type: framework + path: skills/persist-svelte/SKILL.md + status: planned + - slug: persist-preact + type: framework + path: skills/persist-preact/SKILL.md + status: planned + - slug: persist-angular + type: framework + path: skills/persist-angular/SKILL.md + status: planned + - slug: persist-lit + type: framework + path: skills/persist-lit/SKILL.md + status: planned + - slug: persist-alpine + type: framework + path: skills/persist-alpine/SKILL.md status: planned - note: useHydrated over HydrationSignal; other frameworks follow. diff --git a/skills/persist-core/SKILL.md b/skills/persist-core/SKILL.md new file mode 100644 index 0000000..c6cfb74 --- /dev/null +++ b/skills/persist-core/SKILL.md @@ -0,0 +1,80 @@ +--- +name: persist-core +description: Core @stainless-code/persist concepts — persistSource, PersistOptions, PersistApi, hydration gate, throttle, cross-tab, migrate. Load before framework hydration skills or when wiring a custom PersistableSource. +license: MIT +metadata: + type: core + library: "@stainless-code/persist" + library_version: "0.4.0" +sources: + - stainless-code/persist:src/core/persist-core.ts + - stainless-code/persist:docs/architecture.md + - stainless-code/persist:README.md +--- + +# Persist core + +Zero-dep `persistSource(source, options)` owns hydrate → subscribe → write. First-party `./sources/*` adapters only supply a `PersistableSource` shape (`getState` / `setState` / `subscribe`). + +## When to use this skill + +- Custom store shape (no `persist-*` adapter). +- Shared Options / `PersistApi` semantics across adapters. +- Before `persist-react` (and other framework hydration skills). + +Library-specific wiring → the matching `persist-*` composition skill. + +## Minimal custom source + +```ts +import { createJSONStorage, persistSource } from "@stainless-code/persist"; + +const persist = persistSource( + { + getState: () => state, + setState: (updater) => { + state = updater(state); + }, + subscribe: (listener) => { + listeners.add(listener); + return { unsubscribe: () => listeners.delete(listener) }; + }, + }, + { + name: "app:custom:v1", + storage: createJSONStorage(() => localStorage), + }, +); +``` + +## Contracts agents must not invent + +- **Hydration write gate** — `setState` before hydrate settles does not clobber storage. Don't double-gate manually. +- **`throttleMs`** — trailing-only; first write waits out the window. `destroy()` flushes pending writes. +- **`maxAge`** — opt-in; prefs should not silently expire. +- **`instanceof Promise`** on reads — not thenable duck-typing. +- **`PersistDecodeRethrowError`** — decode errors that must not hit `clearCorrupt` (wrong sync/async lane). +- **Teardown** — `persist.destroy()` for non-singleton lifetimes. + +## `PersistApi` (keep the reference) + +`rehydrate()`, `hasHydrated()`, `onHydrate`, `onFinishHydration`, `setOptions`, `clearStorage`, `getOptions`, `destroy`. + +## Hydration signal for UI + +```ts +import { toHydrationSignal } from "@stainless-code/persist"; + +export const hydration = toHydrationSignal(persist); +// frameworks/react → useHydrated(hydration) — see persist-react +``` + +## Backend × codec (short) + +| State | Backend | Helper / codec | +| ------------------ | -------------- | ------------------- | +| JSON-able | `localStorage` | `createJSONStorage` | +| `Set`/`Map`/`Date` | `localStorage` | seroval codec | +| Structured clone | IndexedDB | `identityCodec` | + +See also: `persist-react` for the React gate; composition skills for zustand / jotai / Redux / … diff --git a/skills/persist-jotai/SKILL.md b/skills/persist-jotai/SKILL.md new file mode 100644 index 0000000..e715177 --- /dev/null +++ b/skills/persist-jotai/SKILL.md @@ -0,0 +1,66 @@ +--- +name: persist-jotai +description: Persist a jotai WritableAtom via createStore with @stainless-code/persist (persistAtom). Use when wiring jotai atoms to localStorage/sessionStorage/IndexedDB, or when default replace-merge for primitive atoms matters. +license: MIT +metadata: + type: composition + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "jotai" +sources: + - stainless-code/persist:src/adapters/sources/jotai.ts + - stainless-code/persist:docs/architecture.md +--- + +# Persisting jotai + +`@stainless-code/persist/sources/jotai` persists a **WritableAtom** through a jotai **Store** (`createStore()` → `{ get, set, sub }`). + +## When to use this skill + +- You own a writable jotai atom and a store, and want reload survival. +- Atom values may be primitives — default merge must **replace**, not shallow-spread. + +Zustand / TanStack Store → those `persist-*` skills. Hand-rolled → `persistSource`. + +## Install + +```bash +bun add @stainless-code/persist jotai +``` + +`jotai` is an optional peer of `/sources/jotai`. + +## Minimal wiring + +```ts +import { atom, createStore } from "jotai"; +import { createJSONStorage } from "@stainless-code/persist"; +import { persistAtom } from "@stainless-code/persist/sources/jotai"; + +const themeAtom = atom<"light" | "dark">("light"); +const store = createStore(); +const persist = persistAtom(store, themeAtom, { + name: "app:theme:v1", + storage: createJSONStorage(() => localStorage), +}); +``` + +Needs **both** `store` and `atom`. Provider-only / atom-without-store is not enough. + +## Replace-merge default + +Default `merge` is `(persisted) => persisted`. Shallow-spreading a primitive corrupts it (`{...number}` → `{}`). Pass your own `merge` for object atoms that need spread-merge. Override uses `??`, so `merge: undefined` still replace-merges. + +## Common mistakes + +- **Omitting `createStore`.** `persistAtom(store, atom, opts)` — store first. +- **Readonly / computed atoms.** Only writable atoms; no `set` → not persistable. +- **Assuming object shallow-merge.** Primitives need replace (the default). + +## API surface + +- `persistAtom(store, atom, options) → PersistApi` +- Options / `PersistApi`: same as `persistSource` (see `persist-tanstack-store` for the shared list) + +See also: `persist-tanstack-store` (`persistAtom` there has the same replace-merge opinionation). diff --git a/skills/persist-mobx/SKILL.md b/skills/persist-mobx/SKILL.md new file mode 100644 index 0000000..e27f837 --- /dev/null +++ b/skills/persist-mobx/SKILL.md @@ -0,0 +1,61 @@ +--- +name: persist-mobx +description: Persist a MobX observable object with @stainless-code/persist (persistObservable). Use when wiring MobX state to localStorage/sessionStorage/IndexedDB under enforceActions. +license: MIT +metadata: + type: composition + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "mobx" +sources: + - stainless-code/persist:src/adapters/sources/mobx.ts + - stainless-code/persist:docs/architecture.md +--- + +# Persisting MobX + +`@stainless-code/persist/sources/mobx` maps an **observable object** through `toJS` / `runInAction`+`Object.assign` / `observe` onto `persistSource`. + +## When to use this skill + +- You have `observable.object({…})` (or compatible object observable) and want reload survival. +- You use `configure({ enforceActions: "always" })` — hydrate must run inside an action. + +## Install + +```bash +bun add @stainless-code/persist mobx +``` + +`mobx` is an optional peer of `/sources/mobx`. + +## Minimal wiring + +```ts +import { observable } from "mobx"; +import { createJSONStorage } from "@stainless-code/persist"; +import { persistObservable } from "@stainless-code/persist/sources/mobx"; + +const prefs = observable.object({ theme: "light" as const }); +const persist = persistObservable(prefs, { + name: "app:prefs:v1", + storage: createJSONStorage(() => localStorage), +}); +``` + +## Hydrate semantics + +Hydrate uses `runInAction(() => Object.assign(observable, next))`. Bare assign would throw under `enforceActions: "always"`. Keys absent from `next` are not deleted (same as valtio). + +## Common mistakes + +- **Writing hydrate outside actions** in custom `persistSource` ports — this adapter already wraps `runInAction`. +- **Class instances / primitives** as the persist root — target is an object observable. +- **Expecting deep replace** from `Object.assign` alone. + +## API surface + +- `persistObservable(observable, options) → PersistApi` +- Options / `PersistApi`: same as `persistSource` + +See also: `persist-valtio` — same object + assign hydrate pattern without MobX actions. diff --git a/skills/persist-pinia/SKILL.md b/skills/persist-pinia/SKILL.md new file mode 100644 index 0000000..9a7f542 --- /dev/null +++ b/skills/persist-pinia/SKILL.md @@ -0,0 +1,62 @@ +--- +name: persist-pinia +description: Persist a Pinia store instance with @stainless-code/persist (persistStore). Use when wiring Pinia option/setup stores to localStorage/sessionStorage/IndexedDB; hydrate replaces $state (not $patch). +license: MIT +metadata: + type: composition + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "pinia" +sources: + - stainless-code/persist:src/adapters/sources/pinia.ts + - stainless-code/persist:docs/architecture.md +--- + +# Persisting Pinia + +`@stainless-code/persist/sources/pinia` maps a **store instance** onto `persistSource`. Hydrate assigns **`$state`** (full replace). Subscribe uses `$subscribe(…, { detached: true })` so the listener survives Vue `effectScope` stop. + +## When to use this skill + +- You have `const store = useX()` and want reload survival for option or setup stores. +- You need Persist's gate / codecs / cross-tab instead of (or beside) pinia-plugin-persistedstate. + +## Install + +```bash +bun add @stainless-code/persist pinia +``` + +`pinia` is an optional peer of `/sources/pinia` (Vue app + `createPinia()` still required by Pinia). + +## Minimal wiring + +```ts +import { defineStore } from "pinia"; +import { createJSONStorage } from "@stainless-code/persist"; +import { persistStore } from "@stainless-code/persist/sources/pinia"; + +const usePrefs = defineStore("prefs", { + state: () => ({ theme: "light" as const }), +}); + +const store = usePrefs(); // instance, after pinia is active +const persist = persistStore(store, { + name: "app:prefs:v1", + storage: createJSONStorage(() => localStorage), +}); +``` + +## Common mistakes + +- **Passing `defineStore(...)` / the factory** instead of `usePrefs()` instance. +- **Expecting `$patch` semantics on hydrate.** Persist replaces `$state`. +- **Name clash with Redux `persistStore`.** Import from `/sources/pinia` (or alias). +- **Call before `setActivePinia` / app.use(pinia).** Same as any Pinia store use. + +## API surface + +- `persistStore(store, options) → PersistApi` — `store` is a Pinia `Store` +- Options / `PersistApi`: same as `persistSource` + +See also: `persist-react` / Vue hydration adapters for UI gating on async backends. diff --git a/skills/persist-react/SKILL.md b/skills/persist-react/SKILL.md new file mode 100644 index 0000000..37ab365 --- /dev/null +++ b/skills/persist-react/SKILL.md @@ -0,0 +1,76 @@ +--- +name: persist-react +description: Gate React UI on Persist hydration with useHydrated + toHydrationSignal. Use when avoiding flash of default state on async backends (IndexedDB), SSR snapshot true, or wiring HydrationSignal into React 18/19. +license: MIT +metadata: + type: framework + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "react" +requires: + - persist-core +sources: + - stainless-code/persist:src/adapters/frameworks/react.ts + - stainless-code/persist:docs/architecture.md +--- + +# React hydration gate + +This skill builds on `persist-core`. Read it first for `persistSource` / Options / `toHydrationSignal`. + +`@stainless-code/persist/frameworks/react` exports `useHydrated(signal)` — a thin `useSyncExternalStore` wrapper over `HydrationSignal`. It does **not** persist state; pair it with a `./sources/*` adapter (`persist-zustand`, `persist-tanstack-store`, …). + +## When to use this skill + +- Async backend (IndexedDB) — gate UI until hydrate settles. +- SSR — server snapshot is always `hydrated: true` (nothing to wait for server-side). +- `null` / `undefined` signal → treat as hydrated (no persistence configured). + +## Install + +```bash +bun add @stainless-code/persist react +``` + +`react` `^18 || ^19` is the optional peer of `/frameworks/react`. + +## Minimal wiring + +```ts +// store module +import { toHydrationSignal } from "@stainless-code/persist"; +import { persistStore } from "@stainless-code/persist/sources/zustand"; + +const persist = persistStore(store, { name: "app:prefs:v1", storage }); +export const prefsHydration = toHydrationSignal(persist); +``` + +```tsx +// component +import { useHydrated } from "@stainless-code/persist/frameworks/react"; + +const { hydrated } = useHydrated(prefsHydration); +const prefs = useStore(store); // your selector — not from useHydrated +if (!hydrated) return ; +return ; +``` + +## Contracts + +- Return shape is **only** `{ hydrated: boolean }` — read state via your store's selector. +- Gates **UI flash**, not store reads; pre-hydration `getState()` can still see defaults. +- Sync backends settle in a microtask; module-load stores usually hydrate before first paint, but `useHydrated` is still the safe read. + +## Common mistakes + +- **Treating `useHydrated` as the state source.** It only reports the gate. +- **Expecting `hydrated: false` on the server.** SSR snapshot is `true`. +- **Forgetting `toHydrationSignal(persist)`.** Pass a `HydrationSignal`, not `PersistApi`. +- **Assuming `null` signal means “loading”.** It means “no persist” → hydrated. + +## API surface + +- `useHydrated(signal: HydrationSignal | null | undefined) → { hydrated: boolean }` +- Core helper: `toHydrationSignal(persist)` from `@stainless-code/persist` + +See also: the `persist-*` source skill for your store library; other UI frameworks under `/frameworks/*` (`vue`, `solid`, `svelte`, …). diff --git a/skills/persist-redux/SKILL.md b/skills/persist-redux/SKILL.md new file mode 100644 index 0000000..ea444dc --- /dev/null +++ b/skills/persist-redux/SKILL.md @@ -0,0 +1,73 @@ +--- +name: persist-redux +description: Persist a Redux/RTK store with @stainless-code/persist (persistStore + persistableReducer). Use when wrapping the root reducer for hydrate SET actions, or migrating off redux-persist. +license: MIT +metadata: + type: composition + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "redux" +sources: + - stainless-code/persist:src/adapters/sources/redux.ts + - stainless-code/persist:docs/architecture.md +--- + +# Persisting Redux / RTK + +`@stainless-code/persist/sources/redux` has no native `setState` — hydrate dispatches a private `@stainless-code/persist/SET` action. The **root reducer must** be wrapped with `persistableReducer` so that action replaces state. + +## When to use this skill + +- Classic `redux` or `@reduxjs/toolkit` `configureStore` / `createStore`. +- Migrating off `redux-persist` while keeping one Persist stack across the app. + +## Install + +```bash +bun add @stainless-code/persist redux +# and/or +bun add @reduxjs/toolkit +``` + +`redux` `>=5` is the optional peer (covers classic + RTK stores). + +## Minimal wiring + +```ts +import { createStore } from "redux"; +import { createJSONStorage } from "@stainless-code/persist"; +import { + persistStore, + persistableReducer, +} from "@stainless-code/persist/sources/redux"; + +const store = createStore(persistableReducer(rootReducer)); +const persist = persistStore(store, { + name: "app:root:v1", + storage: createJSONStorage(() => localStorage), +}); +``` + +RTK: pass `reducer: persistableReducer(rootReducer)` (or wrap the combined root) into `configureStore`. + +## `persistableReducer` (required) + +- Wrap the **root** only — not each slice, not leaves inside `combineReducers`. +- Per-slice wrap corrupts hydrate (payload is the full root state). +- Without the wrap, hydrate no-ops; in DEV Persist `console.warn`s if state identity is unchanged after the SET dispatch. +- Named `persistableReducer` to avoid clashing with redux-persist's `persistReducer`. + +## Common mistakes + +- **Forgetting `persistableReducer`.** Silent no-op hydrate. +- **Wrapping slices.** Full-state SET payload won't match slice shape. +- **`replaceReducer` for hydrate.** Not the Persist path. +- **Name clash with redux-persist `persistStore`.** Alias one import. + +## API surface + +- `persistableReducer(baseReducer) → Reducer` +- `persistStore(store, options) → PersistApi` +- Options / `PersistApi`: same as `persistSource` + +See also: `persist-zustand` for a store API with native `setState` (no reducer wrap). diff --git a/skills/persist-valtio/SKILL.md b/skills/persist-valtio/SKILL.md new file mode 100644 index 0000000..27248e7 --- /dev/null +++ b/skills/persist-valtio/SKILL.md @@ -0,0 +1,61 @@ +--- +name: persist-valtio +description: Persist a valtio proxy object with @stainless-code/persist (persistProxy). Use when wiring valtio state to localStorage/sessionStorage/IndexedDB via snapshot/subscribe. +license: MIT +metadata: + type: composition + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "valtio" +sources: + - stainless-code/persist:src/adapters/sources/valtio.ts + - stainless-code/persist:docs/architecture.md +--- + +# Persisting valtio + +`@stainless-code/persist/sources/valtio` maps a **proxy object** through `snapshot` / `Object.assign` / `subscribe` (`valtio/vanilla`) onto `persistSource`. + +## When to use this skill + +- You have a `proxy({…})` object state and want it to survive reload. +- State is an **object** (not a primitive proxy target). + +## Install + +```bash +bun add @stainless-code/persist valtio +``` + +`valtio` is an optional peer of `/sources/valtio`. + +## Minimal wiring + +```ts +import { proxy } from "valtio"; +import { createJSONStorage } from "@stainless-code/persist"; +import { persistProxy } from "@stainless-code/persist/sources/valtio"; + +const prefs = proxy({ theme: "light" as const }); +const persist = persistProxy(prefs, { + name: "app:prefs:v1", + storage: createJSONStorage(() => localStorage), +}); +``` + +## Hydrate semantics + +Writes apply with `Object.assign(proxy, next)` — keys **absent** from `next` are not deleted. Design `partialize` / migrate so hydrate payloads carry the shape you need, or reset keys yourself. + +## Common mistakes + +- **Persisting a non-object.** `TState extends object`. +- **Expecting deep replace / key deletion** on hydrate via assign alone. +- **Forgetting `destroy()`** for non-singleton proxies. + +## API surface + +- `persistProxy(proxyObject, options) → PersistApi` +- Options / `PersistApi`: same as `persistSource` + +See also: `persist-mobx` — same object + assign pattern with `runInAction`. diff --git a/skills/persist-zustand/SKILL.md b/skills/persist-zustand/SKILL.md index 9561330..823502b 100644 --- a/skills/persist-zustand/SKILL.md +++ b/skills/persist-zustand/SKILL.md @@ -23,7 +23,7 @@ sources: - You're migrating off `zustand/middleware`'s `persist`. - You need async-backend hydration gating (`useHydrated`) shared with other Persist sources. -TanStack Store / Atom → `persist-tanstack-store`. Hand-rolled shapes → `persistSource`. +Other sources → matching `persist-*` skill (`persist-jotai`, `persist-redux`, …). Hand-rolled shapes → `persistSource`. UI gate → `persist-react`. ## Install From d1bf9cc3436c2bb42163b578afb678df51226c4a Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Thu, 23 Jul 2026 08:53:41 +0300 Subject: [PATCH 03/10] chore(skills): Intent skills for every frameworks/* adapter Cover preact, vue, solid, angular, lit, alpine, svelte runes, and svelte-store so agent guidance matches the full public hydration surface. --- .changeset/intent-skills-rename.md | 2 +- skills/_artifacts/skill_tree.yaml | 38 +++++++++------- skills/persist-alpine/SKILL.md | 66 ++++++++++++++++++++++++++++ skills/persist-angular/SKILL.md | 58 ++++++++++++++++++++++++ skills/persist-core/SKILL.md | 2 +- skills/persist-lit/SKILL.md | 57 ++++++++++++++++++++++++ skills/persist-preact/SKILL.md | 52 ++++++++++++++++++++++ skills/persist-react/SKILL.md | 2 +- skills/persist-solid/SKILL.md | 49 +++++++++++++++++++++ skills/persist-svelte-store/SKILL.md | 55 +++++++++++++++++++++++ skills/persist-svelte/SKILL.md | 57 ++++++++++++++++++++++++ skills/persist-vue/SKILL.md | 56 +++++++++++++++++++++++ 12 files changed, 475 insertions(+), 19 deletions(-) create mode 100644 skills/persist-alpine/SKILL.md create mode 100644 skills/persist-angular/SKILL.md create mode 100644 skills/persist-lit/SKILL.md create mode 100644 skills/persist-preact/SKILL.md create mode 100644 skills/persist-solid/SKILL.md create mode 100644 skills/persist-svelte-store/SKILL.md create mode 100644 skills/persist-svelte/SKILL.md create mode 100644 skills/persist-vue/SKILL.md diff --git a/.changeset/intent-skills-rename.md b/.changeset/intent-skills-rename.md index a6bc2c7..2271de3 100644 --- a/.changeset/intent-skills-rename.md +++ b/.changeset/intent-skills-rename.md @@ -2,4 +2,4 @@ "@stainless-code/persist": patch --- -Ship Intent skills for persist core, every source adapter (`persist-tanstack-store` … `persist-redux`), and `persist-react` (`useHydrated`). Renames the old `tanstack-store` leaf to `persist-tanstack-store` — re-run `npx @tanstack/intent@latest install` if an agent config still points at `skills/tanstack-store`. +Ship Intent skills covering persist core, every `./sources/*` adapter, and every `./frameworks/*` hydration adapter (including separate `persist-svelte` / `persist-svelte-store`). Renames the old `tanstack-store` leaf to `persist-tanstack-store` — re-run `npx @tanstack/intent@latest install` if an agent config still points at `skills/tanstack-store`. diff --git a/skills/_artifacts/skill_tree.yaml b/skills/_artifacts/skill_tree.yaml index fde99b2..ebe4b88 100644 --- a/skills/_artifacts/skill_tree.yaml +++ b/skills/_artifacts/skill_tree.yaml @@ -1,4 +1,4 @@ -# Intent skill tree — planned vs shipped skills for @stainless-code/persist. +# Intent skill tree for @stainless-code/persist. # `_artifacts/` is excluded from the npm tarball. library: name: "@stainless-code/persist" @@ -14,6 +14,8 @@ skills: type: core path: skills/persist-core/SKILL.md status: shipped + + # Sources (composition) - slug: persist-tanstack-store type: composition path: skills/persist-tanstack-store/SKILL.md @@ -42,37 +44,41 @@ skills: type: composition path: skills/persist-redux/SKILL.md status: shipped + + # Frameworks (all require persist-core) - slug: persist-react type: framework path: skills/persist-react/SKILL.md status: shipped - - # Remaining framework hydration adapters. + - slug: persist-preact + type: framework + path: skills/persist-preact/SKILL.md + status: shipped - slug: persist-vue type: framework path: skills/persist-vue/SKILL.md - status: planned + status: shipped - slug: persist-solid type: framework path: skills/persist-solid/SKILL.md - status: planned - - slug: persist-svelte - type: framework - path: skills/persist-svelte/SKILL.md - status: planned - - slug: persist-preact - type: framework - path: skills/persist-preact/SKILL.md - status: planned + status: shipped - slug: persist-angular type: framework path: skills/persist-angular/SKILL.md - status: planned + status: shipped - slug: persist-lit type: framework path: skills/persist-lit/SKILL.md - status: planned + status: shipped - slug: persist-alpine type: framework path: skills/persist-alpine/SKILL.md - status: planned + status: shipped + - slug: persist-svelte + type: framework + path: skills/persist-svelte/SKILL.md + status: shipped + - slug: persist-svelte-store + type: framework + path: skills/persist-svelte-store/SKILL.md + status: shipped diff --git a/skills/persist-alpine/SKILL.md b/skills/persist-alpine/SKILL.md new file mode 100644 index 0000000..ed198ae --- /dev/null +++ b/skills/persist-alpine/SKILL.md @@ -0,0 +1,66 @@ +--- +name: persist-alpine +description: Gate Alpine.js UI on Persist hydration via Alpine.plugin(persist) and useHydrated / $hydrated magic. Use when wiring HydrationSignal into Alpine data components. +license: MIT +metadata: + type: framework + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "alpine" +requires: + - persist-core +sources: + - stainless-code/persist:src/adapters/frameworks/alpine.ts +--- + +# Alpine hydration gate + +This skill builds on `persist-core`. Read it first for `toHydrationSignal`. + +`@stainless-code/persist/frameworks/alpine` is **plugin-first**: `Alpine.plugin(persist)` registers `$hydrated` and enables reactive `useHydrated`. Call `destroy()` from `Alpine.data` teardown. + +## Install + +```bash +bun add @stainless-code/persist alpinejs +``` + +Peer: `alpinejs` `>=3.0.0` (types via `@types/alpinejs` — package ships none). + +## Minimal wiring + +```ts +import Alpine from "alpinejs"; +import persist, { + useHydrated, +} from "@stainless-code/persist/frameworks/alpine"; + +Alpine.plugin(persist); + +Alpine.data("prefs", () => { + const hydration = useHydrated(prefsHydration); + return { + get hydrated() { + return hydration.hydrated; + }, + destroy() { + hydration.destroy(); + }, + }; +}); +``` + +Template: `x-show="hydrated"` / `$hydrated` magic (cached per element). + +## Common mistakes + +- **Skipping `Alpine.plugin(persist)`.** `useHydrated` falls back to a plain object + one-time non-prod warn. +- **Not forwarding `destroy()`** from `Alpine.data`. +- **Stacking manual `$hydrated` subscriptions** — magic already caches per element. + +## API surface + +- default/`persist(Alpine)` — plugin +- `useHydrated(signal) → { hydrated; destroy() }` + +See also: `persist-core`. diff --git a/skills/persist-angular/SKILL.md b/skills/persist-angular/SKILL.md new file mode 100644 index 0000000..c66a035 --- /dev/null +++ b/skills/persist-angular/SKILL.md @@ -0,0 +1,58 @@ +--- +name: persist-angular +description: Gate Angular UI on Persist hydration with useHydrated (readonly Signal). Call inside an injection context; use when avoiding flash of default state on async backends. +license: MIT +metadata: + type: framework + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "angular" +requires: + - persist-core +sources: + - stainless-code/persist:src/adapters/frameworks/angular.ts +--- + +# Angular hydration gate + +This skill builds on `persist-core`. Read it first for `toHydrationSignal`. + +`@stainless-code/persist/frameworks/angular` returns a readonly `Signal`. Must be called in an **injection context** (`effect()` requires it — typically a component field initializer). + +## Install + +```bash +bun add @stainless-code/persist @angular/core +``` + +Peer: `@angular/core` `>=17.0.0`. + +## Minimal wiring + +```ts +import { useHydrated } from "@stainless-code/persist/frameworks/angular"; + +export class PrefsComponent { + hydrated = useHydrated(prefsHydration); +} +``` + +```html +@if (hydrated()) { + +} @else { + +} +``` + +## Common mistakes + +- **Calling outside injection context.** +- **Treating the signal as store state.** +- **SSR / null signal** — both surface as hydrated `true`. + +## API surface + +- `useHydrated(signal) → Signal` (readonly) + +See also: `persist-core`. diff --git a/skills/persist-core/SKILL.md b/skills/persist-core/SKILL.md index c6cfb74..96953e8 100644 --- a/skills/persist-core/SKILL.md +++ b/skills/persist-core/SKILL.md @@ -77,4 +77,4 @@ export const hydration = toHydrationSignal(persist); | `Set`/`Map`/`Date` | `localStorage` | seroval codec | | Structured clone | IndexedDB | `identityCodec` | -See also: `persist-react` for the React gate; composition skills for zustand / jotai / Redux / … +See also: framework skills (`persist-react`, `persist-vue`, `persist-svelte`, …) for UI gates; composition skills for each `./sources/*` adapter. diff --git a/skills/persist-lit/SKILL.md b/skills/persist-lit/SKILL.md new file mode 100644 index 0000000..0ab389d --- /dev/null +++ b/skills/persist-lit/SKILL.md @@ -0,0 +1,57 @@ +--- +name: persist-lit +description: Gate Lit element UI on Persist hydration with HydrationController. Use when wiring HydrationSignal into a ReactiveControllerHost; not a hook. +license: MIT +metadata: + type: framework + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "lit" +requires: + - persist-core +sources: + - stainless-code/persist:src/adapters/frameworks/lit.ts +--- + +# Lit hydration gate + +This skill builds on `persist-core`. Read it first for `toHydrationSignal`. + +`@stainless-code/persist/frameworks/lit` exports `HydrationController` — a `ReactiveController`, not a hook. Constructor calls `host.addController(this)`; subscribes on `hostConnected`, tears down on `hostDisconnected`. + +## Install + +```bash +bun add @stainless-code/persist lit +``` + +Peer: `lit` `>=3.0.0`. + +## Minimal wiring + +```ts +import { LitElement, html } from "lit"; +import { HydrationController } from "@stainless-code/persist/frameworks/lit"; + +class PrefsEl extends LitElement { + #hydration = new HydrationController(this, prefsHydration); + + render() { + return this.#hydration.hydrated + ? html`` + : html``; + } +} +``` + +## Common mistakes + +- **Inventing `useHydrated` for Lit.** Use the controller. +- **Reading `hydrated` once at construct** without rendering from it after connect (reconnect / late hydrate need `requestUpdate`). +- **Null signal ≠ loading** → `hydrated` true, no subscribe. + +## API surface + +- `new HydrationController(host, signal)` — getter `hydrated: boolean` + +See also: `persist-core`. diff --git a/skills/persist-preact/SKILL.md b/skills/persist-preact/SKILL.md new file mode 100644 index 0000000..d4ccad4 --- /dev/null +++ b/skills/persist-preact/SKILL.md @@ -0,0 +1,52 @@ +--- +name: persist-preact +description: Gate Preact UI on Persist hydration with useHydrated (preact/compat useSyncExternalStore). Use when avoiding flash of default state on async backends or wiring HydrationSignal into Preact. +license: MIT +metadata: + type: framework + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "preact" +requires: + - persist-core +sources: + - stainless-code/persist:src/adapters/frameworks/preact.ts +--- + +# Preact hydration gate + +This skill builds on `persist-core`. Read it first for `toHydrationSignal`. + +`@stainless-code/persist/frameworks/preact` mirrors the React adapter via `preact/compat` `useSyncExternalStore` (SSR snapshot always `true`). Not a state source — pair with a `./sources/*` adapter. + +## Install + +```bash +bun add @stainless-code/persist preact +``` + +Peer: `preact` `>=10.19.0`. + +## Minimal wiring + +```tsx +import { useHydrated } from "@stainless-code/persist/frameworks/preact"; + +const { hydrated } = useHydrated(prefsHydration); +if (!hydrated) return ; +return ; +``` + +`prefsHydration = toHydrationSignal(persist)` from your store module. + +## Common mistakes + +- **Importing `/frameworks/react` in a Preact app.** Use this subpath. +- **Treating the hook as state.** Read the store separately. +- **Null signal ≠ loading** — means no persist → hydrated. + +## API surface + +- `useHydrated(signal) → { hydrated: boolean }` + +See also: `persist-react` (same contract); `persist-core`. diff --git a/skills/persist-react/SKILL.md b/skills/persist-react/SKILL.md index 37ab365..5c496be 100644 --- a/skills/persist-react/SKILL.md +++ b/skills/persist-react/SKILL.md @@ -73,4 +73,4 @@ return ; - `useHydrated(signal: HydrationSignal | null | undefined) → { hydrated: boolean }` - Core helper: `toHydrationSignal(persist)` from `@stainless-code/persist` -See also: the `persist-*` source skill for your store library; other UI frameworks under `/frameworks/*` (`vue`, `solid`, `svelte`, …). +See also: the `persist-*` source skill for your store library; sibling framework skills (`persist-vue`, `persist-solid`, `persist-svelte`, `persist-svelte-store`, `persist-preact`, `persist-angular`, `persist-lit`, `persist-alpine`). diff --git a/skills/persist-solid/SKILL.md b/skills/persist-solid/SKILL.md new file mode 100644 index 0000000..6396460 --- /dev/null +++ b/skills/persist-solid/SKILL.md @@ -0,0 +1,49 @@ +--- +name: persist-solid +description: Gate Solid UI on Persist hydration with useHydrated (Accessor). Use when avoiding flash of default state on async backends; read hydrated() in reactive scopes. +license: MIT +metadata: + type: framework + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "solid" +requires: + - persist-core +sources: + - stainless-code/persist:src/adapters/frameworks/solid.ts +--- + +# Solid hydration gate + +This skill builds on `persist-core`. Read it first for `toHydrationSignal`. + +`@stainless-code/persist/frameworks/solid` returns an `Accessor` — call `hydrated()` inside tracking scopes (`createEffect`, JSX, …). + +## Install + +```bash +bun add @stainless-code/persist solid-js +``` + +Peer: `solid-js` `>=1.6.0`. + +## Minimal wiring + +```tsx +import { useHydrated } from "@stainless-code/persist/frameworks/solid"; + +const hydrated = useHydrated(prefsHydration); +// }>… +``` + +## Common mistakes + +- **Using `hydrated` as a boolean** — it's an accessor; call it. +- **Reading outside a reactive scope** — won't update. +- **Null signal ≠ loading** → hydrated true. + +## API surface + +- `useHydrated(signal) → Accessor` + +See also: `persist-core`. diff --git a/skills/persist-svelte-store/SKILL.md b/skills/persist-svelte-store/SKILL.md new file mode 100644 index 0000000..dac87d5 --- /dev/null +++ b/skills/persist-svelte-store/SKILL.md @@ -0,0 +1,55 @@ +--- +name: persist-svelte-store +description: Gate Svelte store-based UI on Persist hydration with hydratedStore (Readable). Use for Svelte 3–4 or Svelte 5 apps that still use stores; runes UI → persist-svelte. +license: MIT +metadata: + type: framework + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "svelte-store" +requires: + - persist-core +sources: + - stainless-code/persist:src/adapters/frameworks/svelte-store.ts +--- + +# Svelte stores hydration gate + +This skill builds on `persist-core`. Read it first for `toHydrationSignal`. + +`@stainless-code/persist/frameworks/svelte-store` exports `hydratedStore(signal) → Readable`. Use `$hydrated` auto-subscribe in templates. **Separate public entry** from `./frameworks/svelte` (runes). + +## Install + +```bash +bun add @stainless-code/persist svelte +``` + +Peer: `svelte` `>=3.0.0`. + +## Minimal wiring + +```svelte + + +{#if !$hydrated} + +{:else} + +{/if} +``` + +## Common mistakes + +- **Using this for Svelte 5 runes UI.** Use `persist-svelte` / `hydratedRune`. +- **Forgetting the `$` prefix** in templates. +- **Null signal ≠ loading** → `readable(true)`. + +## API surface + +- `hydratedStore(signal) → Readable` + +See also: `persist-svelte`; `persist-core`. diff --git a/skills/persist-svelte/SKILL.md b/skills/persist-svelte/SKILL.md new file mode 100644 index 0000000..8706245 --- /dev/null +++ b/skills/persist-svelte/SKILL.md @@ -0,0 +1,57 @@ +--- +name: persist-svelte +description: Gate Svelte 5 runes UI on Persist hydration with hydratedRune (.current). Use for Svelte 5; for stores / Svelte 3–4 use persist-svelte-store. +license: MIT +metadata: + type: framework + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "svelte" +requires: + - persist-core +sources: + - stainless-code/persist:src/adapters/frameworks/svelte.ts +--- + +# Svelte 5 runes hydration gate + +This skill builds on `persist-core`. Read it first for `toHydrationSignal`. + +`@stainless-code/persist/frameworks/svelte` exports `hydratedRune(signal)` → `{ readonly current: boolean }`. Read `.current` inside `$derived` / `$effect` / `{#if}` (createSubscriber is lazy inside a reactive owner). + +**Stores / Svelte 3–4 / Svelte 5 store users → `persist-svelte-store`** (`./frameworks/svelte-store`). + +## Install + +```bash +bun add @stainless-code/persist svelte +``` + +Needs Svelte **5.7+** for `svelte/reactivity` `createSubscriber` (package peer allows `>=3` because of the separate store entry). + +## Minimal wiring + +```svelte + + +{#if !hydrated.current} + +{:else} + +{/if} +``` + +## Common mistakes + +- **Using this entry for store-based UI.** Import `persist-svelte-store`. +- **Reading `.current` outside a reactive owner** — subscribe is a no-op. +- **Expecting a `useHydrated` name** — runes API is `hydratedRune`. + +## API surface + +- `hydratedRune(signal) → { readonly current: boolean }` + +See also: `persist-svelte-store`; `persist-core`. diff --git a/skills/persist-vue/SKILL.md b/skills/persist-vue/SKILL.md new file mode 100644 index 0000000..5d2b05d --- /dev/null +++ b/skills/persist-vue/SKILL.md @@ -0,0 +1,56 @@ +--- +name: persist-vue +description: Gate Vue 3 UI on Persist hydration with useHydrated (shallowRef). Use in setup/effectScope when avoiding flash of default state on async backends. +license: MIT +metadata: + type: framework + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "vue" +requires: + - persist-core +sources: + - stainless-code/persist:src/adapters/frameworks/vue.ts +--- + +# Vue hydration gate + +This skill builds on `persist-core`. Read it first for `toHydrationSignal`. + +`@stainless-code/persist/frameworks/vue` returns a `Ref` (`shallowRef`). Call inside `setup()` / an active `effectScope()` so `onScopeDispose` unsubscribes. + +## Install + +```bash +bun add @stainless-code/persist vue +``` + +Peer: `vue` `>=3.3.0`. + +## Minimal wiring + +```ts +import { toHydrationSignal } from "@stainless-code/persist"; +import { useHydrated } from "@stainless-code/persist/frameworks/vue"; + +const prefsHydration = toHydrationSignal(persist); +// in setup(): +const hydrated = useHydrated(prefsHydration); +``` + +```vue + + +``` + +## Common mistakes + +- **Calling outside setup / scope** — leak or no teardown. +- **Treating the ref as store state.** +- **SSR expects `false`.** Server renders hydrated `true`. + +## API surface + +- `useHydrated(signal) → Ref` + +See also: `persist-pinia` for Pinia store wiring; `persist-core`. From 656561fee21922e446a2c4f2d0eaad551e50aaba Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Thu, 23 Jul 2026 08:55:49 +0300 Subject: [PATCH 04/10] chore(skills): frameworks as -persist; core as persist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sources stay persist-*; framework leaves flip to Intent-style react-persist / vue-persist / …; persist-core becomes persist. --- .changeset/intent-skills-rename.md | 2 +- skills/_artifacts/skill_tree.yaml | 51 ++++++++++--------- .../SKILL.md | 8 +-- .../SKILL.md | 8 +-- skills/{persist-lit => lit-persist}/SKILL.md | 8 +-- skills/persist-pinia/SKILL.md | 2 +- skills/persist-zustand/SKILL.md | 2 +- skills/{persist-core => persist}/SKILL.md | 10 ++-- .../SKILL.md | 8 +-- .../{persist-react => react-persist}/SKILL.md | 8 +-- .../{persist-solid => solid-persist}/SKILL.md | 8 +-- .../SKILL.md | 14 ++--- .../SKILL.md | 12 ++--- skills/{persist-vue => vue-persist}/SKILL.md | 8 +-- 14 files changed, 76 insertions(+), 73 deletions(-) rename skills/{persist-alpine => alpine-persist}/SKILL.md (91%) rename skills/{persist-angular => angular-persist}/SKILL.md (89%) rename skills/{persist-lit => lit-persist}/SKILL.md (91%) rename skills/{persist-core => persist}/SKILL.md (89%) rename skills/{persist-preact => preact-persist}/SKILL.md (88%) rename skills/{persist-react => react-persist}/SKILL.md (89%) rename skills/{persist-solid => solid-persist}/SKILL.md (88%) rename skills/{persist-svelte => svelte-persist}/SKILL.md (77%) rename skills/{persist-svelte-store => svelte-store-persist}/SKILL.md (80%) rename skills/{persist-vue => vue-persist}/SKILL.md (87%) diff --git a/.changeset/intent-skills-rename.md b/.changeset/intent-skills-rename.md index 2271de3..5151806 100644 --- a/.changeset/intent-skills-rename.md +++ b/.changeset/intent-skills-rename.md @@ -2,4 +2,4 @@ "@stainless-code/persist": patch --- -Ship Intent skills covering persist core, every `./sources/*` adapter, and every `./frameworks/*` hydration adapter (including separate `persist-svelte` / `persist-svelte-store`). Renames the old `tanstack-store` leaf to `persist-tanstack-store` — re-run `npx @tanstack/intent@latest install` if an agent config still points at `skills/tanstack-store`. +Ship Intent skills for persist core (`persist`), every `./sources/*` adapter (`persist-*`), and every `./frameworks/*` hydration adapter (`-persist`, including `svelte-persist` / `svelte-store-persist`). Renames the old `tanstack-store` leaf — re-run `npx @tanstack/intent@latest install` if an agent config still points at `skills/tanstack-store` or `persist-*` framework leaves. diff --git a/skills/_artifacts/skill_tree.yaml b/skills/_artifacts/skill_tree.yaml index ebe4b88..039bb4a 100644 --- a/skills/_artifacts/skill_tree.yaml +++ b/skills/_artifacts/skill_tree.yaml @@ -1,5 +1,10 @@ # Intent skill tree for @stainless-code/persist. # `_artifacts/` is excluded from the npm tarball. +# +# Naming: +# - core → `persist` +# - frameworks → `-persist` +# - sources / codecs / backends / transport → `persist-` library: name: "@stainless-code/persist" version: "0.4.0" @@ -7,15 +12,13 @@ library: description: "Hydration-aware persistence for any reactive store" generated_at: "2026-07-23" -# Flat leaves (`persist-`) avoid clashing with upstream library skill names. -# Intent load: `@stainless-code/persist#`. skills: - - slug: persist-core + - slug: persist type: core - path: skills/persist-core/SKILL.md + path: skills/persist/SKILL.md status: shipped - # Sources (composition) + # Sources (composition) — persist- - slug: persist-tanstack-store type: composition path: skills/persist-tanstack-store/SKILL.md @@ -45,40 +48,40 @@ skills: path: skills/persist-redux/SKILL.md status: shipped - # Frameworks (all require persist-core) - - slug: persist-react + # Frameworks — -persist (all require `persist`) + - slug: react-persist type: framework - path: skills/persist-react/SKILL.md + path: skills/react-persist/SKILL.md status: shipped - - slug: persist-preact + - slug: preact-persist type: framework - path: skills/persist-preact/SKILL.md + path: skills/preact-persist/SKILL.md status: shipped - - slug: persist-vue + - slug: vue-persist type: framework - path: skills/persist-vue/SKILL.md + path: skills/vue-persist/SKILL.md status: shipped - - slug: persist-solid + - slug: solid-persist type: framework - path: skills/persist-solid/SKILL.md + path: skills/solid-persist/SKILL.md status: shipped - - slug: persist-angular + - slug: angular-persist type: framework - path: skills/persist-angular/SKILL.md + path: skills/angular-persist/SKILL.md status: shipped - - slug: persist-lit + - slug: lit-persist type: framework - path: skills/persist-lit/SKILL.md + path: skills/lit-persist/SKILL.md status: shipped - - slug: persist-alpine + - slug: alpine-persist type: framework - path: skills/persist-alpine/SKILL.md + path: skills/alpine-persist/SKILL.md status: shipped - - slug: persist-svelte + - slug: svelte-persist type: framework - path: skills/persist-svelte/SKILL.md + path: skills/svelte-persist/SKILL.md status: shipped - - slug: persist-svelte-store + - slug: svelte-store-persist type: framework - path: skills/persist-svelte-store/SKILL.md + path: skills/svelte-store-persist/SKILL.md status: shipped diff --git a/skills/persist-alpine/SKILL.md b/skills/alpine-persist/SKILL.md similarity index 91% rename from skills/persist-alpine/SKILL.md rename to skills/alpine-persist/SKILL.md index ed198ae..7c6a754 100644 --- a/skills/persist-alpine/SKILL.md +++ b/skills/alpine-persist/SKILL.md @@ -1,5 +1,5 @@ --- -name: persist-alpine +name: alpine-persist description: Gate Alpine.js UI on Persist hydration via Alpine.plugin(persist) and useHydrated / $hydrated magic. Use when wiring HydrationSignal into Alpine data components. license: MIT metadata: @@ -8,14 +8,14 @@ metadata: library_version: "0.4.0" framework: "alpine" requires: - - persist-core + - persist sources: - stainless-code/persist:src/adapters/frameworks/alpine.ts --- # Alpine hydration gate -This skill builds on `persist-core`. Read it first for `toHydrationSignal`. +This skill builds on `persist`. Read it first for `toHydrationSignal`. `@stainless-code/persist/frameworks/alpine` is **plugin-first**: `Alpine.plugin(persist)` registers `$hydrated` and enables reactive `useHydrated`. Call `destroy()` from `Alpine.data` teardown. @@ -63,4 +63,4 @@ Template: `x-show="hydrated"` / `$hydrated` magic (cached per element). - default/`persist(Alpine)` — plugin - `useHydrated(signal) → { hydrated; destroy() }` -See also: `persist-core`. +See also: `persist`. diff --git a/skills/persist-angular/SKILL.md b/skills/angular-persist/SKILL.md similarity index 89% rename from skills/persist-angular/SKILL.md rename to skills/angular-persist/SKILL.md index c66a035..bb5e327 100644 --- a/skills/persist-angular/SKILL.md +++ b/skills/angular-persist/SKILL.md @@ -1,5 +1,5 @@ --- -name: persist-angular +name: angular-persist description: Gate Angular UI on Persist hydration with useHydrated (readonly Signal). Call inside an injection context; use when avoiding flash of default state on async backends. license: MIT metadata: @@ -8,14 +8,14 @@ metadata: library_version: "0.4.0" framework: "angular" requires: - - persist-core + - persist sources: - stainless-code/persist:src/adapters/frameworks/angular.ts --- # Angular hydration gate -This skill builds on `persist-core`. Read it first for `toHydrationSignal`. +This skill builds on `persist`. Read it first for `toHydrationSignal`. `@stainless-code/persist/frameworks/angular` returns a readonly `Signal`. Must be called in an **injection context** (`effect()` requires it — typically a component field initializer). @@ -55,4 +55,4 @@ export class PrefsComponent { - `useHydrated(signal) → Signal` (readonly) -See also: `persist-core`. +See also: `persist`. diff --git a/skills/persist-lit/SKILL.md b/skills/lit-persist/SKILL.md similarity index 91% rename from skills/persist-lit/SKILL.md rename to skills/lit-persist/SKILL.md index 0ab389d..c7211c8 100644 --- a/skills/persist-lit/SKILL.md +++ b/skills/lit-persist/SKILL.md @@ -1,5 +1,5 @@ --- -name: persist-lit +name: lit-persist description: Gate Lit element UI on Persist hydration with HydrationController. Use when wiring HydrationSignal into a ReactiveControllerHost; not a hook. license: MIT metadata: @@ -8,14 +8,14 @@ metadata: library_version: "0.4.0" framework: "lit" requires: - - persist-core + - persist sources: - stainless-code/persist:src/adapters/frameworks/lit.ts --- # Lit hydration gate -This skill builds on `persist-core`. Read it first for `toHydrationSignal`. +This skill builds on `persist`. Read it first for `toHydrationSignal`. `@stainless-code/persist/frameworks/lit` exports `HydrationController` — a `ReactiveController`, not a hook. Constructor calls `host.addController(this)`; subscribes on `hostConnected`, tears down on `hostDisconnected`. @@ -54,4 +54,4 @@ class PrefsEl extends LitElement { - `new HydrationController(host, signal)` — getter `hydrated: boolean` -See also: `persist-core`. +See also: `persist`. diff --git a/skills/persist-pinia/SKILL.md b/skills/persist-pinia/SKILL.md index 9a7f542..bde0c98 100644 --- a/skills/persist-pinia/SKILL.md +++ b/skills/persist-pinia/SKILL.md @@ -59,4 +59,4 @@ const persist = persistStore(store, { - `persistStore(store, options) → PersistApi` — `store` is a Pinia `Store` - Options / `PersistApi`: same as `persistSource` -See also: `persist-react` / Vue hydration adapters for UI gating on async backends. +See also: `react-persist` / Vue hydration adapters for UI gating on async backends. diff --git a/skills/persist-zustand/SKILL.md b/skills/persist-zustand/SKILL.md index 823502b..613aa84 100644 --- a/skills/persist-zustand/SKILL.md +++ b/skills/persist-zustand/SKILL.md @@ -23,7 +23,7 @@ sources: - You're migrating off `zustand/middleware`'s `persist`. - You need async-backend hydration gating (`useHydrated`) shared with other Persist sources. -Other sources → matching `persist-*` skill (`persist-jotai`, `persist-redux`, …). Hand-rolled shapes → `persistSource`. UI gate → `persist-react`. +Other sources → matching `persist-*` skill (`persist-jotai`, `persist-redux`, …). Hand-rolled shapes → `persistSource`. UI gate → `react-persist`. ## Install diff --git a/skills/persist-core/SKILL.md b/skills/persist/SKILL.md similarity index 89% rename from skills/persist-core/SKILL.md rename to skills/persist/SKILL.md index 96953e8..898c897 100644 --- a/skills/persist-core/SKILL.md +++ b/skills/persist/SKILL.md @@ -1,5 +1,5 @@ --- -name: persist-core +name: persist description: Core @stainless-code/persist concepts — persistSource, PersistOptions, PersistApi, hydration gate, throttle, cross-tab, migrate. Load before framework hydration skills or when wiring a custom PersistableSource. license: MIT metadata: @@ -12,7 +12,7 @@ sources: - stainless-code/persist:README.md --- -# Persist core +# Persist Zero-dep `persistSource(source, options)` owns hydrate → subscribe → write. First-party `./sources/*` adapters only supply a `PersistableSource` shape (`getState` / `setState` / `subscribe`). @@ -20,7 +20,7 @@ Zero-dep `persistSource(source, options)` owns hydrate → subscribe → write. - Custom store shape (no `persist-*` adapter). - Shared Options / `PersistApi` semantics across adapters. -- Before `persist-react` (and other framework hydration skills). +- Before `react-persist` (and other framework hydration skills). Library-specific wiring → the matching `persist-*` composition skill. @@ -66,7 +66,7 @@ const persist = persistSource( import { toHydrationSignal } from "@stainless-code/persist"; export const hydration = toHydrationSignal(persist); -// frameworks/react → useHydrated(hydration) — see persist-react +// frameworks/react → useHydrated(hydration) — see react-persist ``` ## Backend × codec (short) @@ -77,4 +77,4 @@ export const hydration = toHydrationSignal(persist); | `Set`/`Map`/`Date` | `localStorage` | seroval codec | | Structured clone | IndexedDB | `identityCodec` | -See also: framework skills (`persist-react`, `persist-vue`, `persist-svelte`, …) for UI gates; composition skills for each `./sources/*` adapter. +See also: framework skills (`react-persist`, `vue-persist`, `svelte-persist`, …) for UI gates; composition skills for each `./sources/*` adapter. diff --git a/skills/persist-preact/SKILL.md b/skills/preact-persist/SKILL.md similarity index 88% rename from skills/persist-preact/SKILL.md rename to skills/preact-persist/SKILL.md index d4ccad4..ccfd713 100644 --- a/skills/persist-preact/SKILL.md +++ b/skills/preact-persist/SKILL.md @@ -1,5 +1,5 @@ --- -name: persist-preact +name: preact-persist description: Gate Preact UI on Persist hydration with useHydrated (preact/compat useSyncExternalStore). Use when avoiding flash of default state on async backends or wiring HydrationSignal into Preact. license: MIT metadata: @@ -8,14 +8,14 @@ metadata: library_version: "0.4.0" framework: "preact" requires: - - persist-core + - persist sources: - stainless-code/persist:src/adapters/frameworks/preact.ts --- # Preact hydration gate -This skill builds on `persist-core`. Read it first for `toHydrationSignal`. +This skill builds on `persist`. Read it first for `toHydrationSignal`. `@stainless-code/persist/frameworks/preact` mirrors the React adapter via `preact/compat` `useSyncExternalStore` (SSR snapshot always `true`). Not a state source — pair with a `./sources/*` adapter. @@ -49,4 +49,4 @@ return ; - `useHydrated(signal) → { hydrated: boolean }` -See also: `persist-react` (same contract); `persist-core`. +See also: `react-persist` (same contract); `persist`. diff --git a/skills/persist-react/SKILL.md b/skills/react-persist/SKILL.md similarity index 89% rename from skills/persist-react/SKILL.md rename to skills/react-persist/SKILL.md index 5c496be..611d49e 100644 --- a/skills/persist-react/SKILL.md +++ b/skills/react-persist/SKILL.md @@ -1,5 +1,5 @@ --- -name: persist-react +name: react-persist description: Gate React UI on Persist hydration with useHydrated + toHydrationSignal. Use when avoiding flash of default state on async backends (IndexedDB), SSR snapshot true, or wiring HydrationSignal into React 18/19. license: MIT metadata: @@ -8,7 +8,7 @@ metadata: library_version: "0.4.0" framework: "react" requires: - - persist-core + - persist sources: - stainless-code/persist:src/adapters/frameworks/react.ts - stainless-code/persist:docs/architecture.md @@ -16,7 +16,7 @@ sources: # React hydration gate -This skill builds on `persist-core`. Read it first for `persistSource` / Options / `toHydrationSignal`. +This skill builds on `persist`. Read it first for `persistSource` / Options / `toHydrationSignal`. `@stainless-code/persist/frameworks/react` exports `useHydrated(signal)` — a thin `useSyncExternalStore` wrapper over `HydrationSignal`. It does **not** persist state; pair it with a `./sources/*` adapter (`persist-zustand`, `persist-tanstack-store`, …). @@ -73,4 +73,4 @@ return ; - `useHydrated(signal: HydrationSignal | null | undefined) → { hydrated: boolean }` - Core helper: `toHydrationSignal(persist)` from `@stainless-code/persist` -See also: the `persist-*` source skill for your store library; sibling framework skills (`persist-vue`, `persist-solid`, `persist-svelte`, `persist-svelte-store`, `persist-preact`, `persist-angular`, `persist-lit`, `persist-alpine`). +See also: the `persist-*` source skill for your store library; sibling framework skills (`vue-persist`, `solid-persist`, `svelte-persist`, `svelte-store-persist`, `preact-persist`, `angular-persist`, `lit-persist`, `alpine-persist`). diff --git a/skills/persist-solid/SKILL.md b/skills/solid-persist/SKILL.md similarity index 88% rename from skills/persist-solid/SKILL.md rename to skills/solid-persist/SKILL.md index 6396460..c8b9035 100644 --- a/skills/persist-solid/SKILL.md +++ b/skills/solid-persist/SKILL.md @@ -1,5 +1,5 @@ --- -name: persist-solid +name: solid-persist description: Gate Solid UI on Persist hydration with useHydrated (Accessor). Use when avoiding flash of default state on async backends; read hydrated() in reactive scopes. license: MIT metadata: @@ -8,14 +8,14 @@ metadata: library_version: "0.4.0" framework: "solid" requires: - - persist-core + - persist sources: - stainless-code/persist:src/adapters/frameworks/solid.ts --- # Solid hydration gate -This skill builds on `persist-core`. Read it first for `toHydrationSignal`. +This skill builds on `persist`. Read it first for `toHydrationSignal`. `@stainless-code/persist/frameworks/solid` returns an `Accessor` — call `hydrated()` inside tracking scopes (`createEffect`, JSX, …). @@ -46,4 +46,4 @@ const hydrated = useHydrated(prefsHydration); - `useHydrated(signal) → Accessor` -See also: `persist-core`. +See also: `persist`. diff --git a/skills/persist-svelte/SKILL.md b/skills/svelte-persist/SKILL.md similarity index 77% rename from skills/persist-svelte/SKILL.md rename to skills/svelte-persist/SKILL.md index 8706245..9c6e9ee 100644 --- a/skills/persist-svelte/SKILL.md +++ b/skills/svelte-persist/SKILL.md @@ -1,6 +1,6 @@ --- -name: persist-svelte -description: Gate Svelte 5 runes UI on Persist hydration with hydratedRune (.current). Use for Svelte 5; for stores / Svelte 3–4 use persist-svelte-store. +name: svelte-persist +description: Gate Svelte 5 runes UI on Persist hydration with hydratedRune (.current). Use for Svelte 5; for stores / Svelte 3–4 use svelte-store-persist. license: MIT metadata: type: framework @@ -8,18 +8,18 @@ metadata: library_version: "0.4.0" framework: "svelte" requires: - - persist-core + - persist sources: - stainless-code/persist:src/adapters/frameworks/svelte.ts --- # Svelte 5 runes hydration gate -This skill builds on `persist-core`. Read it first for `toHydrationSignal`. +This skill builds on `persist`. Read it first for `toHydrationSignal`. `@stainless-code/persist/frameworks/svelte` exports `hydratedRune(signal)` → `{ readonly current: boolean }`. Read `.current` inside `$derived` / `$effect` / `{#if}` (createSubscriber is lazy inside a reactive owner). -**Stores / Svelte 3–4 / Svelte 5 store users → `persist-svelte-store`** (`./frameworks/svelte-store`). +**Stores / Svelte 3–4 / Svelte 5 store users → `svelte-store-persist`** (`./frameworks/svelte-store`). ## Install @@ -46,7 +46,7 @@ Needs Svelte **5.7+** for `svelte/reactivity` `createSubscriber` (package peer a ## Common mistakes -- **Using this entry for store-based UI.** Import `persist-svelte-store`. +- **Using this entry for store-based UI.** Import `svelte-store-persist`. - **Reading `.current` outside a reactive owner** — subscribe is a no-op. - **Expecting a `useHydrated` name** — runes API is `hydratedRune`. @@ -54,4 +54,4 @@ Needs Svelte **5.7+** for `svelte/reactivity` `createSubscriber` (package peer a - `hydratedRune(signal) → { readonly current: boolean }` -See also: `persist-svelte-store`; `persist-core`. +See also: `svelte-store-persist`; `persist`. diff --git a/skills/persist-svelte-store/SKILL.md b/skills/svelte-store-persist/SKILL.md similarity index 80% rename from skills/persist-svelte-store/SKILL.md rename to skills/svelte-store-persist/SKILL.md index dac87d5..3cb33a3 100644 --- a/skills/persist-svelte-store/SKILL.md +++ b/skills/svelte-store-persist/SKILL.md @@ -1,6 +1,6 @@ --- -name: persist-svelte-store -description: Gate Svelte store-based UI on Persist hydration with hydratedStore (Readable). Use for Svelte 3–4 or Svelte 5 apps that still use stores; runes UI → persist-svelte. +name: svelte-store-persist +description: Gate Svelte store-based UI on Persist hydration with hydratedStore (Readable). Use for Svelte 3–4 or Svelte 5 apps that still use stores; runes UI → svelte-persist. license: MIT metadata: type: framework @@ -8,14 +8,14 @@ metadata: library_version: "0.4.0" framework: "svelte-store" requires: - - persist-core + - persist sources: - stainless-code/persist:src/adapters/frameworks/svelte-store.ts --- # Svelte stores hydration gate -This skill builds on `persist-core`. Read it first for `toHydrationSignal`. +This skill builds on `persist`. Read it first for `toHydrationSignal`. `@stainless-code/persist/frameworks/svelte-store` exports `hydratedStore(signal) → Readable`. Use `$hydrated` auto-subscribe in templates. **Separate public entry** from `./frameworks/svelte` (runes). @@ -44,7 +44,7 @@ Peer: `svelte` `>=3.0.0`. ## Common mistakes -- **Using this for Svelte 5 runes UI.** Use `persist-svelte` / `hydratedRune`. +- **Using this for Svelte 5 runes UI.** Use `svelte-persist` / `hydratedRune`. - **Forgetting the `$` prefix** in templates. - **Null signal ≠ loading** → `readable(true)`. @@ -52,4 +52,4 @@ Peer: `svelte` `>=3.0.0`. - `hydratedStore(signal) → Readable` -See also: `persist-svelte`; `persist-core`. +See also: `svelte-persist`; `persist`. diff --git a/skills/persist-vue/SKILL.md b/skills/vue-persist/SKILL.md similarity index 87% rename from skills/persist-vue/SKILL.md rename to skills/vue-persist/SKILL.md index 5d2b05d..4e34fab 100644 --- a/skills/persist-vue/SKILL.md +++ b/skills/vue-persist/SKILL.md @@ -1,5 +1,5 @@ --- -name: persist-vue +name: vue-persist description: Gate Vue 3 UI on Persist hydration with useHydrated (shallowRef). Use in setup/effectScope when avoiding flash of default state on async backends. license: MIT metadata: @@ -8,14 +8,14 @@ metadata: library_version: "0.4.0" framework: "vue" requires: - - persist-core + - persist sources: - stainless-code/persist:src/adapters/frameworks/vue.ts --- # Vue hydration gate -This skill builds on `persist-core`. Read it first for `toHydrationSignal`. +This skill builds on `persist`. Read it first for `toHydrationSignal`. `@stainless-code/persist/frameworks/vue` returns a `Ref` (`shallowRef`). Call inside `setup()` / an active `effectScope()` so `onScopeDispose` unsubscribes. @@ -53,4 +53,4 @@ const hydrated = useHydrated(prefsHydration); - `useHydrated(signal) → Ref` -See also: `persist-pinia` for Pinia store wiring; `persist-core`. +See also: `persist-pinia` for Pinia store wiring; `persist`. From efa9b5782c5e3bc1364430db2141e441d709fd05 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Thu, 23 Jul 2026 08:57:50 +0300 Subject: [PATCH 05/10] chore(skills): Intent skills for codecs, backends, and crosstab Cover seroval, standard-schema, idb, async-storage, mmkv, secure-store, encrypted, compressed, node-fs, and BroadcastChannel crosstab as persist-* composition leaves so agent guidance matches every opt-in subpath. --- .changeset/intent-skills-rename.md | 2 +- skills/_artifacts/skill_tree.yaml | 50 ++++++++++++++++++++- skills/persist-async-storage/SKILL.md | 48 ++++++++++++++++++++ skills/persist-compressed/SKILL.md | 46 ++++++++++++++++++++ skills/persist-crosstab/SKILL.md | 49 +++++++++++++++++++++ skills/persist-encrypted/SKILL.md | 49 +++++++++++++++++++++ skills/persist-idb/SKILL.md | 56 ++++++++++++++++++++++++ skills/persist-mmkv/SKILL.md | 48 ++++++++++++++++++++ skills/persist-node-fs/SKILL.md | 46 ++++++++++++++++++++ skills/persist-secure-store/SKILL.md | 53 ++++++++++++++++++++++ skills/persist-seroval/SKILL.md | 53 ++++++++++++++++++++++ skills/persist-standard-schema/SKILL.md | 58 +++++++++++++++++++++++++ skills/persist/SKILL.md | 2 +- 13 files changed, 556 insertions(+), 4 deletions(-) create mode 100644 skills/persist-async-storage/SKILL.md create mode 100644 skills/persist-compressed/SKILL.md create mode 100644 skills/persist-crosstab/SKILL.md create mode 100644 skills/persist-encrypted/SKILL.md create mode 100644 skills/persist-idb/SKILL.md create mode 100644 skills/persist-mmkv/SKILL.md create mode 100644 skills/persist-node-fs/SKILL.md create mode 100644 skills/persist-secure-store/SKILL.md create mode 100644 skills/persist-seroval/SKILL.md create mode 100644 skills/persist-standard-schema/SKILL.md diff --git a/.changeset/intent-skills-rename.md b/.changeset/intent-skills-rename.md index 5151806..2cb3b1c 100644 --- a/.changeset/intent-skills-rename.md +++ b/.changeset/intent-skills-rename.md @@ -2,4 +2,4 @@ "@stainless-code/persist": patch --- -Ship Intent skills for persist core (`persist`), every `./sources/*` adapter (`persist-*`), and every `./frameworks/*` hydration adapter (`-persist`, including `svelte-persist` / `svelte-store-persist`). Renames the old `tanstack-store` leaf — re-run `npx @tanstack/intent@latest install` if an agent config still points at `skills/tanstack-store` or `persist-*` framework leaves. +Ship Intent skills for the full public surface: core (`persist`), every `./sources/*` (`persist-*`), every `./codecs/*` / `./backends/*` / `./transport/*` (`persist-*`), and every `./frameworks/*` (`-persist`). Renames the old `tanstack-store` leaf — re-run `npx @tanstack/intent@latest install` if an agent config still points at stale paths. diff --git a/skills/_artifacts/skill_tree.yaml b/skills/_artifacts/skill_tree.yaml index 039bb4a..7b1d461 100644 --- a/skills/_artifacts/skill_tree.yaml +++ b/skills/_artifacts/skill_tree.yaml @@ -18,7 +18,7 @@ skills: path: skills/persist/SKILL.md status: shipped - # Sources (composition) — persist- + # Sources - slug: persist-tanstack-store type: composition path: skills/persist-tanstack-store/SKILL.md @@ -48,7 +48,53 @@ skills: path: skills/persist-redux/SKILL.md status: shipped - # Frameworks — -persist (all require `persist`) + # Codecs + - slug: persist-seroval + type: composition + path: skills/persist-seroval/SKILL.md + status: shipped + - slug: persist-standard-schema + type: composition + path: skills/persist-standard-schema/SKILL.md + status: shipped + + # Backends + - slug: persist-idb + type: composition + path: skills/persist-idb/SKILL.md + status: shipped + - slug: persist-async-storage + type: composition + path: skills/persist-async-storage/SKILL.md + status: shipped + - slug: persist-mmkv + type: composition + path: skills/persist-mmkv/SKILL.md + status: shipped + - slug: persist-secure-store + type: composition + path: skills/persist-secure-store/SKILL.md + status: shipped + - slug: persist-encrypted + type: composition + path: skills/persist-encrypted/SKILL.md + status: shipped + - slug: persist-compressed + type: composition + path: skills/persist-compressed/SKILL.md + status: shipped + - slug: persist-node-fs + type: composition + path: skills/persist-node-fs/SKILL.md + status: shipped + + # Transport + - slug: persist-crosstab + type: composition + path: skills/persist-crosstab/SKILL.md + status: shipped + + # Frameworks — -persist - slug: react-persist type: framework path: skills/react-persist/SKILL.md diff --git a/skills/persist-async-storage/SKILL.md b/skills/persist-async-storage/SKILL.md new file mode 100644 index 0000000..44a364a --- /dev/null +++ b/skills/persist-async-storage/SKILL.md @@ -0,0 +1,48 @@ +--- +name: persist-async-storage +description: Persist with React Native AsyncStorage via @stainless-code/persist/backends/async-storage. Use for RN string-wire JSON state; always gate with useHydrated. +license: MIT +metadata: + type: composition + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "async-storage" +requires: + - persist +sources: + - stainless-code/persist:src/adapters/backends/async-storage.ts +--- + +# AsyncStorage backend + +This skill builds on `persist`. + +JSON via `createJSONStorage` under the hood. Fully **async** → **`useHydrated` mandatory**. Factory does **not** accept `clearCorruptOnFailure` — use `createStorage(() => asyncStorageStateStorage(), jsonCodec(), opts)` when you need it. + +## Install + +```bash +bun add @stainless-code/persist @react-native-async-storage/async-storage +``` + +## Minimal wiring + +```ts +import { createAsyncStorage } from "@stainless-code/persist/backends/async-storage"; + +const storage = createAsyncStorage()!; +``` + +Optional custom instance for namespacing: `createAsyncStorage(myAsyncStorage)`. + +## Common mistakes + +- **Treating hydrate as sync.** +- **Expecting Set/Map/Date** — use seroval compose or avoid. +- **Passing `clearCorruptOnFailure` to the factory.** + +## API surface + +- `createAsyncStorage(storage?)` · `asyncStorageStateStorage(storage?)` + +See also: `persist-mmkv` (sync RN); `persist`. diff --git a/skills/persist-compressed/SKILL.md b/skills/persist-compressed/SKILL.md new file mode 100644 index 0000000..fe58161 --- /dev/null +++ b/skills/persist-compressed/SKILL.md @@ -0,0 +1,46 @@ +--- +name: persist-compressed +description: Compress a string-wire StateStorage with @stainless-code/persist/backends/compressed (gzip/deflate). Backend wrapper — compose with createStorage; stack compress-then-encrypt. +license: MIT +metadata: + type: composition + library: "@stainless-code/persist" + library_version: "0.4.0" +requires: + - persist +sources: + - stainless-code/persist:src/adapters/backends/compressed.ts +--- + +# Compressed backend wrapper + +This skill builds on `persist`. + +**Not a `StorageCodec`.** Uses `CompressionStream` / `DecompressionStream` (default `gzip`); output is **base64** on the string wire. Returns `undefined` if streams unavailable. Documented stack with encryption: **compress → encrypt**. + +## Minimal wiring + +```ts +import { createStorage } from "@stainless-code/persist"; +import { createCompressedStorage } from "@stainless-code/persist/backends/compressed"; +import { serovalCodec } from "@stainless-code/persist/codecs/seroval"; + +const storage = createStorage( + () => createCompressedStorage(() => localStorage)!, + serovalCodec(), +); +``` + +Formats: `gzip` | `deflate` | `deflate-raw` (`deflate-raw` needs newer Node). + +## Common mistakes + +- **Using as a codec.** +- **Encrypt-then-compress.** +- **Assuming streams exist in every runtime.** + +## API surface + +- `createCompressedStorage(getStorage, { format? }) → StateStorage | undefined` + +See also: `persist-encrypted`; `persist`. diff --git a/skills/persist-crosstab/SKILL.md b/skills/persist-crosstab/SKILL.md new file mode 100644 index 0000000..c6f60ed --- /dev/null +++ b/skills/persist-crosstab/SKILL.md @@ -0,0 +1,49 @@ +--- +name: persist-crosstab +description: Cross-tab sync for backends without storage events via @stainless-code/persist/transport/crosstab (createBroadcastCrossTab). Wrap storage + pass crossTabEventTarget; call close() on teardown. +license: MIT +metadata: + type: composition + library: "@stainless-code/persist" + library_version: "0.4.0" +requires: + - persist +sources: + - stainless-code/persist:src/adapters/transport/crosstab.ts +--- + +# BroadcastChannel cross-tab transport + +This skill builds on `persist`. + +For backends with **no** `storage` events (IndexedDB). Must **`wrap(storage)`** and pass `crossTabEventTarget` with `crossTab: true`. Posts `storageArea: null` (key-only match — each tab owns its backend instance). Returns `undefined` if `BroadcastChannel` missing. Call `close()` on teardown. + +## Minimal wiring + +```ts +import { createIdbStorage } from "@stainless-code/persist/backends/idb"; +import { createBroadcastCrossTab } from "@stainless-code/persist/transport/crosstab"; + +const bridge = createBroadcastCrossTab({ channelName: "app:prefs" })!; +const persist = persistStore(store, { + name: "app:prefs:v1", + storage: bridge.wrap(createIdbStorage()!), + crossTab: true, + crossTabEventTarget: bridge.crossTabEventTarget, +}); +// teardown: persist.destroy(); bridge.close(); +``` + +`localStorage` can use `crossTab: true` alone — this bridge is for IDB-like backends. + +## Common mistakes + +- **`crossTab: true` alone on IDB** — no native events. +- **Forgetting `wrap` or `close`.** +- **Expecting `storageArea` reference equality across tabs.** + +## API surface + +- `createBroadcastCrossTab({ channelName }) → { crossTabEventTarget, wrap, close } | undefined` + +See also: `persist-idb`; `persist`. diff --git a/skills/persist-encrypted/SKILL.md b/skills/persist-encrypted/SKILL.md new file mode 100644 index 0000000..a300ebb --- /dev/null +++ b/skills/persist-encrypted/SKILL.md @@ -0,0 +1,49 @@ +--- +name: persist-encrypted +description: AES-GCM encrypt a string-wire StateStorage with @stainless-code/persist/backends/encrypted (createEncryptedStorage). Backend wrapper — compose with createStorage + codec; wrong key rejects hydrate (not clearCorrupt). +license: MIT +metadata: + type: composition + library: "@stainless-code/persist" + library_version: "0.4.0" +requires: + - persist +sources: + - stainless-code/persist:src/adapters/backends/encrypted.ts +--- + +# Encrypted backend wrapper + +This skill builds on `persist`. + +**Not a `StorageCodec`.** Wraps an inner string `StateStorage` with WebCrypto AES-GCM (`base64(iv).base64(ct)`). Returns `undefined` if `crypto.subtle` missing. Stack with compression: **compress → encrypt**. + +Decrypt failure (wrong key / tamper) → backend `getItem` **rejects** → `onError` phase `"hydrate"` — **not** the codec `clearCorruptOnFailure` path. + +## Minimal wiring + +```ts +import { createStorage } from "@stainless-code/persist"; +import { createEncryptedStorage } from "@stainless-code/persist/backends/encrypted"; +import { serovalCodec } from "@stainless-code/persist/codecs/seroval"; + +const storage = createStorage( + () => createEncryptedStorage(() => localStorage, { key })!, + serovalCodec(), + { clearCorruptOnFailure: true }, +); +``` + +`key` is a `CryptoKey` for AES-GCM. + +## Common mistakes + +- **Treating this as a sync codec.** +- **Expecting clearCorrupt on decrypt fail.** +- **Encrypt-then-compress** — reverse of the documented stack. + +## API surface + +- `createEncryptedStorage(getStorage, { key }) → StateStorage | undefined` + +See also: `persist-compressed`; `persist-seroval`; `persist`. diff --git a/skills/persist-idb/SKILL.md b/skills/persist-idb/SKILL.md new file mode 100644 index 0000000..35abb34 --- /dev/null +++ b/skills/persist-idb/SKILL.md @@ -0,0 +1,56 @@ +--- +name: persist-idb +description: Persist with IndexedDB via @stainless-code/persist/backends/idb (createIdbStorage / idbStateStorage). Use for large or structured-clone state; pair with useHydrated and BroadcastChannel cross-tab. +license: MIT +metadata: + type: composition + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "idb-keyval" +requires: + - persist +sources: + - stainless-code/persist:src/adapters/backends/idb.ts +--- + +# IndexedDB backend + +This skill builds on `persist`. + +`createIdbStorage` uses **structured clone** + `identityCodec` — `Set`/`Map`/`Date` work without seroval. Fully **async** → gate UI. No `storage` events → use `persist-crosstab` for multi-tab. + +## Install + +```bash +bun add @stainless-code/persist idb-keyval +``` + +Peer: `idb-keyval` `>=4.0.0`. + +## Minimal wiring + +```ts +import { createIdbStorage } from "@stainless-code/persist/backends/idb"; + +const storage = createIdbStorage(); +``` + +Custom codec / encrypt: `createStorage(() => idbStateStorage(store), codec, opts)`. + +## Gotchas + +- **`clearCorruptOnFailure` is inert** on the default identity path (identity never throws). +- IDB missing → reject on first use (`onError` `"hydrate"`), not at construct. +- `crossTab: true` alone does nothing — need `persist-crosstab`. + +## Common mistakes + +- **Layering JSON/seroval on default IDB** when structured clone already covers the graph. +- **Skipping `useHydrated`.** +- **Expecting native `storage` events.** + +## API surface + +- `createIdbStorage(options?)` · `idbStateStorage(store?)` + +See also: `persist-crosstab`; `react-persist` (or other `*-persist`); `persist`. diff --git a/skills/persist-mmkv/SKILL.md b/skills/persist-mmkv/SKILL.md new file mode 100644 index 0000000..d2d01ae --- /dev/null +++ b/skills/persist-mmkv/SKILL.md @@ -0,0 +1,48 @@ +--- +name: persist-mmkv +description: Persist with react-native-mmkv via @stainless-code/persist/backends/mmkv (createMmkvStorage). Sync JSON string-wire; id required; optional MMKV encryptionKey (≤16 bytes). +license: MIT +metadata: + type: composition + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "mmkv" +requires: + - persist +sources: + - stainless-code/persist:src/adapters/backends/mmkv.ts +--- + +# MMKV backend + +This skill builds on `persist`. + +**Sync** backend — no hydrate UI gate required for flash. JSON string-wire. `id` namespaces the MMKV file. `encryptionKey` is MMKV's own key (max **16 bytes**), not `persist-encrypted`. + +## Install + +```bash +bun add @stainless-code/persist react-native-mmkv +``` + +Peer: `react-native-mmkv` `>=4.0.0`. + +## Minimal wiring + +```ts +import { createMmkvStorage } from "@stainless-code/persist/backends/mmkv"; + +const storage = createMmkvStorage({ id: "app-prefs" })!; +``` + +## Common mistakes + +- **Gating like IDB** — MMKV is sync. +- **Confusing `encryptionKey` with `backends/encrypted`.** +- **Omitting `id`.** + +## API surface + +- `createMmkvStorage({ id, path?, encryptionKey? })` · `mmkvStateStorage(instance)` + +See also: `persist-async-storage`; `persist-encrypted` for WebCrypto AES-GCM; `persist`. diff --git a/skills/persist-node-fs/SKILL.md b/skills/persist-node-fs/SKILL.md new file mode 100644 index 0000000..6f00f02 --- /dev/null +++ b/skills/persist-node-fs/SKILL.md @@ -0,0 +1,46 @@ +--- +name: persist-node-fs +description: Persist to the filesystem with @stainless-code/persist/backends/node-fs (nodeFsStateStorage). StateStorage only — compose createStorage + codec; one file per key with hash suffix. +license: MIT +metadata: + type: composition + library: "@stainless-code/persist" + library_version: "0.4.0" +requires: + - persist +sources: + - stainless-code/persist:src/adapters/backends/node-fs.ts +--- + +# Node filesystem backend + +This skill builds on `persist`. + +Exports **`nodeFsStateStorage({ dir })` only** — compose with `createStorage` + a codec. One file per key; sanitized name + **djb2 hash** suffix (collision-safe). Refuses keys that sanitize to `.` / `..` / `""`. ENOENT → null / no-op remove. + +## Minimal wiring + +```ts +import { createStorage } from "@stainless-code/persist"; +import { nodeFsStateStorage } from "@stainless-code/persist/backends/node-fs"; +import { serovalCodec } from "@stainless-code/persist/codecs/seroval"; + +const storage = createStorage( + () => nodeFsStateStorage({ dir: "./.persist" }), + serovalCodec(), +); +``` + +No optional peer — uses `node:fs`. + +## Common mistakes + +- **Expecting a `create*Storage` PersistStorage factory.** +- **Assuming sanitize alone makes unique paths** — the hash suffix is required for collisions. +- **Path-traversal keys** — refused after sanitize. + +## API surface + +- `nodeFsStateStorage({ dir }) → StateStorage` + +See also: `persist-seroval`; `persist`. diff --git a/skills/persist-secure-store/SKILL.md b/skills/persist-secure-store/SKILL.md new file mode 100644 index 0000000..ba9f6a4 --- /dev/null +++ b/skills/persist-secure-store/SKILL.md @@ -0,0 +1,53 @@ +--- +name: persist-secure-store +description: Persist small secrets with Expo SecureStore via @stainless-code/persist/backends/secure-store. ~2KB/key — use partialize; async → useHydrated. +license: MIT +metadata: + type: composition + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "expo-secure-store" +requires: + - persist +sources: + - stainless-code/persist:src/adapters/backends/secure-store.ts +--- + +# SecureStore backend + +This skill builds on `persist`. + +For **tokens / small secrets**, not full app state (~**2KB/key**). Async → gate UI. Keys sanitized to `/^[\w.-]+$/` (`:` → `_`). + +## Install + +```bash +bun add @stainless-code/persist expo-secure-store +``` + +Peer: `expo-secure-store` `>=12.0.0`. + +## Minimal wiring + +```ts +import { createSecureStoreStorage } from "@stainless-code/persist/backends/secure-store"; + +const storage = createSecureStoreStorage()!; +persistStore(store, { + name: "auth:token:v1", + storage, + partialize: (s) => s.token, +}); +``` + +## Common mistakes + +- **Storing large documents** — wrong backend. +- **Skipping `partialize`.** +- **Assuming colon keys are stored literally.** + +## API surface + +- `createSecureStoreStorage()` · `secureStoreStateStorage()` + +See also: `persist-mmkv`; `persist-encrypted`; `persist`. diff --git a/skills/persist-seroval/SKILL.md b/skills/persist-seroval/SKILL.md new file mode 100644 index 0000000..029e107 --- /dev/null +++ b/skills/persist-seroval/SKILL.md @@ -0,0 +1,53 @@ +--- +name: persist-seroval +description: Persist Set/Map/Date (and richer graphs) with @stainless-code/persist/codecs/seroval (serovalCodec / createSerovalStorage). Use when JSON.stringify would lose types on string-wire backends. +license: MIT +metadata: + type: composition + library: "@stainless-code/persist" + library_version: "0.4.0" + framework: "seroval" +requires: + - persist +sources: + - stainless-code/persist:src/adapters/codecs/seroval.ts +--- + +# Seroval codec + +This skill builds on `persist`. Read it first for `createStorage` / `clearCorruptOnFailure`. + +`@stainless-code/persist/codecs/seroval` round-trips values JSON cannot (`Set`/`Map`/`Date`) via `seroval` `toJSON`/`fromJSON` on string-wire backends (`localStorage`, etc.). + +## Install + +```bash +bun add @stainless-code/persist seroval +``` + +Peer: `seroval` `>=1.0.0` (optional of this subpath). + +## Minimal wiring + +```ts +import { createSerovalStorage } from "@stainless-code/persist/codecs/seroval"; + +const storage = createSerovalStorage(() => localStorage, { + clearCorruptOnFailure: true, +}); +``` + +Or `createStorage(getStorage, serovalCodec(), opts)`. + +## Common mistakes + +- **Using `createJSONStorage` for Set/Map/Date** — they won't round-trip. +- **Layering seroval on default IDB** — `persist-idb` already uses structured clone / `identityCodec`. +- **Forgetting the `seroval` peer.** + +## API surface + +- `serovalCodec() → StorageCodec` +- `createSerovalStorage(getStorage, options?) → PersistStorage | undefined` + +See also: `persist-idb` for structured-clone without seroval; `persist`. diff --git a/skills/persist-standard-schema/SKILL.md b/skills/persist-standard-schema/SKILL.md new file mode 100644 index 0000000..2862b99 --- /dev/null +++ b/skills/persist-standard-schema/SKILL.md @@ -0,0 +1,58 @@ +--- +name: persist-standard-schema +description: Validate persisted state with Standard Schema (~standard) via withStandardSchema / withStandardSchemaAsync / createStandardSchemaStorage. Use for Zod/Yup (etc.) sync vs async lanes and PersistDecodeRethrowError. +license: MIT +metadata: + type: composition + library: "@stainless-code/persist" + library_version: "0.4.0" +requires: + - persist +sources: + - stainless-code/persist:src/adapters/codecs/standard-schema.ts + - stainless-code/persist:apps/docs/content/recipes/standard-schema.mdx +--- + +# Standard Schema codec / wraps + +This skill builds on `persist`. Types for `~standard` are **vendored** — no runtime peer. Bring Zod / Yup / Valibot yourself. + +Validates persisted **`state` only** (not envelope `version` / `timestamp` / `buster`). Encode writes schema **output** (defaults/transforms applied). + +## Sync vs async lanes + +| API | When | +| -------------------------------------------------------------- | ------------------------------------------------ | +| `withStandardSchema` / `createStandardSchemaStorage` | Sync `validate` (Zod ≥3.24 / v4 via `~standard`) | +| `withStandardSchemaAsync` / `createStandardSchemaStorageAsync` | Async `validate` (Yup, async Zod) | + +Wrong lane: sync wrap + async schema → `PersistDecodeRethrowError` (rethrown; **not** `clearCorrupt`). Async lane → async hydrate → gate UI (`react-persist`, …). + +Sync wrap is still Promise-aware for async **backend** `getItem` (e.g. IDB). + +## Minimal wiring + +```ts +import { withStandardSchema } from "@stainless-code/persist/codecs/standard-schema"; +import { createIdbStorage } from "@stainless-code/persist/backends/idb"; + +const storage = withStandardSchema(createIdbStorage()!, prefsSchema, { + clearCorruptOnFailure: true, +}); +``` + +JSON sugar: `createStandardSchemaStorage(() => localStorage, schema)`. + +## Common mistakes + +- **Async schema on the sync wrap.** +- **Treating wrong-lane throws as corrupt** — they must not clear storage. +- **Schema-checking the whole envelope** — only `state`. +- **Skipping `useHydrated` on the async lane.** + +## API surface + +- `standardSchemaCodec(schema)` · `withStandardSchema` / `withStandardSchemaAsync` +- `createStandardSchemaStorage` / `createStandardSchemaStorageAsync` + +See also: `persist`; `persist-idb` for async backends. diff --git a/skills/persist/SKILL.md b/skills/persist/SKILL.md index 898c897..76c5832 100644 --- a/skills/persist/SKILL.md +++ b/skills/persist/SKILL.md @@ -77,4 +77,4 @@ export const hydration = toHydrationSignal(persist); | `Set`/`Map`/`Date` | `localStorage` | seroval codec | | Structured clone | IndexedDB | `identityCodec` | -See also: framework skills (`react-persist`, `vue-persist`, `svelte-persist`, …) for UI gates; composition skills for each `./sources/*` adapter. +See also: `persist-*` for sources / codecs / backends / transport; `*-persist` for UI frameworks. From a27c4ff341e4cb6a0b0dfe7e59163e041fe9e04f Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Thu, 23 Jul 2026 09:01:43 +0300 Subject: [PATCH 06/10] chore(skills): drop unused Intent _artifacts scaffolding --- .changeset/intent-skills-rename.md | 2 +- .github/workflows/check-skills.yml | 10 +-- package.json | 3 +- scripts/sync-skill-versions.ts | 2 +- skills/_artifacts/skill_tree.yaml | 133 ----------------------------- 5 files changed, 4 insertions(+), 146 deletions(-) delete mode 100644 skills/_artifacts/skill_tree.yaml diff --git a/.changeset/intent-skills-rename.md b/.changeset/intent-skills-rename.md index 2cb3b1c..39cd8de 100644 --- a/.changeset/intent-skills-rename.md +++ b/.changeset/intent-skills-rename.md @@ -2,4 +2,4 @@ "@stainless-code/persist": patch --- -Ship Intent skills for the full public surface: core (`persist`), every `./sources/*` (`persist-*`), every `./codecs/*` / `./backends/*` / `./transport/*` (`persist-*`), and every `./frameworks/*` (`-persist`). Renames the old `tanstack-store` leaf — re-run `npx @tanstack/intent@latest install` if an agent config still points at stale paths. +Ship Intent skills for the full public surface — core `persist`, `persist-*` (sources/codecs/backends/transport), and `-persist`. Re-run `npx @tanstack/intent@latest install` if agent config still points at `skills/tanstack-store`. diff --git a/.github/workflows/check-skills.yml b/.github/workflows/check-skills.yml index ab1e4a9..da536ed 100644 --- a/.github/workflows/check-skills.yml +++ b/.github/workflows/check-skills.yml @@ -1,10 +1,4 @@ -# check-skills.yml — Validates intent skills on PRs. After a release or manual -# run, opens or updates one review PR when existing skills, artifact coverage, -# or workspace package coverage need review. -# -# Triggers: pull requests touching skills/artifacts, new release published, or -# manual workflow_dispatch. -# +# Validate skills on PR; post-release/manual: open Intent stale-coverage review PR. # intent-workflow-version: 3 name: Check Skills @@ -14,8 +8,6 @@ on: paths: - "skills/**" - "**/skills/**" - - "_artifacts/**" - - "**/_artifacts/**" release: types: [published] workflow_dispatch: {} diff --git a/package.json b/package.json index 93b2a2a..8e007e7 100644 --- a/package.json +++ b/package.json @@ -55,8 +55,7 @@ ], "files": [ "dist", - "skills", - "!skills/_artifacts" + "skills" ], "type": "module", "sideEffects": false, diff --git a/scripts/sync-skill-versions.ts b/scripts/sync-skill-versions.ts index 6dbafa2..9a221c2 100755 --- a/scripts/sync-skill-versions.ts +++ b/scripts/sync-skill-versions.ts @@ -11,7 +11,7 @@ const FIELD = /^( *library_version:\s*")([^"]*)(")/m; function* walkSkillFiles(dir: string): Generator { for (const entry of readdirSync(dir, { withFileTypes: true })) { - if (!entry.isDirectory() || entry.name === "_artifacts") continue; + if (!entry.isDirectory()) continue; const nested = join(dir, entry.name); const skill = join(nested, "SKILL.md"); if (existsSync(skill)) yield skill; diff --git a/skills/_artifacts/skill_tree.yaml b/skills/_artifacts/skill_tree.yaml deleted file mode 100644 index 7b1d461..0000000 --- a/skills/_artifacts/skill_tree.yaml +++ /dev/null @@ -1,133 +0,0 @@ -# Intent skill tree for @stainless-code/persist. -# `_artifacts/` is excluded from the npm tarball. -# -# Naming: -# - core → `persist` -# - frameworks → `-persist` -# - sources / codecs / backends / transport → `persist-` -library: - name: "@stainless-code/persist" - version: "0.4.0" - repository: "https://github.com/stainless-code/persist" - description: "Hydration-aware persistence for any reactive store" -generated_at: "2026-07-23" - -skills: - - slug: persist - type: core - path: skills/persist/SKILL.md - status: shipped - - # Sources - - slug: persist-tanstack-store - type: composition - path: skills/persist-tanstack-store/SKILL.md - status: shipped - - slug: persist-zustand - type: composition - path: skills/persist-zustand/SKILL.md - status: shipped - - slug: persist-jotai - type: composition - path: skills/persist-jotai/SKILL.md - status: shipped - - slug: persist-valtio - type: composition - path: skills/persist-valtio/SKILL.md - status: shipped - - slug: persist-mobx - type: composition - path: skills/persist-mobx/SKILL.md - status: shipped - - slug: persist-pinia - type: composition - path: skills/persist-pinia/SKILL.md - status: shipped - - slug: persist-redux - type: composition - path: skills/persist-redux/SKILL.md - status: shipped - - # Codecs - - slug: persist-seroval - type: composition - path: skills/persist-seroval/SKILL.md - status: shipped - - slug: persist-standard-schema - type: composition - path: skills/persist-standard-schema/SKILL.md - status: shipped - - # Backends - - slug: persist-idb - type: composition - path: skills/persist-idb/SKILL.md - status: shipped - - slug: persist-async-storage - type: composition - path: skills/persist-async-storage/SKILL.md - status: shipped - - slug: persist-mmkv - type: composition - path: skills/persist-mmkv/SKILL.md - status: shipped - - slug: persist-secure-store - type: composition - path: skills/persist-secure-store/SKILL.md - status: shipped - - slug: persist-encrypted - type: composition - path: skills/persist-encrypted/SKILL.md - status: shipped - - slug: persist-compressed - type: composition - path: skills/persist-compressed/SKILL.md - status: shipped - - slug: persist-node-fs - type: composition - path: skills/persist-node-fs/SKILL.md - status: shipped - - # Transport - - slug: persist-crosstab - type: composition - path: skills/persist-crosstab/SKILL.md - status: shipped - - # Frameworks — -persist - - slug: react-persist - type: framework - path: skills/react-persist/SKILL.md - status: shipped - - slug: preact-persist - type: framework - path: skills/preact-persist/SKILL.md - status: shipped - - slug: vue-persist - type: framework - path: skills/vue-persist/SKILL.md - status: shipped - - slug: solid-persist - type: framework - path: skills/solid-persist/SKILL.md - status: shipped - - slug: angular-persist - type: framework - path: skills/angular-persist/SKILL.md - status: shipped - - slug: lit-persist - type: framework - path: skills/lit-persist/SKILL.md - status: shipped - - slug: alpine-persist - type: framework - path: skills/alpine-persist/SKILL.md - status: shipped - - slug: svelte-persist - type: framework - path: skills/svelte-persist/SKILL.md - status: shipped - - slug: svelte-store-persist - type: framework - path: skills/svelte-store-persist/SKILL.md - status: shipped From 6a59f2426d93039d2b917e818e119ad98d933ca7 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Thu, 23 Jul 2026 09:05:15 +0300 Subject: [PATCH 07/10] fix(skills): fact-check Intent skills against adapters Correct migrate/one-shot, alpine $hydrated bag, IDB encrypt compose, pinia/$state+merge, jotai readonly typing; slim tanstack-store sprawl and drop no-op persist pointers (requires already loads core). --- skills/alpine-persist/SKILL.md | 14 +-- skills/angular-persist/SKILL.md | 6 +- skills/lit-persist/SKILL.md | 6 +- skills/persist-async-storage/SKILL.md | 8 +- skills/persist-compressed/SKILL.md | 6 +- skills/persist-crosstab/SKILL.md | 4 +- skills/persist-encrypted/SKILL.md | 4 +- skills/persist-idb/SKILL.md | 8 +- skills/persist-jotai/SKILL.md | 6 +- skills/persist-mmkv/SKILL.md | 6 +- skills/persist-node-fs/SKILL.md | 4 +- skills/persist-pinia/SKILL.md | 6 +- skills/persist-secure-store/SKILL.md | 4 +- skills/persist-seroval/SKILL.md | 4 +- skills/persist-standard-schema/SKILL.md | 3 +- skills/persist-tanstack-store/SKILL.md | 133 +++--------------------- skills/persist-zustand/SKILL.md | 6 +- skills/persist/SKILL.md | 5 +- skills/preact-persist/SKILL.md | 4 +- skills/react-persist/SKILL.md | 4 +- skills/solid-persist/SKILL.md | 8 +- skills/svelte-persist/SKILL.md | 8 +- skills/svelte-store-persist/SKILL.md | 8 +- skills/vue-persist/SKILL.md | 6 +- 24 files changed, 70 insertions(+), 201 deletions(-) diff --git a/skills/alpine-persist/SKILL.md b/skills/alpine-persist/SKILL.md index 7c6a754..1d71348 100644 --- a/skills/alpine-persist/SKILL.md +++ b/skills/alpine-persist/SKILL.md @@ -1,6 +1,6 @@ --- name: alpine-persist -description: Gate Alpine.js UI on Persist hydration via Alpine.plugin(persist) and useHydrated / $hydrated magic. Use when wiring HydrationSignal into Alpine data components. +description: Gate Alpine.js UI on Persist hydration (Alpine.plugin + useHydrated / $hydrated). Use when wiring HydrationSignal into Alpine data components. license: MIT metadata: type: framework @@ -15,8 +15,6 @@ sources: # Alpine hydration gate -This skill builds on `persist`. Read it first for `toHydrationSignal`. - `@stainless-code/persist/frameworks/alpine` is **plugin-first**: `Alpine.plugin(persist)` registers `$hydrated` and enables reactive `useHydrated`. Call `destroy()` from `Alpine.data` teardown. ## Install @@ -50,17 +48,15 @@ Alpine.data("prefs", () => { }); ``` -Template: `x-show="hydrated"` / `$hydrated` magic (cached per element). +Template: `x-show="$hydrated(prefsHydration).hydrated"` — `$hydrated(signal)` returns a **bag** (`{ hydrated, destroy }`), not a boolean (a bare bag is always truthy in `x-show`). Magic caches per element. ## Common mistakes - **Skipping `Alpine.plugin(persist)`.** `useHydrated` falls back to a plain object + one-time non-prod warn. +- **`x-show="$hydrated(...)"` without `.hydrated`.** - **Not forwarding `destroy()`** from `Alpine.data`. -- **Stacking manual `$hydrated` subscriptions** — magic already caches per element. ## API surface -- default/`persist(Alpine)` — plugin -- `useHydrated(signal) → { hydrated; destroy() }` - -See also: `persist`. +- default/`persist(Alpine)` — plugin; registers `$hydrated(signal) → HydratedBag` +- `useHydrated(signal) → { hydrated; destroy() }` (null signal → `{ hydrated: true }`) diff --git a/skills/angular-persist/SKILL.md b/skills/angular-persist/SKILL.md index bb5e327..90919e2 100644 --- a/skills/angular-persist/SKILL.md +++ b/skills/angular-persist/SKILL.md @@ -15,9 +15,7 @@ sources: # Angular hydration gate -This skill builds on `persist`. Read it first for `toHydrationSignal`. - -`@stainless-code/persist/frameworks/angular` returns a readonly `Signal`. Must be called in an **injection context** (`effect()` requires it — typically a component field initializer). +`@stainless-code/persist/frameworks/angular` returns a readonly `Signal`. A real signal needs an **injection context** (`effect()`); null/undefined returns a shared `signal(true)` without `effect()`. ## Install @@ -54,5 +52,3 @@ export class PrefsComponent { ## API surface - `useHydrated(signal) → Signal` (readonly) - -See also: `persist`. diff --git a/skills/lit-persist/SKILL.md b/skills/lit-persist/SKILL.md index c7211c8..7a86ae9 100644 --- a/skills/lit-persist/SKILL.md +++ b/skills/lit-persist/SKILL.md @@ -15,8 +15,6 @@ sources: # Lit hydration gate -This skill builds on `persist`. Read it first for `toHydrationSignal`. - `@stainless-code/persist/frameworks/lit` exports `HydrationController` — a `ReactiveController`, not a hook. Constructor calls `host.addController(this)`; subscribes on `hostConnected`, tears down on `hostDisconnected`. ## Install @@ -47,11 +45,9 @@ class PrefsEl extends LitElement { ## Common mistakes - **Inventing `useHydrated` for Lit.** Use the controller. -- **Reading `hydrated` once at construct** without rendering from it after connect (reconnect / late hydrate need `requestUpdate`). +- **Caching `hydrated` outside render** — the controller already `requestUpdate`s on connect/change; read the getter in `render()`. - **Null signal ≠ loading** → `hydrated` true, no subscribe. ## API surface - `new HydrationController(host, signal)` — getter `hydrated: boolean` - -See also: `persist`. diff --git a/skills/persist-async-storage/SKILL.md b/skills/persist-async-storage/SKILL.md index 44a364a..36bfbc9 100644 --- a/skills/persist-async-storage/SKILL.md +++ b/skills/persist-async-storage/SKILL.md @@ -6,7 +6,7 @@ metadata: type: composition library: "@stainless-code/persist" library_version: "0.4.0" - framework: "async-storage" + framework: "@react-native-async-storage/async-storage" requires: - persist sources: @@ -15,8 +15,6 @@ sources: # AsyncStorage backend -This skill builds on `persist`. - JSON via `createJSONStorage` under the hood. Fully **async** → **`useHydrated` mandatory**. Factory does **not** accept `clearCorruptOnFailure` — use `createStorage(() => asyncStorageStateStorage(), jsonCodec(), opts)` when you need it. ## Install @@ -25,6 +23,8 @@ JSON via `createJSONStorage` under the hood. Fully **async** → **`useHydrated` bun add @stainless-code/persist @react-native-async-storage/async-storage ``` +Peer: `@react-native-async-storage/async-storage` `>=1.0.0`. + ## Minimal wiring ```ts @@ -45,4 +45,4 @@ Optional custom instance for namespacing: `createAsyncStorage(myAsyncStorage)`. - `createAsyncStorage(storage?)` · `asyncStorageStateStorage(storage?)` -See also: `persist-mmkv` (sync RN); `persist`. +See also: `persist-mmkv` (sync RN). diff --git a/skills/persist-compressed/SKILL.md b/skills/persist-compressed/SKILL.md index fe58161..d213c89 100644 --- a/skills/persist-compressed/SKILL.md +++ b/skills/persist-compressed/SKILL.md @@ -14,8 +14,6 @@ sources: # Compressed backend wrapper -This skill builds on `persist`. - **Not a `StorageCodec`.** Uses `CompressionStream` / `DecompressionStream` (default `gzip`); output is **base64** on the string wire. Returns `undefined` if streams unavailable. Documented stack with encryption: **compress → encrypt**. ## Minimal wiring @@ -31,7 +29,7 @@ const storage = createStorage( ); ``` -Formats: `gzip` | `deflate` | `deflate-raw` (`deflate-raw` needs newer Node). +Formats: `gzip` | `deflate` | `deflate-raw` (`deflate-raw` needs Node `20.12+`). ## Common mistakes @@ -43,4 +41,4 @@ Formats: `gzip` | `deflate` | `deflate-raw` (`deflate-raw` needs newer Node). - `createCompressedStorage(getStorage, { format? }) → StateStorage | undefined` -See also: `persist-encrypted`; `persist`. +See also: `persist-encrypted`. diff --git a/skills/persist-crosstab/SKILL.md b/skills/persist-crosstab/SKILL.md index c6f60ed..b100dee 100644 --- a/skills/persist-crosstab/SKILL.md +++ b/skills/persist-crosstab/SKILL.md @@ -14,8 +14,6 @@ sources: # BroadcastChannel cross-tab transport -This skill builds on `persist`. - For backends with **no** `storage` events (IndexedDB). Must **`wrap(storage)`** and pass `crossTabEventTarget` with `crossTab: true`. Posts `storageArea: null` (key-only match — each tab owns its backend instance). Returns `undefined` if `BroadcastChannel` missing. Call `close()` on teardown. ## Minimal wiring @@ -46,4 +44,4 @@ const persist = persistStore(store, { - `createBroadcastCrossTab({ channelName }) → { crossTabEventTarget, wrap, close } | undefined` -See also: `persist-idb`; `persist`. +See also: `persist-idb`. diff --git a/skills/persist-encrypted/SKILL.md b/skills/persist-encrypted/SKILL.md index a300ebb..3155f82 100644 --- a/skills/persist-encrypted/SKILL.md +++ b/skills/persist-encrypted/SKILL.md @@ -14,8 +14,6 @@ sources: # Encrypted backend wrapper -This skill builds on `persist`. - **Not a `StorageCodec`.** Wraps an inner string `StateStorage` with WebCrypto AES-GCM (`base64(iv).base64(ct)`). Returns `undefined` if `crypto.subtle` missing. Stack with compression: **compress → encrypt**. Decrypt failure (wrong key / tamper) → backend `getItem` **rejects** → `onError` phase `"hydrate"` — **not** the codec `clearCorruptOnFailure` path. @@ -46,4 +44,4 @@ const storage = createStorage( - `createEncryptedStorage(getStorage, { key }) → StateStorage | undefined` -See also: `persist-compressed`; `persist-seroval`; `persist`. +See also: `persist-compressed`; `persist-seroval`. diff --git a/skills/persist-idb/SKILL.md b/skills/persist-idb/SKILL.md index 35abb34..2de6259 100644 --- a/skills/persist-idb/SKILL.md +++ b/skills/persist-idb/SKILL.md @@ -15,8 +15,6 @@ sources: # IndexedDB backend -This skill builds on `persist`. - `createIdbStorage` uses **structured clone** + `identityCodec` — `Set`/`Map`/`Date` work without seroval. Fully **async** → gate UI. No `storage` events → use `persist-crosstab` for multi-tab. ## Install @@ -35,13 +33,13 @@ import { createIdbStorage } from "@stainless-code/persist/backends/idb"; const storage = createIdbStorage(); ``` -Custom codec / encrypt: `createStorage(() => idbStateStorage(store), codec, opts)`. +Custom codec: `createStorage(() => idbStateStorage(store), codec, opts)`. Encrypt wraps the **backend**, not the codec arg — `createStorage(() => createEncryptedStorage(() => idbStateStorage(), { key })!, codec, opts)` (see `persist-encrypted`). ## Gotchas - **`clearCorruptOnFailure` is inert** on the default identity path (identity never throws). - IDB missing → reject on first use (`onError` `"hydrate"`), not at construct. -- `crossTab: true` alone does nothing — need `persist-crosstab`. +- `crossTab: true` alone is useless on IDB — no `storage` events; use `persist-crosstab` (`wrap` + `crossTabEventTarget`). ## Common mistakes @@ -53,4 +51,4 @@ Custom codec / encrypt: `createStorage(() => idbStateStorage(store), codec, opts - `createIdbStorage(options?)` · `idbStateStorage(store?)` -See also: `persist-crosstab`; `react-persist` (or other `*-persist`); `persist`. +See also: `persist-crosstab`; `*-persist` for UI gates. diff --git a/skills/persist-jotai/SKILL.md b/skills/persist-jotai/SKILL.md index e715177..08c6184 100644 --- a/skills/persist-jotai/SKILL.md +++ b/skills/persist-jotai/SKILL.md @@ -55,12 +55,10 @@ Default `merge` is `(persisted) => persisted`. Shallow-spreading a primitive cor ## Common mistakes - **Omitting `createStore`.** `persistAtom(store, atom, opts)` — store first. -- **Readonly / computed atoms.** Only writable atoms; no `set` → not persistable. +- **Readonly / computed atoms.** Typed as `WritableAtom` only — **no** runtime throw (unlike TanStack `persistAtom`). - **Assuming object shallow-merge.** Primitives need replace (the default). ## API surface - `persistAtom(store, atom, options) → PersistApi` -- Options / `PersistApi`: same as `persistSource` (see `persist-tanstack-store` for the shared list) - -See also: `persist-tanstack-store` (`persistAtom` there has the same replace-merge opinionation). +- Options / `PersistApi`: see skill `persist`. Replace-merge also used by TanStack `persistAtom` (`persist-tanstack-store`). diff --git a/skills/persist-mmkv/SKILL.md b/skills/persist-mmkv/SKILL.md index d2d01ae..c758355 100644 --- a/skills/persist-mmkv/SKILL.md +++ b/skills/persist-mmkv/SKILL.md @@ -6,7 +6,7 @@ metadata: type: composition library: "@stainless-code/persist" library_version: "0.4.0" - framework: "mmkv" + framework: "react-native-mmkv" requires: - persist sources: @@ -15,8 +15,6 @@ sources: # MMKV backend -This skill builds on `persist`. - **Sync** backend — no hydrate UI gate required for flash. JSON string-wire. `id` namespaces the MMKV file. `encryptionKey` is MMKV's own key (max **16 bytes**), not `persist-encrypted`. ## Install @@ -45,4 +43,4 @@ const storage = createMmkvStorage({ id: "app-prefs" })!; - `createMmkvStorage({ id, path?, encryptionKey? })` · `mmkvStateStorage(instance)` -See also: `persist-async-storage`; `persist-encrypted` for WebCrypto AES-GCM; `persist`. +See also: `persist-async-storage`; `persist-encrypted` for WebCrypto AES-GCM. diff --git a/skills/persist-node-fs/SKILL.md b/skills/persist-node-fs/SKILL.md index 6f00f02..6638a61 100644 --- a/skills/persist-node-fs/SKILL.md +++ b/skills/persist-node-fs/SKILL.md @@ -14,8 +14,6 @@ sources: # Node filesystem backend -This skill builds on `persist`. - Exports **`nodeFsStateStorage({ dir })` only** — compose with `createStorage` + a codec. One file per key; sanitized name + **djb2 hash** suffix (collision-safe). Refuses keys that sanitize to `.` / `..` / `""`. ENOENT → null / no-op remove. ## Minimal wiring @@ -43,4 +41,4 @@ No optional peer — uses `node:fs`. - `nodeFsStateStorage({ dir }) → StateStorage` -See also: `persist-seroval`; `persist`. +See also: `persist-seroval`. diff --git a/skills/persist-pinia/SKILL.md b/skills/persist-pinia/SKILL.md index bde0c98..5a11115 100644 --- a/skills/persist-pinia/SKILL.md +++ b/skills/persist-pinia/SKILL.md @@ -1,6 +1,6 @@ --- name: persist-pinia -description: Persist a Pinia store instance with @stainless-code/persist (persistStore). Use when wiring Pinia option/setup stores to localStorage/sessionStorage/IndexedDB; hydrate replaces $state (not $patch). +description: Persist a Pinia store instance with @stainless-code/persist (persistStore). Use when wiring Pinia option/setup stores to storage; hydrate applies via $state = (not $patch), with default shallow merge. license: MIT metadata: type: composition @@ -14,7 +14,7 @@ sources: # Persisting Pinia -`@stainless-code/persist/sources/pinia` maps a **store instance** onto `persistSource`. Hydrate assigns **`$state`** (full replace). Subscribe uses `$subscribe(…, { detached: true })` so the listener survives Vue `effectScope` stop. +`@stainless-code/persist/sources/pinia` maps a **store instance** onto `persistSource`. Hydrate applies via **`$state =`** (not `$patch`); default `merge` still shallow-spreads, so keys absent from the payload can remain. Subscribe uses `$subscribe(…, { detached: true })` so the listener survives Vue `effectScope` stop. ## When to use this skill @@ -50,7 +50,7 @@ const persist = persistStore(store, { ## Common mistakes - **Passing `defineStore(...)` / the factory** instead of `usePrefs()` instance. -- **Expecting `$patch` semantics on hydrate.** Persist replaces `$state`. +- **Expecting `$patch` semantics on hydrate.** Persist assigns `$state` after `merge`. - **Name clash with Redux `persistStore`.** Import from `/sources/pinia` (or alias). - **Call before `setActivePinia` / app.use(pinia).** Same as any Pinia store use. diff --git a/skills/persist-secure-store/SKILL.md b/skills/persist-secure-store/SKILL.md index ba9f6a4..97f09f9 100644 --- a/skills/persist-secure-store/SKILL.md +++ b/skills/persist-secure-store/SKILL.md @@ -15,8 +15,6 @@ sources: # SecureStore backend -This skill builds on `persist`. - For **tokens / small secrets**, not full app state (~**2KB/key**). Async → gate UI. Keys sanitized to `/^[\w.-]+$/` (`:` → `_`). ## Install @@ -50,4 +48,4 @@ persistStore(store, { - `createSecureStoreStorage()` · `secureStoreStateStorage()` -See also: `persist-mmkv`; `persist-encrypted`; `persist`. +See also: `persist-mmkv`; `persist-encrypted`. diff --git a/skills/persist-seroval/SKILL.md b/skills/persist-seroval/SKILL.md index 029e107..de6a018 100644 --- a/skills/persist-seroval/SKILL.md +++ b/skills/persist-seroval/SKILL.md @@ -15,8 +15,6 @@ sources: # Seroval codec -This skill builds on `persist`. Read it first for `createStorage` / `clearCorruptOnFailure`. - `@stainless-code/persist/codecs/seroval` round-trips values JSON cannot (`Set`/`Map`/`Date`) via `seroval` `toJSON`/`fromJSON` on string-wire backends (`localStorage`, etc.). ## Install @@ -50,4 +48,4 @@ Or `createStorage(getStorage, serovalCodec(), opts)`. - `serovalCodec() → StorageCodec` - `createSerovalStorage(getStorage, options?) → PersistStorage | undefined` -See also: `persist-idb` for structured-clone without seroval; `persist`. +See also: `persist-idb` for structured-clone without seroval. diff --git a/skills/persist-standard-schema/SKILL.md b/skills/persist-standard-schema/SKILL.md index 2862b99..948452a 100644 --- a/skills/persist-standard-schema/SKILL.md +++ b/skills/persist-standard-schema/SKILL.md @@ -49,10 +49,11 @@ JSON sugar: `createStandardSchemaStorage(() => localStorage, schema)`. - **Treating wrong-lane throws as corrupt** — they must not clear storage. - **Schema-checking the whole envelope** — only `state`. - **Skipping `useHydrated` on the async lane.** +- **Stacking wrap + `standardSchemaCodec`** — double-validates; pick one. ## API surface - `standardSchemaCodec(schema)` · `withStandardSchema` / `withStandardSchemaAsync` - `createStandardSchemaStorage` / `createStandardSchemaStorageAsync` -See also: `persist`; `persist-idb` for async backends. +See also: `persist-idb` for async backends. diff --git a/skills/persist-tanstack-store/SKILL.md b/skills/persist-tanstack-store/SKILL.md index 61f4d72..f608253 100644 --- a/skills/persist-tanstack-store/SKILL.md +++ b/skills/persist-tanstack-store/SKILL.md @@ -1,39 +1,31 @@ --- name: persist-tanstack-store -description: Persist a @tanstack/store Store or writable Atom with @stainless-code/persist (persistStore/persistAtom). Use when persisting TanStack Store state to localStorage/sessionStorage/IndexedDB, gating UI on hydration for an async backend, or deciding persistStore vs persistSource. +description: Persist a @tanstack/store Store or writable Atom with @stainless-code/persist (persistStore/persistAtom). Use when wiring TanStack Store to storage, or choosing persistAtom replace-merge vs persistStore. license: MIT metadata: type: composition library: "@stainless-code/persist" library_version: "0.4.0" framework: "@tanstack/store" +requires: + - persist sources: - - stainless-code/persist:README.md - - stainless-code/persist:docs/architecture.md - stainless-code/persist:src/adapters/sources/tanstack-store.ts --- # Persisting TanStack Store -`@stainless-code/persist/sources/tanstack-store` ships two adapters over the store-agnostic `persistSource` core: `persistStore(store, options)` for `@tanstack/store`'s `Store` (action-bearing stores included), and `persistAtom(atom, options)` for a writable `Atom`. The middleware owns the lifecycle so the store stays a plain store; the adapters are thin wrappers that supply the `PersistableSource` shape. +Thin adapters over `persistSource` — shared Options / gate / throttle / cross-tab live in skill `persist`. -## When to use this skill - -- You have a `@tanstack/store` `Store` (or a writable Atom) and want it to survive reload. -- You need a hydration signal to gate UI flash on async backends (IndexedDB). -- You're deciding between `persistStore` and dropping to `persistSource`. - -Zustand → `persist-zustand`. Other libraries with a first-party adapter → that `persist-*` skill. Hand-rolled shapes → `persistSource`. +`persistStore(store, options)` for `Store` (action-bearing included). `persistAtom(atom, options)` for a writable `Atom`. ## Install ```bash bun add @stainless-code/persist @tanstack/store -# only when you use a codec that needs it: -bun add seroval ``` -`@tanstack/store` is an optional peer of the `/sources/tanstack-store` subpath — importing the subpath is the dep opt-in. +`@tanstack/store` is an optional peer of `/sources/tanstack-store`. ## Minimal wiring @@ -49,122 +41,31 @@ const persist = persistStore(store, { }); ``` -The middleware hydrates on create, subscribes to `setState`, and writes through. `persist` is a `PersistApi` — keep the reference for `rehydrate()` / `destroy()` / `onHydrate` / `clearStorage()`. +Keep the `PersistApi` for `rehydrate()` / `destroy()` / `onHydrate` / `clearStorage()`. Non-singletons must `destroy()` on teardown. ## `persistAtom` vs `persistStore` -`persistAtom` has two opinionations `persistStore` doesn't: +`persistAtom` only: -- **Default `merge` REPLACES, not shallow-spreads.** Atoms commonly hold primitives (`"light"`, a number); a shallow spread of a primitive corrupts it (`{}` for a number). `persistAtom` overrides `merge` to `(persisted) => persisted`. Pass your own `merge` to restore spread-merge for object atoms. The override uses `??` (not spread order), so an explicit `merge: undefined` still gets replace-merge. -- **Throws on readonly atoms.** A computed/readonly atom has no `set`; `persistAtom` throws `[persistAtom] Cannot persist a readonly atom.` rather than silently no-op'ing. Only writable atoms are persistable. +- **Default `merge` REPLACES** (`(persisted) => persisted`) — shallow-spreading a primitive corrupts it. Override uses `??`, so `merge: undefined` still replace-merges. +- **Throws on readonly atoms** — `"[persistAtom] Cannot persist a readonly atom."` ```ts import { createAtom } from "@tanstack/store"; import { persistAtom } from "@stainless-code/persist/sources/tanstack-store"; const theme = createAtom<"light" | "dark">("light"); -const persist = persistAtom(theme, { name: "app:theme:v1" }); -// hydrate REPLACES the primitive; theme.set() writes through -``` - -## Hydration gate - -Writes are **gated until hydration settles** — a `setState` fired before the stored state is loaded will not clobber stored state with the constructor default. This is why you don't need to manually defer your first write. - -- **Sync backend (localStorage):** hydration settles before first paint for stores created at module load. Caveat: `hydrate` is async and `await`s the (sync) `getItem` return, so the flag flips in a **microtask**, not synchronously — `hasHydrated()` is `false` for a brief window right after `persistStore()` returns. Module-load creation settles before React's first render; creation inside a component mount may not. No flash, no `Suspense`; `useHydrated` is still the safe way to read it. -- **Async backend (IndexedDB):** hydration settles after first paint. Gate the UI on `useHydrated` (`@stainless-code/persist/frameworks/react`) or read `persist.hasHydrated()` before rendering persisted-dependent UI. - -```ts -import { toHydrationSignal } from "@stainless-code/persist"; -import { useHydrated } from "@stainless-code/persist/frameworks/react"; - -export const prefsHydration = toHydrationSignal(persist); -// in a component: -const { hydrated } = useHydrated(prefsHydration); -``` - -SSR: render `hydrated: true` on the server (no storage server-side). `null` signal = no persistence = hydrated. - -## Trailing-only throttle - -`throttleMs` coalesces bursts (typing, dragging) into **trailing** writes. The first eligible `setState` schedules a timer of `throttleMs`; further calls within the window coalesce; when the timer fires, ONE write happens with the state read at flush time (last write wins). **The first write does NOT fire immediately** — it waits out the window. This trades first-write latency for a single-timer model (TanStack Query's persister throttle is leading+trailing; ours is trailing-only). - -Not throttled: `skipPersist` removals (a reset-to-default drops the key immediately, cancelling any pending write) and the one-shot post-migrate write-back. `destroy()` flushes a pending write immediately so no coalesced state is silently dropped. Set `throttleMs` when `setState` fires at high frequency and the backend is slow (IndexedDB) or networked — and only when you can tolerate first-write latency. - -## Teardown — required for non-singletons - -`persistStore` subscribes to the store. For a singleton app store you can let it live for the process. For stores tied to a component/route lifetime, call `persist.destroy()` on unmount — otherwise the subscription and write timer leak and stale retries can fire after the owner is gone. - -```ts -useEffect(() => { - const persist = persistStore(store, opts); - return () => persist.destroy(); -}, [store]); -``` - -## Cross-tab sync - -`crossTab: true` enables `storage`-event sync for `localStorage`. Pair with `onCrossTabRemove` when using `skipPersist` — it fires when another tab clears the key, so you can reset the store: - -```ts -persistStore(store, { - name, - storage, - crossTab: true, - onCrossTabRemove: () => store.actions.reset(), -}); -``` - -Caveats that bite: `sessionStorage` is per-tab — `crossTab` is meaningless there. IndexedDB has no `storage` events — bridge a `BroadcastChannel` via `crossTabEventTarget` instead. - -## Schema evolution - -Bump `version` in options and provide `migrate`. Payloads carry `version`; on hydrate, the middleware walks migrations to the current version before seeding the store. - -```ts -persistStore(store, { - name: "app:prefs:v1", - storage, - version: 2, - migrate: (state, from) => ({ ...state, newField: "default", _v: from }), -}); +persistAtom(theme, { name: "app:theme:v1" }); ``` ## When to drop to `persistSource` -Use `persistSource({ getState, setState, subscribe }, opts)` directly when: - -- There is no first-party `./sources/*` adapter for the store (hand-rolled atom). -- You want to control subscription timing without the adapter's opinion. -- You're building a framework adapter (the React `useHydrated` hook is the template — a thin layer over `HydrationSignal`; see its JSDoc for the subscribe contract). - -See also: `persist-zustand` — same core options over zustand's `StoreApi`. - -## Common mistakes - -- **Gating writes manually before hydration.** Don't — the gate is built in. Manually deferring usually double-gates and drops legitimate writes. -- **`identityCodec` with a string-only backend.** `identityCodec` is for structured-clone backends (IndexedDB via `idbStateStorage`). With `localStorage` use `jsonCodec` (default) or `serovalCodec`. -- **Treating `maxAge` as on by default.** It's opt-in — prefs shouldn't silently expire. Add it only for cache-shaped state. -- **Duck-typing a `then` field as a pending read.** The read path switches on `instanceof Promise`, not thenable — so a stored value with a `then` property is safe. Don't "fix" this by awaiting thenables elsewhere. - -## Backend × codec choice - -| State shape | Backend | Codec | Notes | -| ------------------ | -------------- | --------------- | ------------------------------------------- | -| Plain JSON-able | `localStorage` | `jsonCodec` | default; no extra dep | -| `Set`/`Map`/`Date` | `localStorage` | `serovalCodec` | needs `seroval` peer | -| Large / structured | IndexedDB | `identityCodec` | structured-clone mode via `idbStateStorage` | -| Encrypted at rest | any | custom | `encode`/`decode` pair over the backend | - -`createStorage(backend, codec, options)` composes any other cell. - -## API surface for this skill +- No first-party adapter for the store shape. +- You need subscription timing without the adapter's opinions. -- `persistStore(store, options) → PersistApi` (accepts action-bearing `Store`) -- `persistAtom(atom, options) → PersistApi` (writable atoms only; throws on readonly; default `merge` replaces) -- Options: `name`, `storage`, `partialize`, `merge`, `onRehydrateStorage`, `version`, `migrate`, `skipHydration`, `skipPersist`, `crossTab`, `crossTabEventTarget`, `onCrossTabRemove`, `maxAge`, `buster`, `throttleMs`, `retryWrite`, `onError`, `registry` -- `PersistApi`: `rehydrate()`, `hasHydrated()`, `onHydrate(fn)`, `onFinishHydration(fn)`, `setOptions(partial)`, `clearStorage()`, `getOptions()`, `destroy()` +Zustand → `persist-zustand`. UI gate → `react-persist` (or other `*-persist`). -Notes: `registry` + `clearStorage()` wipe every persisted key in one `registry.clearAll()` (session-end / clear-all-on-demand). `partialize` projects `TState` to the persisted slice; `merge` combines persisted with current on hydrate (default shallow spread). +## API surface -Full contracts live in the JSDoc of each module (hovers + published `.d.mts`). +- `persistStore(store, options) → PersistApi` +- `persistAtom(atom, options) → PersistApi` (writable only; replace-merge default) diff --git a/skills/persist-zustand/SKILL.md b/skills/persist-zustand/SKILL.md index 613aa84..52eb9e6 100644 --- a/skills/persist-zustand/SKILL.md +++ b/skills/persist-zustand/SKILL.md @@ -23,7 +23,7 @@ sources: - You're migrating off `zustand/middleware`'s `persist`. - You need async-backend hydration gating (`useHydrated`) shared with other Persist sources. -Other sources → matching `persist-*` skill (`persist-jotai`, `persist-redux`, …). Hand-rolled shapes → `persistSource`. UI gate → `react-persist`. +Other sources → matching `persist-*` skill. UI gate → `*-persist`. ## Install @@ -95,6 +95,4 @@ useEffect(() => { ## API surface for this skill - `persistStore(store, options) → PersistApi` — `store: StoreApi` -- Options / `PersistApi`: same as `persistSource` (see JSDoc on `@stainless-code/persist` and `persist-tanstack-store` for the full list) - -See also: `persist-tanstack-store` — shared Options semantics (`throttleMs`, `crossTab`, `migrate`, `maxAge`) with TanStack Store examples. +- Options / `PersistApi`: see skill `persist`. diff --git a/skills/persist/SKILL.md b/skills/persist/SKILL.md index 76c5832..4718116 100644 --- a/skills/persist/SKILL.md +++ b/skills/persist/SKILL.md @@ -14,7 +14,7 @@ sources: # Persist -Zero-dep `persistSource(source, options)` owns hydrate → subscribe → write. First-party `./sources/*` adapters only supply a `PersistableSource` shape (`getState` / `setState` / `subscribe`). +Zero-dep `persistSource(source, options)` owns hydrate → subscribe → write. First-party `./sources/*` adapters map library APIs onto `PersistableSource` (`getState` / `setState` / `subscribe`) and may add opinions (default `merge`, reducer wraps, assign vs `$patch`). ## When to use this skill @@ -53,7 +53,8 @@ const persist = persistSource( - **`throttleMs`** — trailing-only; first write waits out the window. `destroy()` flushes pending writes. - **`maxAge`** — opt-in; prefs should not silently expire. - **`instanceof Promise`** on reads — not thenable duck-typing. -- **`PersistDecodeRethrowError`** — decode errors that must not hit `clearCorrupt` (wrong sync/async lane). +- **`PersistDecodeRethrowError`** — decode errors that must rethrow / skip `clearCorrupt` (e.g. wrong sync/async schema lane). +- **`migrate`** — called once with `(state, fromVersion)`; multi-step chains → `createMigrationChain`. - **Teardown** — `persist.destroy()` for non-singleton lifetimes. ## `PersistApi` (keep the reference) diff --git a/skills/preact-persist/SKILL.md b/skills/preact-persist/SKILL.md index ccfd713..ef2ecd0 100644 --- a/skills/preact-persist/SKILL.md +++ b/skills/preact-persist/SKILL.md @@ -15,8 +15,6 @@ sources: # Preact hydration gate -This skill builds on `persist`. Read it first for `toHydrationSignal`. - `@stainless-code/persist/frameworks/preact` mirrors the React adapter via `preact/compat` `useSyncExternalStore` (SSR snapshot always `true`). Not a state source — pair with a `./sources/*` adapter. ## Install @@ -49,4 +47,4 @@ return ; - `useHydrated(signal) → { hydrated: boolean }` -See also: `react-persist` (same contract); `persist`. +See also: `react-persist` (same contract). diff --git a/skills/react-persist/SKILL.md b/skills/react-persist/SKILL.md index 611d49e..76f6b3c 100644 --- a/skills/react-persist/SKILL.md +++ b/skills/react-persist/SKILL.md @@ -16,8 +16,6 @@ sources: # React hydration gate -This skill builds on `persist`. Read it first for `persistSource` / Options / `toHydrationSignal`. - `@stainless-code/persist/frameworks/react` exports `useHydrated(signal)` — a thin `useSyncExternalStore` wrapper over `HydrationSignal`. It does **not** persist state; pair it with a `./sources/*` adapter (`persist-zustand`, `persist-tanstack-store`, …). ## When to use this skill @@ -73,4 +71,4 @@ return ; - `useHydrated(signal: HydrationSignal | null | undefined) → { hydrated: boolean }` - Core helper: `toHydrationSignal(persist)` from `@stainless-code/persist` -See also: the `persist-*` source skill for your store library; sibling framework skills (`vue-persist`, `solid-persist`, `svelte-persist`, `svelte-store-persist`, `preact-persist`, `angular-persist`, `lit-persist`, `alpine-persist`). +See also: the matching `persist-*` source skill for your store. diff --git a/skills/solid-persist/SKILL.md b/skills/solid-persist/SKILL.md index c8b9035..fc0402d 100644 --- a/skills/solid-persist/SKILL.md +++ b/skills/solid-persist/SKILL.md @@ -15,8 +15,6 @@ sources: # Solid hydration gate -This skill builds on `persist`. Read it first for `toHydrationSignal`. - `@stainless-code/persist/frameworks/solid` returns an `Accessor` — call `hydrated()` inside tracking scopes (`createEffect`, JSX, …). ## Install @@ -36,6 +34,10 @@ const hydrated = useHydrated(prefsHydration); // }>… ``` +## Contracts + +Null/undefined signal → hydrated `true`. SSR policy: treat as hydrated `true`. + ## Common mistakes - **Using `hydrated` as a boolean** — it's an accessor; call it. @@ -45,5 +47,3 @@ const hydrated = useHydrated(prefsHydration); ## API surface - `useHydrated(signal) → Accessor` - -See also: `persist`. diff --git a/skills/svelte-persist/SKILL.md b/skills/svelte-persist/SKILL.md index 9c6e9ee..a0b9b4a 100644 --- a/skills/svelte-persist/SKILL.md +++ b/skills/svelte-persist/SKILL.md @@ -15,8 +15,6 @@ sources: # Svelte 5 runes hydration gate -This skill builds on `persist`. Read it first for `toHydrationSignal`. - `@stainless-code/persist/frameworks/svelte` exports `hydratedRune(signal)` → `{ readonly current: boolean }`. Read `.current` inside `$derived` / `$effect` / `{#if}` (createSubscriber is lazy inside a reactive owner). **Stores / Svelte 3–4 / Svelte 5 store users → `svelte-store-persist`** (`./frameworks/svelte-store`). @@ -44,6 +42,10 @@ Needs Svelte **5.7+** for `svelte/reactivity` `createSubscriber` (package peer a {/if} ``` +## Contracts + +Null/undefined → `current: true`. SSR: hydrated `true`. + ## Common mistakes - **Using this entry for store-based UI.** Import `svelte-store-persist`. @@ -54,4 +56,4 @@ Needs Svelte **5.7+** for `svelte/reactivity` `createSubscriber` (package peer a - `hydratedRune(signal) → { readonly current: boolean }` -See also: `svelte-store-persist`; `persist`. +See also: `svelte-store-persist`. diff --git a/skills/svelte-store-persist/SKILL.md b/skills/svelte-store-persist/SKILL.md index 3cb33a3..cb35862 100644 --- a/skills/svelte-store-persist/SKILL.md +++ b/skills/svelte-store-persist/SKILL.md @@ -15,8 +15,6 @@ sources: # Svelte stores hydration gate -This skill builds on `persist`. Read it first for `toHydrationSignal`. - `@stainless-code/persist/frameworks/svelte-store` exports `hydratedStore(signal) → Readable`. Use `$hydrated` auto-subscribe in templates. **Separate public entry** from `./frameworks/svelte` (runes). ## Install @@ -42,6 +40,10 @@ Peer: `svelte` `>=3.0.0`. {/if} ``` +## Contracts + +Null/undefined → `readable(true)`. SSR: hydrated `true`. + ## Common mistakes - **Using this for Svelte 5 runes UI.** Use `svelte-persist` / `hydratedRune`. @@ -52,4 +54,4 @@ Peer: `svelte` `>=3.0.0`. - `hydratedStore(signal) → Readable` -See also: `svelte-persist`; `persist`. +See also: `svelte-persist`. diff --git a/skills/vue-persist/SKILL.md b/skills/vue-persist/SKILL.md index 4e34fab..f74d66b 100644 --- a/skills/vue-persist/SKILL.md +++ b/skills/vue-persist/SKILL.md @@ -15,8 +15,6 @@ sources: # Vue hydration gate -This skill builds on `persist`. Read it first for `toHydrationSignal`. - `@stainless-code/persist/frameworks/vue` returns a `Ref` (`shallowRef`). Call inside `setup()` / an active `effectScope()` so `onScopeDispose` unsubscribes. ## Install @@ -47,10 +45,10 @@ const hydrated = useHydrated(prefsHydration); - **Calling outside setup / scope** — leak or no teardown. - **Treating the ref as store state.** -- **SSR expects `false`.** Server renders hydrated `true`. +- **SSR / null signal.** Null stays `true`; there is no separate server snapshot (unlike React `getServerSnapshot`). ## API surface - `useHydrated(signal) → Ref` -See also: `persist-pinia` for Pinia store wiring; `persist`. +See also: `persist-pinia` for Pinia store wiring. From 81f75bcad9951dfc650eee03fd5ca82e598df868 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Thu, 23 Jul 2026 09:05:22 +0300 Subject: [PATCH 08/10] docs(skills): drop leftover persist pointer in standard-schema --- skills/persist-standard-schema/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/persist-standard-schema/SKILL.md b/skills/persist-standard-schema/SKILL.md index 948452a..c8d7d77 100644 --- a/skills/persist-standard-schema/SKILL.md +++ b/skills/persist-standard-schema/SKILL.md @@ -15,7 +15,7 @@ sources: # Standard Schema codec / wraps -This skill builds on `persist`. Types for `~standard` are **vendored** — no runtime peer. Bring Zod / Yup / Valibot yourself. +Types for `~standard` are **vendored** — no runtime peer. Bring Zod / Yup / Valibot yourself. Validates persisted **`state` only** (not envelope `version` / `timestamp` / `buster`). Encode writes schema **output** (defaults/transforms applied). From 19d9424048040ee4e8302a8d6716c5be506f8a6b Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Thu, 23 Jul 2026 09:08:24 +0300 Subject: [PATCH 09/10] ci(check-skills): run Intent from frozen lockfile via Bun Avoid npm install -g so @tanstack/intent and its transitive tree resolve from bun.lock like the rest of CI. --- .github/workflows/check-skills.yml | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/.github/workflows/check-skills.yml b/.github/workflows/check-skills.yml index da536ed..ccece94 100644 --- a/.github/workflows/check-skills.yml +++ b/.github/workflows/check-skills.yml @@ -23,16 +23,11 @@ jobs: - name: Checkout uses: actions/checkout@v7 - - name: Setup Node - uses: actions/setup-node@v7 - with: - node-version: 20 - - - name: Install intent - run: npm install -g @tanstack/intent@0.3.5 + - name: Setup Bun and install + uses: ./.github/actions/setup - name: Validate skills - run: intent validate --github-summary + run: bun run intent:validate -- --github-summary review: name: Check intent skill coverage @@ -47,18 +42,13 @@ jobs: with: fetch-depth: 0 - - name: Setup Node - uses: actions/setup-node@v7 - with: - node-version: 20 - - - name: Install intent - run: npm install -g @tanstack/intent@0.3.5 + - name: Setup Bun and install + uses: ./.github/actions/setup - name: Check skills id: stale run: | - intent stale --github-review --package-label "@stainless-code/persist" + bun run intent:stale -- --github-review --package-label "@stainless-code/persist" - name: Open or update review PR if: steps.stale.outputs.has_review == 'true' From a137334786bb2770c6af7ee0e7f7510877526405 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Thu, 23 Jul 2026 09:13:38 +0300 Subject: [PATCH 10/10] harden: align Intent skill install/examples with peers Copy-paste wiring now matches Install (JSON default storage; crosstab/ secure-store declare example peers; CryptoKey shown for encrypted). --- skills/persist-compressed/SKILL.md | 7 ++++--- skills/persist-crosstab/SKILL.md | 11 +++++++++++ skills/persist-encrypted/SKILL.md | 14 +++++++++----- skills/persist-node-fs/SKILL.md | 9 +++------ skills/persist-secure-store/SKILL.md | 12 ++++++++---- skills/persist-tanstack-store/SKILL.md | 8 ++++++-- 6 files changed, 41 insertions(+), 20 deletions(-) diff --git a/skills/persist-compressed/SKILL.md b/skills/persist-compressed/SKILL.md index d213c89..0e04c8b 100644 --- a/skills/persist-compressed/SKILL.md +++ b/skills/persist-compressed/SKILL.md @@ -19,16 +19,17 @@ sources: ## Minimal wiring ```ts -import { createStorage } from "@stainless-code/persist"; +import { createStorage, jsonCodec } from "@stainless-code/persist"; import { createCompressedStorage } from "@stainless-code/persist/backends/compressed"; -import { serovalCodec } from "@stainless-code/persist/codecs/seroval"; const storage = createStorage( () => createCompressedStorage(() => localStorage)!, - serovalCodec(), + jsonCodec(), ); ``` +Richer graphs → `persist-seroval` as the codec. + Formats: `gzip` | `deflate` | `deflate-raw` (`deflate-raw` needs Node `20.12+`). ## Common mistakes diff --git a/skills/persist-crosstab/SKILL.md b/skills/persist-crosstab/SKILL.md index b100dee..1b24b93 100644 --- a/skills/persist-crosstab/SKILL.md +++ b/skills/persist-crosstab/SKILL.md @@ -16,12 +16,23 @@ sources: For backends with **no** `storage` events (IndexedDB). Must **`wrap(storage)`** and pass `crossTabEventTarget` with `crossTab: true`. Posts `storageArea: null` (key-only match — each tab owns its backend instance). Returns `undefined` if `BroadcastChannel` missing. Call `close()` on teardown. +## Install + +```bash +bun add @stainless-code/persist idb-keyval @tanstack/store +``` + +Example peers: `idb-keyval` (IDB backend) + `@tanstack/store` (source). Swap either for your stack. + ## Minimal wiring ```ts +import { Store } from "@tanstack/store"; import { createIdbStorage } from "@stainless-code/persist/backends/idb"; +import { persistStore } from "@stainless-code/persist/sources/tanstack-store"; import { createBroadcastCrossTab } from "@stainless-code/persist/transport/crosstab"; +const store = new Store({ theme: "light" }); const bridge = createBroadcastCrossTab({ channelName: "app:prefs" })!; const persist = persistStore(store, { name: "app:prefs:v1", diff --git a/skills/persist-encrypted/SKILL.md b/skills/persist-encrypted/SKILL.md index 3155f82..1a541a8 100644 --- a/skills/persist-encrypted/SKILL.md +++ b/skills/persist-encrypted/SKILL.md @@ -21,18 +21,22 @@ Decrypt failure (wrong key / tamper) → backend `getItem` **rejects** → `onEr ## Minimal wiring ```ts -import { createStorage } from "@stainless-code/persist"; +import { createStorage, jsonCodec } from "@stainless-code/persist"; import { createEncryptedStorage } from "@stainless-code/persist/backends/encrypted"; -import { serovalCodec } from "@stainless-code/persist/codecs/seroval"; +const key = await crypto.subtle.generateKey( + { name: "AES-GCM", length: 256 }, + true, + ["encrypt", "decrypt"], +); const storage = createStorage( () => createEncryptedStorage(() => localStorage, { key })!, - serovalCodec(), + jsonCodec(), { clearCorruptOnFailure: true }, ); ``` -`key` is a `CryptoKey` for AES-GCM. +Richer graphs → compose with `persist-seroval` instead of `jsonCodec`. ## Common mistakes @@ -44,4 +48,4 @@ const storage = createStorage( - `createEncryptedStorage(getStorage, { key }) → StateStorage | undefined` -See also: `persist-compressed`; `persist-seroval`. +See also: `persist-compressed`. diff --git a/skills/persist-node-fs/SKILL.md b/skills/persist-node-fs/SKILL.md index 6638a61..0e6dd37 100644 --- a/skills/persist-node-fs/SKILL.md +++ b/skills/persist-node-fs/SKILL.md @@ -19,17 +19,16 @@ Exports **`nodeFsStateStorage({ dir })` only** — compose with `createStorage` ## Minimal wiring ```ts -import { createStorage } from "@stainless-code/persist"; +import { createStorage, jsonCodec } from "@stainless-code/persist"; import { nodeFsStateStorage } from "@stainless-code/persist/backends/node-fs"; -import { serovalCodec } from "@stainless-code/persist/codecs/seroval"; const storage = createStorage( () => nodeFsStateStorage({ dir: "./.persist" }), - serovalCodec(), + jsonCodec(), ); ``` -No optional peer — uses `node:fs`. +No optional peer for this backend — uses `node:fs`. Richer graphs → `persist-seroval` as the codec. ## Common mistakes @@ -40,5 +39,3 @@ No optional peer — uses `node:fs`. ## API surface - `nodeFsStateStorage({ dir }) → StateStorage` - -See also: `persist-seroval`. diff --git a/skills/persist-secure-store/SKILL.md b/skills/persist-secure-store/SKILL.md index 97f09f9..2be6da6 100644 --- a/skills/persist-secure-store/SKILL.md +++ b/skills/persist-secure-store/SKILL.md @@ -20,18 +20,22 @@ For **tokens / small secrets**, not full app state (~**2KB/key**). Async → gat ## Install ```bash -bun add @stainless-code/persist expo-secure-store +bun add @stainless-code/persist expo-secure-store zustand ``` -Peer: `expo-secure-store` `>=12.0.0`. +Peers: `expo-secure-store` `>=12.0.0`; example uses `zustand` (any `./sources/*` adapter works). ## Minimal wiring ```ts +import { create } from "zustand"; import { createSecureStoreStorage } from "@stainless-code/persist/backends/secure-store"; +import { persistStore } from "@stainless-code/persist/sources/zustand"; -const storage = createSecureStoreStorage()!; -persistStore(store, { +type Auth = { token: string }; +const useAuth = create(() => ({ token: "" })); +const storage = createSecureStoreStorage()!; +persistStore(useAuth, { name: "auth:token:v1", storage, partialize: (s) => s.token, diff --git a/skills/persist-tanstack-store/SKILL.md b/skills/persist-tanstack-store/SKILL.md index f608253..14ee23a 100644 --- a/skills/persist-tanstack-store/SKILL.md +++ b/skills/persist-tanstack-store/SKILL.md @@ -23,6 +23,8 @@ Thin adapters over `persistSource` — shared Options / gate / throttle / cross- ```bash bun add @stainless-code/persist @tanstack/store +# Set/Map/Date on string-wire backends: +bun add seroval ``` `@tanstack/store` is an optional peer of `/sources/tanstack-store`. @@ -31,16 +33,18 @@ bun add @stainless-code/persist @tanstack/store ```ts import { Store } from "@tanstack/store"; -import { createSerovalStorage } from "@stainless-code/persist/codecs/seroval"; +import { createJSONStorage } from "@stainless-code/persist"; import { persistStore } from "@stainless-code/persist/sources/tanstack-store"; const store = new Store({ theme: "light" }); const persist = persistStore(store, { name: "app:prefs:v1", - storage: createSerovalStorage(() => localStorage), + storage: createJSONStorage(() => localStorage), }); ``` +`Set`/`Map`/`Date` → `createSerovalStorage` from `/codecs/seroval` (see `persist-seroval`). + Keep the `PersistApi` for `rehydrate()` / `destroy()` / `onHydrate` / `clearStorage()`. Non-singletons must `destroy()` on teardown. ## `persistAtom` vs `persistStore`