Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/docs-voice/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ was rejected) and the "when **not** to use it" anti-sell — keep both.
- **Adapter / listing order:** core → backends → codecs → sources → frameworks
→ transport. Within sources: tanstack-store → zustand → jotai → valtio →
mobx → pinia → redux → custom. Within frameworks: react → preact → solid →
angular → vue → svelte (runes → store).
angular → vue → lit → alpine → svelte (runes → store).

## Don't

Expand Down
5 changes: 5 additions & 0 deletions .changeset/alpine-framework-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stainless-code/persist": minor
---

Add Alpine framework adapter (`./frameworks/alpine`) — `useHydrated` + `$hydrated` plugin over `HydrationSignal`.
5 changes: 5 additions & 0 deletions .changeset/lit-framework-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stainless-code/persist": minor
---

Add Lit `HydrationController` framework adapter (`./frameworks/lit`).
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Any store, any storage, one middleware — no flash.**

Hydration-aware persistence for any reactive store — no hydrate flash, no SSR mismatch. Store-agnostic via a structural `PersistableSource` (TanStack Store, zustand, jotai, valtio, mobx, pinia, redux, or a hand-rolled atom); three composable seams (backend × codec × source) so you swap storage, serialization, or framework without rewriting. A first-class hydration signal gates UI on async backends; opt-in cross-tab sync, versioned migrations, encrypted/compressed backends, and retry-on-quota. Framework adapters for React, Solid, Vue, Svelte, Angular, and Preact.
Hydration-aware persistence for any reactive store — no hydrate flash, no SSR mismatch. Store-agnostic via a structural `PersistableSource` (TanStack Store, zustand, jotai, valtio, mobx, pinia, redux, or a hand-rolled atom); three composable seams (backend × codec × source) so you swap storage, serialization, or framework without rewriting. A first-class hydration signal gates UI on async backends; opt-in cross-tab sync, versioned migrations, encrypted/compressed backends, and retry-on-quota. Framework adapters for React, Preact, Solid, Angular, Vue, Lit, Alpine, and Svelte.

