Skip to content

Commit 96259f9

Browse files
authored
feat: add framework-specific store utilities (Vue, Svelte, Angular, Solid) (#8)
1 parent 7e25467 commit 96259f9

117 files changed

Lines changed: 5740 additions & 340 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/config.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,11 @@
1212
"access": "public",
1313
"baseBranch": "main",
1414
"updateInternalDependencies": "patch",
15-
"ignore": ["classy-store-demo"]
15+
"ignore": [
16+
"classy-store-react-example",
17+
"classy-store-vue-example",
18+
"classy-store-svelte-example",
19+
"classy-store-solid-example",
20+
"classy-store-angular-example"
21+
]
1622
}

.changeset/two-bugs-stop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@codebelt/classy-store": minor
3+
---
4+
5+
add framework-specific store utilities (Vue, Svelte, Angular, Solid)

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
2727
.cache
2828
*.tsbuildinfo
2929

30+
# Angular CLI build cache
31+
.angular
32+
3033
# IntelliJ based IDEs
3134
.idea
3235

CLAUDE.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# CLAUDE.md — @codebelt/classy-store
2+
3+
Context for Claude Code sessions in this monorepo.
4+
5+
## What This Library Is
6+
7+
`@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.
8+
9+
## Monorepo Layout
10+
11+
```
12+
packages/classy-store/src/
13+
├── core/core.ts # Layer 1: Write Proxy — createClassyStore(), subscribe(), getVersion()
14+
├── snapshot/snapshot.ts # Layer 2: Immutable snapshots — snapshot(), structural sharing
15+
├── types.ts # Snapshot<T>, StoreInternal, DepEntry, ComputedEntry
16+
├── index.ts # Main barrel: createClassyStore, snapshot, subscribe, getVersion, shallowEqual, reactiveMap, reactiveSet
17+
├── collections/collections.ts # ReactiveMap and ReactiveSet (array-backed Map/Set emulation)
18+
├── frameworks/
19+
│ ├── react/react.ts # Layer 3 (React): useStore(), useLocalStore() via useSyncExternalStore
20+
│ ├── vue/vue.ts # Vue: useStore() → ShallowRef<Snapshot<T>> (onUnmounted cleanup)
21+
│ ├── svelte/svelte.ts # Svelte: toSvelteStore() → ClassyReadable<Snapshot<T>>
22+
│ ├── solid/solid.ts # Solid: useStore() → () => Snapshot<T> signal (onCleanup)
23+
│ └── angular/angular.ts # Angular: injectStore() → Signal<Snapshot<T>> (DestroyRef)
24+
└── utils/
25+
├── index.ts # Utils barrel: persist, devtools, subscribeKey, withHistory
26+
├── persist/persist.ts # persist() — storage, transforms, versioning, cross-tab sync, TTL
27+
├── devtools/devtools.ts # devtools() — Redux DevTools integration, time-travel
28+
├── history/history.ts # withHistory() — undo/redo via snapshot stack, pause/resume
29+
├── subscribe-key/subscribe-key.ts # subscribeKey() — single-property subscription
30+
├── equality/equality.ts # shallowEqual
31+
└── internal/internal.ts # isPlainObject, canProxy, findGetterDescriptor, PROXYABLE
32+
33+
website/ # Docusaurus documentation site
34+
website/docs/ # .md source for all doc pages
35+
website/static/ # Served verbatim at site root (llms.txt, llms-full.txt, etc.)
36+
37+
examples/ # Framework demo examples
38+
```
39+
40+
## Key Technical Facts
41+
42+
- **Batching:** mutations are coalesced via `queueMicrotask`. Multiple synchronous writes (including array `push` which triggers multiple SET traps) produce a single subscriber notification.
43+
- **Internal state:** stored in a `WeakMap<proxy, StoreInternal>` — never on the user's object. Allows GC when a store is dereferenced.
44+
- **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.
45+
- **`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.
46+
- **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.
47+
- **Structural sharing:** unchanged sub-trees reuse the previous frozen snapshot reference. This makes `Object.is` comparisons in selectors efficient without `shallowEqual`.
48+
- **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).
49+
50+
## Package Export Entry Points
51+
52+
| Import path | Contents |
53+
|---|---|
54+
| `@codebelt/classy-store` | `createClassyStore`, `snapshot`, `subscribe`, `getVersion`, `shallowEqual`, `reactiveMap`, `reactiveSet`, `Snapshot` type |
55+
| `@codebelt/classy-store/react` | `useStore`, `useLocalStore` |
56+
| `@codebelt/classy-store/vue` | `useStore` (ShallowRef) |
57+
| `@codebelt/classy-store/svelte` | `toSvelteStore` (ClassyReadable) |
58+
| `@codebelt/classy-store/solid` | `useStore` (signal getter) |
59+
| `@codebelt/classy-store/angular` | `injectStore` (Signal) |
60+
| `@codebelt/classy-store/utils` | `persist`, `devtools`, `subscribeKey`, `withHistory` |
61+
62+
## Build & Test Commands
63+
64+
Run from the repo root:
65+
66+
```bash
67+
bun install # Install all workspace dependencies
68+
69+
bun run build # Build all packages (tsdown, outputs to packages/classy-store/dist/)
70+
bun run test # Run all tests (Bun test runner, uses happy-dom for React hook tests)
71+
72+
bun run docs:dev # Start Docusaurus dev server at http://localhost:3000/classy-store/
73+
bun run docs:build # Build docs site to website/build/
74+
```
75+
76+
Run from `packages/classy-store/`:
77+
78+
```bash
79+
bun run dev # Build in watch mode
80+
bun test # Run tests for this package only
81+
bun run typecheck # TypeScript type check without emit
82+
```
83+
84+
## LLM Documentation Files
85+
86+
- `website/static/llms.txt` — navigation index (served at `/classy-store/llms.txt`)
87+
- `website/static/llms-full.txt` — all docs concatenated (served at `/classy-store/llms-full.txt`)
88+
89+
These follow the [llms.txt standard](https://llmstxt.org/).

CONTRIBUTING.md

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,5 @@
11
# Contributing to @codebelt/classy-store
22

3-
## Development Setup
3+
We've got it covered — me and AI.
44

5-
1. Install [Bun](https://bun.sh).
6-
2. Clone the repository.
7-
3. Run `bun install`.
8-
9-
## Workflow
10-
11-
- **Dev**: `bun run dev` (watch mode)
12-
- **Test**: `bun run test`
13-
- **Lint**: `bun run lint`
14-
- **Docs**: `bun run docs:dev`
15-
16-
## Releasing
17-
18-
We use [Changesets](https://github.com/changesets/changesets) to manage versioning and changelogs.
19-
20-
### 1. Create a Changeset
21-
22-
When you make changes that should be released, run:
23-
24-
```bash
25-
bun run changeset:add
26-
```
27-
28-
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.
29-
30-
### 2. Automated Release
31-
32-
When your PR is merged to `main`:
33-
1. The **Release** GitHub Action will create a "Version Packages" PR.
34-
2. When you merge that PR, the action will automatically publish the new version to npm and update the CHANGELOG.
35-
36-
### 3. Pre-releases
37-
38-
To enter pre-release mode (e.g., beta):
39-
40-
```bash
41-
bun run prerelease:enter beta
42-
bun run changeset:version
43-
bun run changeset:publish
44-
```
45-
46-
To exit:
47-
```bash
48-
bun run prerelease:exit
49-
```
5+
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.

README.md

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# @codebelt/classy-store
22

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

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

911
## 📚 Documentation
1012

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

1517
- **Class-Based**: Define state and logic using standard ES6 classes.
1618
- **Reactive**: Automatic reactivity using Proxies.
17-
- **React Integration**: Seamless integration with React 18+ hooks.
19+
- **Computed Getters**: Class getters are automatically memoized and only recompute when dependencies change.
20+
- **Framework Bindings**: First-class integrations for React, Vue, Svelte, Solid, and Angular.
1821
- **TypeScript**: Written in TypeScript with first-class type support.
22+
- **Persist**: Save and restore store state with versioning, migration, cross-tab sync, and SSR support.
23+
- **Undo / Redo**: Add undo/redo to any store via a snapshot stack.
24+
- **Subscribe Key**: Watch a single property for changes with previous and current values.
25+
- **DevTools**: Connect to Redux DevTools for state inspection and time-travel debugging.
26+
27+
## ⚡ Quick Example
28+
29+
```ts
30+
// 1. Plain class — fields are state, getters are computed values, the class is the type
31+
class CartStore {
32+
items: { name: string; price: number; qty: number }[] = [];
33+
34+
// Computed: auto-memoized, only recalculates when items change
35+
get total() {
36+
return this.items.reduce((sum, item) => sum + item.price * item.qty, 0);
37+
}
38+
39+
add(name: string, price: number) {
40+
this.items.push({ name, price, qty: 1 });
41+
}
42+
}
43+
44+
// 2. Wrap it once — use it anywhere
45+
const cartStore = createClassyStore(new CartStore());
46+
47+
// 3. Use in any framework — React shown here
48+
function CartTotal() {
49+
const total = useStore(cartStore, (store) => store.total);
50+
return <span>${total.toFixed(2)}</span>;
51+
}
52+
53+
function AddButton() {
54+
return <button onClick={() => cartStore.add('Widget', 9.99)}>Add item</button>;
55+
}
56+
```
1957

2058
## 📦 Installation
2159

@@ -25,23 +63,38 @@ npm install @codebelt/classy-store
2563
bun add @codebelt/classy-store
2664
```
2765

28-
## 🛠️ Development
66+
## 🤖 AI / LLM Usage
2967

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

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

36-
# Run tests
37-
bun run test
73+
These files follow the [llms.txt standard](https://llmstxt.org/).
3874

39-
# Run build
40-
bun run build
4175

42-
# Start docs website locally
43-
bun run docs:dev
44-
```
76+
## 💡 Why Another State Library
77+
78+
- A class-based store where the class is the object and the type — no separate interface or type definition needed.
79+
- Getters as computed values, cached automatically and only recalculated when something changes.
80+
- No component wrapping — no observers, no HOCs, just a hook.
81+
82+
83+
## 🙏 Acknowledgements
84+
85+
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:
86+
87+
**[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.
88+
89+
**[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.
90+
91+
**[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.
92+
93+
**[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.
94+
95+
**[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.
96+
97+
**[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.
4598

4699
## 🤝 Contributing
47100

biome.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"!dist",
1515
"!build",
1616
"!**/_generated",
17-
"!coverage"
17+
"!coverage",
18+
"!**/.angular"
1819
]
1920
},
2021
"formatter": {
@@ -70,6 +71,18 @@
7071
}
7172
},
7273
"overrides": [
74+
{
75+
"includes": ["**/*.vue", "**/*.svelte"],
76+
"linter": {
77+
"rules": {
78+
"correctness": {
79+
"useHookAtTopLevel": "off",
80+
"noUnusedVariables": "off",
81+
"noUnusedImports": "off"
82+
}
83+
}
84+
}
85+
},
7386
{
7487
"includes": ["**/styles.css"],
7588
"linter": {

0 commit comments

Comments
 (0)