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
8 changes: 7 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,11 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["classy-store-demo"]
"ignore": [
"classy-store-react-example",
"classy-store-vue-example",
"classy-store-svelte-example",
"classy-store-solid-example",
"classy-store-angular-example"
]
}
5 changes: 5 additions & 0 deletions .changeset/two-bugs-stop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@codebelt/classy-store": minor
---

add framework-specific store utilities (Vue, Svelte, Angular, Solid)
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
.cache
*.tsbuildinfo

# Angular CLI build cache
.angular

# IntelliJ based IDEs
.idea

Expand Down
89 changes: 89 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# CLAUDE.md — @codebelt/classy-store

Context for Claude Code sessions in this monorepo.

## What This Library Is

`@codebelt/classy-store` is a class-based reactive state management library (~2.3 KB gzipped) for React, Vue, Svelte, Solid, and Angular. You define state as a plain TypeScript class, wrap it with `createClassyStore()`, and get a reactive proxy back. Class getters become automatically memoized computed values. ES6 Proxy intercepts mutations, batches them via `queueMicrotask`, and notifies framework-specific bindings via immutable snapshots with structural sharing.

## Monorepo Layout

```
packages/classy-store/src/
├── core/core.ts # Layer 1: Write Proxy — createClassyStore(), subscribe(), getVersion()
├── snapshot/snapshot.ts # Layer 2: Immutable snapshots — snapshot(), structural sharing
├── types.ts # Snapshot<T>, StoreInternal, DepEntry, ComputedEntry
├── index.ts # Main barrel: createClassyStore, snapshot, subscribe, getVersion, shallowEqual, reactiveMap, reactiveSet
├── collections/collections.ts # ReactiveMap and ReactiveSet (array-backed Map/Set emulation)
├── frameworks/
│ ├── react/react.ts # Layer 3 (React): useStore(), useLocalStore() via useSyncExternalStore
│ ├── vue/vue.ts # Vue: useStore() → ShallowRef<Snapshot<T>> (onUnmounted cleanup)
│ ├── svelte/svelte.ts # Svelte: toSvelteStore() → ClassyReadable<Snapshot<T>>
│ ├── solid/solid.ts # Solid: useStore() → () => Snapshot<T> signal (onCleanup)
│ └── angular/angular.ts # Angular: injectStore() → Signal<Snapshot<T>> (DestroyRef)
└── utils/
├── index.ts # Utils barrel: persist, devtools, subscribeKey, withHistory
├── persist/persist.ts # persist() — storage, transforms, versioning, cross-tab sync, TTL
├── devtools/devtools.ts # devtools() — Redux DevTools integration, time-travel
├── history/history.ts # withHistory() — undo/redo via snapshot stack, pause/resume
├── subscribe-key/subscribe-key.ts # subscribeKey() — single-property subscription
├── equality/equality.ts # shallowEqual
└── internal/internal.ts # isPlainObject, canProxy, findGetterDescriptor, PROXYABLE

website/ # Docusaurus documentation site
website/docs/ # .md source for all doc pages
website/static/ # Served verbatim at site root (llms.txt, llms-full.txt, etc.)

examples/ # Framework demo examples
```

## Key Technical Facts