[![core size](https://img.shields.io/size-limit/label/gzip/.size-limit.json/core/stainless-code/persist)](https://github.com/stainless-code/persist/blob/main/.size-limit.json)

Expand Down
6 changes: 4 additions & 2 deletions apps/docs/content/adapters/framework-adapter.mdx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
---
title: Writing a framework adapter
description: Bridge HydrationSignal into your UI framework — the same shape as the shipped React, Solid, Vue, and Svelte adapters.
description: Bridge HydrationSignal into your UI framework — same shape as the shipped React, Preact, Solid, Angular, Vue, Lit, Alpine, and Svelte adapters.
search:
tags: ["adapter", "framework"]
---

The React hook (`@stainless-code/persist/frameworks/react`) is ~20 lines over `HydrationSignal` — every adapter is the same shape. Solid (`@stainless-code/persist/frameworks/solid`, `Accessor<boolean>` via `from`), Vue (`@stainless-code/persist/frameworks/vue`, `Ref<boolean>` via `shallowRef` + `onScopeDispose`), and Svelte (`@stainless-code/persist/frameworks/svelte` runes `hydratedRune`; `@stainless-code/persist/frameworks/svelte-store` `hydratedStore` for Svelte 4 + Svelte 5 store users) ship the same way. The contract (full version on `HydrationSignal`'s JSDoc): subscribe returns an idempotent unsubscribe; each subscribe call is an independent subscription; **no initial notification and no payload** — pull `isHydrated()` after attach and on every notification; transitions while detached aren't replayed (the snapshot re-read recovers); **render `hydrated: true` on the server** (no storage server-side); `null` signal = no persistence = hydrated.
The React hook is ~20 lines over `HydrationSignal` — every adapter is that shape. Preact, Solid, Angular, Vue, Lit, Alpine, and Svelte ship the same contract (full text on `HydrationSignal`'s JSDoc).

Subscribe returns an idempotent unsubscribe; each call is an independent subscription. **No initial notification and no payload** — pull `isHydrated()` after attach and on every notification. Transitions while detached aren't replayed (re-read recovers). **Render `hydrated: true` on the server**; `null` signal = no persistence = hydrated.

```ts
import type { HydrationSignal } from "@stainless-code/persist";
Expand Down
6 changes: 4 additions & 2 deletions apps/docs/content/concepts/entry-points.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ One subpath = one optional peer. No barrel — importing a subpath is the depend
| `@stainless-code/persist/sources/pinia` | `persistStore` | `pinia` |
| `@stainless-code/persist/sources/redux` | `persistStore`, `persistableReducer` | `redux` |
| `@stainless-code/persist/frameworks/react` | `useHydrated` React hook | `react` |
| `@stainless-code/persist/frameworks/preact` | `useHydrated` (Preact `{ hydrated: boolean }`) | `preact` (>=10.19) |
| `@stainless-code/persist/frameworks/solid` | `useHydrated` (Solid `Accessor<boolean>`) | `solid-js` |
| `@stainless-code/persist/frameworks/angular` | `useHydrated` (Angular `Signal<boolean>`) | `@angular/core` (>=17) |
| `@stainless-code/persist/frameworks/vue` | `useHydrated` (Vue `Ref<boolean>`) | `vue` |
| `@stainless-code/persist/frameworks/lit` | `HydrationController` (Lit `ReactiveController`) | `lit` (>=3) |
| `@stainless-code/persist/frameworks/alpine` | `useHydrated` + `$hydrated` plugin (Alpine reactive bag) | `alpinejs` (>=3) |
| `@stainless-code/persist/frameworks/svelte` | `hydratedRune` (Svelte 5 runes `current`) | `svelte` (>=5.7) |
| `@stainless-code/persist/frameworks/svelte-store` | `hydratedStore` (Svelte `Readable<boolean>`) | `svelte` (>=3) |
| `@stainless-code/persist/frameworks/angular` | `useHydrated` (Angular `Signal<boolean>`) | `@angular/core` (>=17) |
| `@stainless-code/persist/frameworks/preact` | `useHydrated` (Preact `{ hydrated: boolean }`) | `preact` (>=10.19) |

Generated signatures: [API reference](/reference/api).
39 changes: 27 additions & 12 deletions apps/docs/content/guides/hydration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,45 @@ const { hydrated } = useHydrated(prefsHydration);
if (!hydrated) return <Skeleton />;
```

```ts
// Preact — { hydrated: boolean }
import { useHydrated } from "@stainless-code/persist/frameworks/preact";
const { hydrated } = useHydrated(prefsHydration);
```

```ts
// Solid — Accessor<boolean>
import { useHydrated } from "@stainless-code/persist/frameworks/solid";
const hydrated = useHydrated(prefsHydration);
```

```ts
// Angular — Signal<boolean>
import { useHydrated } from "@stainless-code/persist/frameworks/angular";
const hydrated = useHydrated(prefsHydration);
```

```ts
// Vue — Ref<boolean>
import { useHydrated } from "@stainless-code/persist/frameworks/vue";
const hydrated = useHydrated(prefsHydration);
```

```ts
// Lit — ReactiveController
import { HydrationController } from "@stainless-code/persist/frameworks/lit";
// in LitElement: this.#hydration = new HydrationController(this, prefsHydration);
// gate: this.#hydration.hydrated
```

```ts
// Alpine — reactive bag + plugin
import persist, { useHydrated } from "@stainless-code/persist/frameworks/alpine";
Alpine.plugin(persist);
const { hydrated } = useHydrated(prefsHydration);
// template: x-show="hydrated" / $hydrated(prefsHydration).hydrated
```

```ts
// Svelte 5 runes
import { hydratedRune } from "@stainless-code/persist/frameworks/svelte";
Expand All @@ -41,16 +68,4 @@ import { hydratedStore } from "@stainless-code/persist/frameworks/svelte-store";
const hydrated = hydratedStore(prefsHydration);
```

```ts
// Angular — Signal<boolean>
import { useHydrated } from "@stainless-code/persist/frameworks/angular";
const hydrated = useHydrated(prefsHydration);
```

```ts
// Preact — { hydrated: boolean }
import { useHydrated } from "@stainless-code/persist/frameworks/preact";
const { hydrated } = useHydrated(prefsHydration);
```

See [IndexedDB + React](/guides/idb-react) for the full async path, and [Writing a framework adapter](/adapters/framework-adapter) to author a new one.
18 changes: 12 additions & 6 deletions apps/docs/content/reference/api/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,24 @@ One TypeDoc module per package entry. [Reference](/reference)
<Card title="React" href="/reference/api/adapters-frameworks-react" arrow>
`@stainless-code/persist/frameworks/react`
</Card>
<Card title="Preact" href="/reference/api/adapters-frameworks-preact" arrow>
`@stainless-code/persist/frameworks/preact`
</Card>
<Card title="Solid" href="/reference/api/adapters-frameworks-solid" arrow>
`@stainless-code/persist/frameworks/solid`
</Card>
<Card title="Angular" href="/reference/api/adapters-frameworks-angular" arrow>
`@stainless-code/persist/frameworks/angular`
</Card>
<Card title="Vue" href="/reference/api/adapters-frameworks-vue" arrow>
`@stainless-code/persist/frameworks/vue`
</Card>
<Card title="Lit" href="/reference/api/adapters-frameworks-lit" arrow>
`@stainless-code/persist/frameworks/lit`
</Card>
<Card title="Alpine" href="/reference/api/adapters-frameworks-alpine" arrow>
`@stainless-code/persist/frameworks/alpine`
</Card>
<Card title="Svelte" href="/reference/api/adapters-frameworks-svelte" arrow>
`@stainless-code/persist/frameworks/svelte`
</Card>
Expand All @@ -137,10 +149,4 @@ One TypeDoc module per package entry. [Reference](/reference)
>
`@stainless-code/persist/frameworks/svelte-store`
</Card>
<Card title="Angular" href="/reference/api/adapters-frameworks-angular" arrow>
`@stainless-code/persist/frameworks/angular`
</Card>
<Card title="Preact" href="/reference/api/adapters-frameworks-preact" arrow>
`@stainless-code/persist/frameworks/preact`
</Card>
</CardGroup>
6 changes: 4 additions & 2 deletions apps/docs/content/reference/api/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ export default defineMeta({
"adapters-sources-pinia",
"adapters-sources-redux",
"adapters-frameworks-react",
"adapters-frameworks-preact",
"adapters-frameworks-solid",
"adapters-frameworks-angular",
"adapters-frameworks-vue",
"adapters-frameworks-lit",
"adapters-frameworks-alpine",
"adapters-frameworks-svelte",
"adapters-frameworks-svelte-store",
"adapters-frameworks-angular",
"adapters-frameworks-preact",
],
});
2 changes: 1 addition & 1 deletion apps/docs/pages/_home/Batteries.astro
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ import { contentHref } from "blume/components/content/base-href.ts";
title="No framework in the core"
>
Zero-dep engine — adapters mount <code>HydrationSignal</code> into
React, Solid, Vue, Svelte, Angular, Preact.
React, Preact, Solid, Angular, Vue, Lit, Alpine, Svelte.
</Card>
</CardGroup>
</div>
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/pages/_home/Seams.astro
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const frameworks = [
"Solid",
"Angular",
"Vue",
"Lit",
"Alpine",
"Svelte (runes)",
"Svelte (store)",
];
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/pages/_home/UseCases.astro
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const reachFor = [
fit: "Nice-to-have",
},
{
useCase: "Gate UI the same way in React / Solid / Vue / Svelte / Angular / Preact",
useCase: "Gate UI the same way in React / Preact / Solid / Angular / Vue / Lit / Alpine / Svelte",
involves: "`./frameworks/*` hydration adapters",
fit: "Ideal",
},
Expand Down
Loading
Loading