- **Batching:** mutations are coalesced via `queueMicrotask`. Multiple synchronous writes (including array `push` which triggers multiple SET traps) produce a single subscriber notification.
- **Internal state:** stored in a `WeakMap<proxy, StoreInternal>` — never on the user's object. Allows GC when a store is dereferenced.
- **Non-proxyable types:** `Date`, `RegExp`, native `Map`, and native `Set` are treated as opaque values (internal slots can't be intercepted by Proxy). Use `reactiveMap()` and `reactiveSet()` for Map/Set semantics. Replace Date instances entirely to trigger updates.
- **`persist()` exclusions:** getters (detected by walking the prototype chain with `Object.getOwnPropertyDescriptor`) and methods (`typeof value === 'function'`) are automatically excluded from persistence. Only own data properties are saved.
- **Computed memoization:** two layers — the write proxy caches getter results keyed on dependency versions/values; the snapshot layer adds cross-snapshot caching using structural sharing reference equality.
- **Structural sharing:** unchanged sub-trees reuse the previous frozen snapshot reference. This makes `Object.is` comparisons in selectors efficient without `shallowEqual`.
- **Version numbers:** monotonically increasing integers stored per proxy node. Child mutations propagate version bumps up to the root. The snapshot cache is keyed on version — a cache hit is O(1).

## Package Export Entry Points

| Import path | Contents |
|---|---|
| `@codebelt/classy-store` | `createClassyStore`, `snapshot`, `subscribe`, `getVersion`, `shallowEqual`, `reactiveMap`, `reactiveSet`, `Snapshot` type |
| `@codebelt/classy-store/react` | `useStore`, `useLocalStore` |
| `@codebelt/classy-store/vue` | `useStore` (ShallowRef) |
| `@codebelt/classy-store/svelte` | `toSvelteStore` (ClassyReadable) |
| `@codebelt/classy-store/solid` | `useStore` (signal getter) |
| `@codebelt/classy-store/angular` | `injectStore` (Signal) |
| `@codebelt/classy-store/utils` | `persist`, `devtools`, `subscribeKey`, `withHistory` |

## Build & Test Commands

Run from the repo root:

```bash
bun install # Install all workspace dependencies

bun run build # Build all packages (tsdown, outputs to packages/classy-store/dist/)
bun run test # Run all tests (Bun test runner, uses happy-dom for React hook tests)

bun run docs:dev # Start Docusaurus dev server at http://localhost:3000/classy-store/
bun run docs:build # Build docs site to website/build/
```

Run from `packages/classy-store/`:

```bash
bun run dev # Build in watch mode
bun test # Run tests for this package only
bun run typecheck # TypeScript type check without emit
```

## LLM Documentation Files

- `website/static/llms.txt` — navigation index (served at `/classy-store/llms.txt`)
- `website/static/llms-full.txt` — all docs concatenated (served at `/classy-store/llms-full.txt`)

These follow the [llms.txt standard](https://llmstxt.org/).
48 changes: 2 additions & 46 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,5 @@
# Contributing to @codebelt/classy-store

## Development Setup
We've got it covered — me and AI.

1. Install [Bun](https://bun.sh).
2. Clone the repository.
3. Run `bun install`.

## Workflow

- **Dev**: `bun run dev` (watch mode)
- **Test**: `bun run test`
- **Lint**: `bun run lint`
- **Docs**: `bun run docs:dev`

## Releasing

We use [Changesets](https://github.com/changesets/changesets) to manage versioning and changelogs.

### 1. Create a Changeset

When you make changes that should be released, run:

```bash
bun run changeset:add
```

Follow the prompts to select the package and bump type (patch, minor, major). This creates a file in `.changeset/`. Commit this file with your PR.

### 2. Automated Release

When your PR is merged to `main`:
1. The **Release** GitHub Action will create a "Version Packages" PR.
2. When you merge that PR, the action will automatically publish the new version to npm and update the CHANGELOG.

### 3. Pre-releases

To enter pre-release mode (e.g., beta):

```bash
bun run prerelease:enter beta
bun run changeset:version
bun run changeset:publish
```

To exit:
```bash
bun run prerelease:exit
```
If you've found a bug, please [open an issue](https://github.com/codebelt/classy-store/issues). That's the most helpful thing you can do.
81 changes: 67 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# @codebelt/classy-store

**Class-based reactive state management for React.**
**Class-based reactive state management for React, Vue, Svelte, Solid, and Angular.**

[![npm version](https://img.shields.io/npm/v/@codebelt/classy-store.svg)](https://www.npmjs.com/package/@codebelt/classy-store)
[![CI](https://github.com/codebelt/classy-store/actions/workflows/ci.yml/badge.svg)](https://github.com/codebelt/classy-store/actions/workflows/ci.yml)
[![License](https://img.shields.io/npm/l/@codebelt/classy-store.svg)](https://github.com/codebelt/classy-store/blob/main/LICENSE)
[![Install size](https://pkg-size.dev/badge/install/394414)](https://pkg-size.dev/@codebelt/classy-store)
[![Bundle size](https://deno.bundlejs.com/badge?q=@codebelt/classy-store)](https://bundlejs.com/?q=@codebelt/classy-store)

## 📚 Documentation

Expand All @@ -14,8 +16,44 @@ Visit the **[Documentation Website](https://codebelt.github.io/classy-store/)**

- **Class-Based**: Define state and logic using standard ES6 classes.
- **Reactive**: Automatic reactivity using Proxies.
- **React Integration**: Seamless integration with React 18+ hooks.
- **Computed Getters**: Class getters are automatically memoized and only recompute when dependencies change.
- **Framework Bindings**: First-class integrations for React, Vue, Svelte, Solid, and Angular.
- **TypeScript**: Written in TypeScript with first-class type support.
- **Persist**: Save and restore store state with versioning, migration, cross-tab sync, and SSR support.
- **Undo / Redo**: Add undo/redo to any store via a snapshot stack.
- **Subscribe Key**: Watch a single property for changes with previous and current values.
- **DevTools**: Connect to Redux DevTools for state inspection and time-travel debugging.

## ⚡ Quick Example

```ts
// 1. Plain class — fields are state, getters are computed values, the class is the type
class CartStore {
items: { name: string; price: number; qty: number }[] = [];

// Computed: auto-memoized, only recalculates when items change
get total() {
return this.items.reduce((sum, item) => sum + item.price * item.qty, 0);
}

add(name: string, price: number) {
this.items.push({ name, price, qty: 1 });
}
}

// 2. Wrap it once — use it anywhere
const cartStore = createClassyStore(new CartStore());

// 3. Use in any framework — React shown here
function CartTotal() {
const total = useStore(cartStore, (store) => store.total);
return <span>${total.toFixed(2)}</span>;
}

function AddButton() {
return <button onClick={() => cartStore.add('Widget', 9.99)}>Add item</button>;
}
```

## 📦 Installation

Expand All @@ -25,23 +63,38 @@ npm install @codebelt/classy-store
bun add @codebelt/classy-store
```

## 🛠️ Development
## 🤖 AI / LLM Usage

This project uses [Bun](https://bun.sh) for development.
This library provides machine-readable documentation for LLM-powered tools:

```bash
# Install dependencies
bun install
- [`llms.txt`](https://codebelt.github.io/classy-store/llms.txt) — Navigation index for AI assistants
- [`llms-full.txt`](https://codebelt.github.io/classy-store/llms-full.txt) — Complete documentation in one file

# Run tests
bun run test
These files follow the [llms.txt standard](https://llmstxt.org/).

# Run build
bun run build

# Start docs website locally
bun run docs:dev
```
## 💡 Why Another State Library

- A class-based store where the class is the object and the type — no separate interface or type definition needed.
- Getters as computed values, cached automatically and only recalculated when something changes.
- No component wrapping — no observers, no HOCs, just a hook.


## 🙏 Acknowledgements

This library wouldn't exist without the ideas pioneered by these projects. Each one taught us something different, and we took the best of each:

**[MobX](https://github.com/mobxjs/mobx)** — The OG of class-based reactive state. MobX proved that classes with fields, methods, and getters are the most natural way to model state. We took its `makeAutoObservable` philosophy — everything is reactive by default, no decorators or boilerplate — and its automatic computed memoization with fine-grained dependency tracking. MobX showed that getters should "just work" as cached derived values.

**[Valtio](https://github.com/pmndrs/valtio)** — Daishi Kato's proxy-based masterpiece gave us the core architectural pattern: a mutable write proxy for ergonomic mutations paired with immutable snapshots for React integration. Valtio's structural sharing approach — where unchanged sub-trees keep the same frozen reference across snapshots — is what makes `Object.is` selectors efficient without custom equality. We also adopted its `proxy-compare` library for automatic property tracking in selectorless mode.

**[Zustand](https://github.com/pmndrs/zustand)** — Also by Daishi Kato, Zustand set the standard for minimal, hook-first state management. Its selector pattern (`useStore(store, s => s.count)`) with `Object.is` equality is what we use in selector mode. Zustand proved that you don't need Providers, context wrappers, or HOCs — just a hook and a store. Its focus on tiny bundle size pushed us to keep things lean.

**[proxy-compare](https://github.com/dai-shi/proxy-compare)** — The ~1KB utility (also by Dai-shi) that powers our auto-tracked mode. It wraps frozen snapshot objects in a tracking proxy, recording which properties a component reads, then efficiently diffs only those properties between snapshots. This eliminates the need for manual selectors in most cases.

**[React](https://react.dev)** — React 18's `useSyncExternalStore` is the foundation of our hook layer. It provides tear-free concurrent-mode-safe integration with external stores, and it's the same API used by Zustand, Redux, and Valtio under the hood.

**[Claude 4.6 Opus](https://anthropic.com)** — Let's be real: this library was designed, architected, implemented, tested, and documented almost entirely by Claude 4.6 Opus (Anthropic) via [Cursor](https://cursor.com). From the three-layer proxy architecture to the memoized computed getters with dependency tracking, the cross-snapshot caching strategy, and tests — it was all pair-programmed with an AI that never gets tired of writing Proxy traps. The human brought the vision, the taste, and the "no, make it better" energy. Claude brought the code.

## 🤝 Contributing

Expand Down
15 changes: 14 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"!dist",
"!build",
"!**/_generated",
"!coverage"
"!coverage",
"!**/.angular"
]
},
"formatter": {
Expand Down Expand Up @@ -70,6 +71,18 @@
}
},
"overrides": [
{
"includes": ["**/*.vue", "**/*.svelte"],
"linter": {
"rules": {
"correctness": {
"useHookAtTopLevel": "off",
"noUnusedVariables": "off",
"noUnusedImports": "off"
}
}
}
},
{
"includes": ["**/styles.css"],
"linter": {
Expand Down
Loading