From 971859e58deecf42af0ff13c0681134d3191b82a Mon Sep 17 00:00:00 2001 From: codeBelt Date: Mon, 16 Feb 2026 09:31:55 -0600 Subject: [PATCH 1/3] refactor: replace `store` with `createClassyStore` across the codebase --- examples/rendering/package.json | 2 +- .../src/ReactiveFundamentalsDemo.tsx | 4 +- examples/rendering/src/persistStores.ts | 10 +- examples/rendering/src/stores.ts | 14 ++- src/collections/collections.test.ts | 22 ++-- src/collections/collections.ts | 12 +- src/core/computed.test.tsx | 54 ++++----- src/core/core.test.ts | 64 ++++++----- src/core/core.ts | 18 ++- src/index.ts | 2 +- src/react/react.behavior.test.tsx | 48 ++++---- src/react/react.test.tsx | 31 ++--- src/snapshot/snapshot.test.ts | 36 +++--- src/snapshot/snapshot.ts | 18 +-- src/utils/persist/persist.test.ts | 108 ++++++++++-------- src/utils/persist/persist.ts | 2 +- website/docs/ARCHITECTURE.md | 20 ++-- website/docs/PERSIST_ARCHITECTURE.md | 2 +- website/docs/PERSIST_TUTORIAL.md | 20 ++-- website/docs/TUTORIAL.md | 29 ++--- website/docs/index.md | 39 ++++--- 21 files changed, 292 insertions(+), 263 deletions(-) diff --git a/examples/rendering/package.json b/examples/rendering/package.json index 30dbaa3..eab751c 100644 --- a/examples/rendering/package.json +++ b/examples/rendering/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "dev": "bun --hot src/index.ts", + "dev": "bun --hot --port 3001 src/index.ts", "start": "NODE_ENV=production bun src/index.ts", "build": "bun run build.ts" }, diff --git a/examples/rendering/src/ReactiveFundamentalsDemo.tsx b/examples/rendering/src/ReactiveFundamentalsDemo.tsx index 3bb822f..4a5aa5f 100644 --- a/examples/rendering/src/ReactiveFundamentalsDemo.tsx +++ b/examples/rendering/src/ReactiveFundamentalsDemo.tsx @@ -1,4 +1,4 @@ -import {store} from '@codebelt/classy-store'; +import {createClassyStore} from '@codebelt/classy-store'; import {useStore} from '@codebelt/classy-store/react'; import {useState} from 'react'; import {Panel} from './Panel'; @@ -6,7 +6,7 @@ import {RenderBadge} from './RenderBadge'; import {CounterStore, counterStore} from './stores'; import {useRenderCount} from './useRenderCount'; -const batchStore = store(new CounterStore()); +const batchStore = createClassyStore(new CounterStore()); const names = ['World', 'Bun', 'React', 'Store']; let nameIndex = 0; diff --git a/examples/rendering/src/persistStores.ts b/examples/rendering/src/persistStores.ts index 0f9bdcb..2895f0c 100644 --- a/examples/rendering/src/persistStores.ts +++ b/examples/rendering/src/persistStores.ts @@ -1,4 +1,8 @@ -import {reactiveMap, reactiveSet, store} from '@codebelt/classy-store'; +import { + createClassyStore, + reactiveMap, + reactiveSet, +} from '@codebelt/classy-store'; import {persist} from '@codebelt/classy-store/utils'; // ── Simple Store ──────────────────────────────────────────────────────────── @@ -43,7 +47,7 @@ export class PreferencesStore { } } -export const preferencesStore = store(new PreferencesStore()); +export const preferencesStore = createClassyStore(new PreferencesStore()); export const preferencesHandle = persist(preferencesStore, { name: 'preferences', @@ -145,7 +149,7 @@ export class KitchenSinkStore { } } -export const kitchenSinkStore = store(new KitchenSinkStore()); +export const kitchenSinkStore = createClassyStore(new KitchenSinkStore()); export const kitchenSinkHandle = persist(kitchenSinkStore, { name: 'kitchen-sink', diff --git a/examples/rendering/src/stores.ts b/examples/rendering/src/stores.ts index b3ef62b..ad440e0 100644 --- a/examples/rendering/src/stores.ts +++ b/examples/rendering/src/stores.ts @@ -1,4 +1,8 @@ -import {reactiveMap, reactiveSet, store} from '@codebelt/classy-store'; +import { + createClassyStore, + reactiveMap, + reactiveSet, +} from '@codebelt/classy-store'; export class CounterStore { count = 0; @@ -145,7 +149,7 @@ export class CollectionStore { } } -export const counterStore = store(new CounterStore()); -export const postStore = store(new PostStore()); -export const documentStore = store(new DocumentStore()); -export const collectionStore = store(new CollectionStore()); +export const counterStore = createClassyStore(new CounterStore()); +export const postStore = createClassyStore(new PostStore()); +export const documentStore = createClassyStore(new DocumentStore()); +export const collectionStore = createClassyStore(new CollectionStore()); diff --git a/src/collections/collections.test.ts b/src/collections/collections.test.ts index 35a6ebd..df9ed57 100644 --- a/src/collections/collections.test.ts +++ b/src/collections/collections.test.ts @@ -1,5 +1,5 @@ import {describe, expect, test} from 'bun:test'; -import {store, subscribe} from '../core/core'; +import {createClassyStore, subscribe} from '../core/core'; import {snapshot} from '../snapshot/snapshot'; import {reactiveMap, reactiveSet} from './collections'; @@ -87,7 +87,7 @@ describe('reactiveMap()', () => { }); test('triggers store subscription on set', async () => { - const s = store({users: reactiveMap()}); + const s = createClassyStore({users: reactiveMap()}); let count = 0; subscribe(s, () => count++); @@ -98,7 +98,7 @@ describe('reactiveMap()', () => { }); test('triggers store subscription on delete', async () => { - const s = store({ + const s = createClassyStore({ users: reactiveMap([['id1', 'Alice']] as [string, string][]), }); let count = 0; @@ -111,7 +111,7 @@ describe('reactiveMap()', () => { }); test('triggers store subscription on clear', async () => { - const s = store({ + const s = createClassyStore({ users: reactiveMap([ ['a', 1], ['b', 2], @@ -127,7 +127,9 @@ describe('reactiveMap()', () => { }); test('snapshot captures map data', async () => { - const s = store({m: reactiveMap([['k', 'v']] as [string, string][])}); + const s = createClassyStore({ + m: reactiveMap([['k', 'v']] as [string, string][]), + }); const snap = snapshot(s); expect(snap.m._entries).toEqual([['k', 'v']]); // Snapshot is frozen @@ -188,7 +190,7 @@ describe('reactiveSet()', () => { }); test('triggers store subscription on add', async () => { - const s = store({tags: reactiveSet()}); + const s = createClassyStore({tags: reactiveSet()}); let count = 0; subscribe(s, () => count++); @@ -199,7 +201,7 @@ describe('reactiveSet()', () => { }); test('triggers store subscription on delete', async () => { - const s = store({tags: reactiveSet(['a', 'b'])}); + const s = createClassyStore({tags: reactiveSet(['a', 'b'])}); let count = 0; subscribe(s, () => count++); @@ -210,7 +212,7 @@ describe('reactiveSet()', () => { }); test('triggers store subscription on clear', async () => { - const s = store({tags: reactiveSet(['a', 'b'])}); + const s = createClassyStore({tags: reactiveSet(['a', 'b'])}); let count = 0; subscribe(s, () => count++); @@ -221,7 +223,7 @@ describe('reactiveSet()', () => { }); test('snapshot captures set data', async () => { - const s = store({t: reactiveSet([1, 2, 3])}); + const s = createClassyStore({t: reactiveSet([1, 2, 3])}); const snap = snapshot(s); expect(snap.t._items).toEqual([1, 2, 3]); expect(Object.isFrozen(snap.t)).toBe(true); @@ -245,7 +247,7 @@ describe('collections inside class store', () => { } test('class methods mutate collections reactively', async () => { - const s = store(new ProjectStore()); + const s = createClassyStore(new ProjectStore()); let count = 0; subscribe(s, () => count++); diff --git a/src/collections/collections.ts b/src/collections/collections.ts index 091b083..916e794 100644 --- a/src/collections/collections.ts +++ b/src/collections/collections.ts @@ -3,7 +3,7 @@ import {PROXYABLE} from '../utils/internal/internal'; // ── ReactiveMap ─────────────────────────────────────────────────────────────── /** - * A Map-like class backed by a plain array so `store()` can proxy mutations. + * A Map-like class backed by a plain array so `createClassyStore()` can proxy mutations. * * Native `Map` uses internal slots that ES6 Proxy can't intercept, so mutations * like `.set()` would be invisible to the store. `ReactiveMap` solves this by @@ -11,7 +11,7 @@ import {PROXYABLE} from '../utils/internal/internal'; * * Usage: * ```ts - * const myStore = store({ users: reactiveMap() }); + * const myStore = createClassyStore({ users: reactiveMap() }); * myStore.users.set('id1', { name: 'Alice' }); // reactive * ``` */ @@ -106,7 +106,7 @@ export class ReactiveMap { // ── ReactiveSet ─────────────────────────────────────────────────────────────── /** - * A Set-like class backed by a plain array so `store()` can proxy mutations. + * A Set-like class backed by a plain array so `createClassyStore()` can proxy mutations. * * Native `Set` uses internal slots that ES6 Proxy can't intercept, so mutations * like `.add()` would be invisible to the store. `ReactiveSet` solves this by @@ -114,7 +114,7 @@ export class ReactiveMap { * * Usage: * ```ts - * const myStore = store({ tags: reactiveSet(['urgent']) }); + * const myStore = createClassyStore({ tags: reactiveSet(['urgent']) }); * myStore.tags.add('bug'); // reactive * ``` */ @@ -198,7 +198,7 @@ export class ReactiveSet { /** * Creates a reactive Map-like collection backed by a plain array. - * Wrap the parent object with `store()` for full reactivity. + * Wrap the parent object with `createClassyStore()` for full reactivity. * * @param initial - Optional iterable of `[key, value]` pairs. */ @@ -210,7 +210,7 @@ export function reactiveMap( /** * Creates a reactive Set-like collection backed by a plain array. - * Wrap the parent object with `store()` for full reactivity. + * Wrap the parent object with `createClassyStore()` for full reactivity. * * @param initial - Optional iterable of values. */ diff --git a/src/core/computed.test.tsx b/src/core/computed.test.tsx index 3c63158..fef74e5 100644 --- a/src/core/computed.test.tsx +++ b/src/core/computed.test.tsx @@ -3,7 +3,7 @@ import {act, type ReactNode} from 'react'; import {createRoot} from 'react-dom/client'; import {useStore} from '../react/react'; import {snapshot} from '../snapshot/snapshot'; -import {store} from './core'; +import {createClassyStore} from './core'; /** Helper: flush the queueMicrotask-based batching. */ const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); @@ -26,7 +26,7 @@ describe('computed getters — write proxy memoization', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); const result1 = s.filtered; const result2 = s.filtered; @@ -55,7 +55,7 @@ describe('computed getters — write proxy memoization', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); expect(s.doubled).toBe(10); expect(callCount).toBe(1); @@ -83,7 +83,7 @@ describe('computed getters — write proxy memoization', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); expect(s.doubled).toBe(10); expect(callCount).toBe(1); @@ -114,7 +114,7 @@ describe('computed getters — write proxy memoization', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); expect(s.filteredCount).toBe(3); expect(filteredCallCount).toBe(1); @@ -143,7 +143,7 @@ describe('computed getters — write proxy memoization', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); expect(s.count).toBe(2); expect(callCount).toBe(1); @@ -168,7 +168,7 @@ describe('computed getters — write proxy memoization', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); expect(s.currentTheme).toBe('dark'); expect(callCount).toBe(1); @@ -199,7 +199,7 @@ describe('computed getters — write proxy memoization', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); expect(s.sum).toBe(6); expect(s.average).toBe(2); @@ -229,7 +229,7 @@ describe('computed getters — write proxy memoization', () => { } } - const s = store(new Derived()); + const s = createClassyStore(new Derived()); expect(s.label).toBe('derived:5'); // Snapshot should also use the derived getter @@ -255,7 +255,7 @@ describe('computed getters — write proxy memoization', () => { } } - const s = store(new Derived()); + const s = createClassyStore(new Derived()); // Write proxy: both getters work expect(s.doubled).toBe(10); // base getter @@ -288,7 +288,7 @@ describe('computed getters — write proxy memoization', () => { // `doubled` is NOT overridden — inherits from Base } - const s = store(new Derived()); + const s = createClassyStore(new Derived()); // Override wins for `label` expect(s.label).toBe('derived:5'); @@ -323,7 +323,7 @@ describe('computed getters — write proxy memoization', () => { } } - const s = store(new LevelC()); + const s = createClassyStore(new LevelC()); // Write proxy expect(s.squared).toBe(4); @@ -351,7 +351,7 @@ describe('computed getters — write proxy memoization', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); expect(s.isPositive).toBe(false); @@ -380,7 +380,7 @@ describe('computed getters — write proxy memoization', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); expect(s.count).toBe(3); expect(callCount).toBe(1); @@ -408,7 +408,7 @@ describe('computed getters — write proxy memoization', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); // First access: returns undefined, cached. expect(s.first).toBeUndefined(); @@ -441,7 +441,7 @@ describe('computed getters — write proxy memoization', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); expect(s.constant).toBe(42); expect(callCount).toBe(1); @@ -466,7 +466,7 @@ describe('computed getters — write proxy memoization', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); // First access throws. expect(() => s.computed).toThrow('intentional error'); @@ -492,7 +492,7 @@ describe('computed getters — write proxy memoization', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); expect(s.sum).toBe(3); // Replace the data object entirely (delete is hard to test on child proxies @@ -514,7 +514,7 @@ describe('computed getters — write proxy memoization', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); expect(s.hasA).toBe(true); expect(callCount).toBe(1); @@ -548,7 +548,7 @@ describe('computed getters — snapshot memoization', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); const snap = snapshot(s); expect(snap.count).toBe(3); @@ -568,7 +568,7 @@ describe('computed getters — snapshot memoization', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); const snap = snapshot(s); const result1 = snap.filtered; @@ -593,7 +593,7 @@ describe('computed getters — snapshot memoization', () => { } } - const s = store(new TodoStore()); + const s = createClassyStore(new TodoStore()); const snap1 = snapshot(s); const result1 = snap1.activeTodos; @@ -622,7 +622,7 @@ describe('computed getters — snapshot memoization', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); const snap1 = snapshot(s); expect(snap1.doubled).toBe(10); @@ -651,7 +651,7 @@ describe('computed getters — snapshot memoization', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); const snap1 = snapshot(s); expect(snap1.count).toBe(3); @@ -674,7 +674,7 @@ describe('computed getters — snapshot memoization', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); const snap = snapshot(s); expect(snap.fullName).toBe('John Doe'); @@ -720,7 +720,7 @@ describe('computed getters — useStore integration', () => { } } - const todoStore = store(new TodoStore()); + const todoStore = createClassyStore(new TodoStore()); const renderCount = mock(() => {}); function ActiveList() { @@ -759,7 +759,7 @@ describe('computed getters — useStore integration', () => { } } - const s = store(new Store()); + const s = createClassyStore(new Store()); function Display() { const snap = useStore(s); diff --git a/src/core/core.test.ts b/src/core/core.test.ts index 863a8e3..fd1f0ae 100644 --- a/src/core/core.test.ts +++ b/src/core/core.test.ts @@ -1,21 +1,21 @@ import {describe, expect, it, mock} from 'bun:test'; -import {getVersion, store, subscribe} from './core'; +import {createClassyStore, getVersion, subscribe} from './core'; /** Helper: flush the queueMicrotask-based batching. */ const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); -describe('store() — core reactivity', () => { +describe('createClassyStore() — core reactivity', () => { // ── Primitive mutations ─────────────────────────────────────────────────── describe('primitive mutations', () => { it('reads initial values through the proxy', () => { - const s = store({count: 0, name: 'hello'}); + const s = createClassyStore({count: 0, name: 'hello'}); expect(s.count).toBe(0); expect(s.name).toBe('hello'); }); it('notifies listeners when a primitive property changes', async () => { - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const listener = mock(() => {}); subscribe(s, listener); @@ -27,7 +27,7 @@ describe('store() — core reactivity', () => { }); it('does NOT notify when same value is set (noop)', async () => { - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const listener = mock(() => {}); subscribe(s, listener); @@ -38,7 +38,7 @@ describe('store() — core reactivity', () => { }); it('bumps version on mutation', async () => { - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const v1 = getVersion(s); s.count = 1; @@ -53,7 +53,7 @@ describe('store() — core reactivity', () => { describe('batching', () => { it('batches multiple synchronous mutations into one notification', async () => { - const s = store({a: 0, b: 0, c: 0}); + const s = createClassyStore({a: 0, b: 0, c: 0}); const listener = mock(() => {}); subscribe(s, listener); @@ -69,7 +69,7 @@ describe('store() — core reactivity', () => { }); it('batches array push (multiple set traps) into one notification', async () => { - const s = store({items: [] as string[]}); + const s = createClassyStore({items: [] as string[]}); const listener = mock(() => {}); subscribe(s, listener); @@ -97,7 +97,7 @@ describe('store() — core reactivity', () => { } it('methods mutate through the proxy', async () => { - const s = store(new Counter()); + const s = createClassyStore(new Counter()); const listener = mock(() => {}); subscribe(s, listener); @@ -109,7 +109,7 @@ describe('store() — core reactivity', () => { }); it('methods with arguments work correctly', async () => { - const s = store(new Counter()); + const s = createClassyStore(new Counter()); s.add(10); await flush(); expect(s.count).toBe(10); @@ -136,13 +136,13 @@ describe('store() — core reactivity', () => { } it('getters return computed values', () => { - const s = store(new Store()); + const s = createClassyStore(new Store()); expect(s.doubled).toBe(10); expect(s.isPositive).toBe(true); }); it('getters reflect mutations', async () => { - const s = store(new Store()); + const s = createClassyStore(new Store()); s.setCount(0); expect(s.doubled).toBe(0); expect(s.isPositive).toBe(false); @@ -153,7 +153,9 @@ describe('store() — core reactivity', () => { describe('deep nested objects', () => { it('nested object property mutations trigger root listener', async () => { - const s = store({user: {name: 'Alice', address: {city: 'NYC'}}}); + const s = createClassyStore({ + user: {name: 'Alice', address: {city: 'NYC'}}, + }); const listener = mock(() => {}); subscribe(s, listener); @@ -165,7 +167,9 @@ describe('store() — core reactivity', () => { }); it('deeply nested mutations trigger root listener', async () => { - const s = store({user: {name: 'Alice', address: {city: 'NYC'}}}); + const s = createClassyStore({ + user: {name: 'Alice', address: {city: 'NYC'}}, + }); const listener = mock(() => {}); subscribe(s, listener); @@ -177,7 +181,7 @@ describe('store() — core reactivity', () => { }); it('replacing a nested object triggers listener', async () => { - const s = store({user: {name: 'Alice'}}); + const s = createClassyStore({user: {name: 'Alice'}}); const listener = mock(() => {}); subscribe(s, listener); @@ -193,7 +197,7 @@ describe('store() — core reactivity', () => { describe('array operations', () => { it('push triggers listener', async () => { - const s = store({items: ['a']}); + const s = createClassyStore({items: ['a']}); const listener = mock(() => {}); subscribe(s, listener); @@ -205,7 +209,7 @@ describe('store() — core reactivity', () => { }); it('splice triggers listener', async () => { - const s = store({items: ['a', 'b', 'c']}); + const s = createClassyStore({items: ['a', 'b', 'c']}); const listener = mock(() => {}); subscribe(s, listener); @@ -217,7 +221,7 @@ describe('store() — core reactivity', () => { }); it('index assignment triggers listener', async () => { - const s = store({items: ['a', 'b']}); + const s = createClassyStore({items: ['a', 'b']}); const listener = mock(() => {}); subscribe(s, listener); @@ -229,7 +233,7 @@ describe('store() — core reactivity', () => { }); it('array of objects: nested mutation triggers listener', async () => { - const s = store({items: [{name: 'Alice'}, {name: 'Bob'}]}); + const s = createClassyStore({items: [{name: 'Alice'}, {name: 'Bob'}]}); const listener = mock(() => {}); subscribe(s, listener); @@ -245,7 +249,7 @@ describe('store() — core reactivity', () => { describe('subscribe / unsubscribe', () => { it('unsubscribe stops notifications', async () => { - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const listener = mock(() => {}); const unsub = subscribe(s, listener); @@ -260,7 +264,7 @@ describe('store() — core reactivity', () => { }); it('multiple listeners all fire', async () => { - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const listener1 = mock(() => {}); const listener2 = mock(() => {}); subscribe(s, listener1); @@ -274,7 +278,7 @@ describe('store() — core reactivity', () => { }); it('subscribe on a child proxy fires when the child mutates', async () => { - const s = store({user: {name: 'Alice'}}); + const s = createClassyStore({user: {name: 'Alice'}}); const listener = mock(() => {}); subscribe(s.user, listener); @@ -285,7 +289,7 @@ describe('store() — core reactivity', () => { }); it('subscribe on a child proxy fires when a sibling mutates', async () => { - const s = store({user: {name: 'Alice'}, count: 0}); + const s = createClassyStore({user: {name: 'Alice'}, count: 0}); const listener = mock(() => {}); subscribe(s.user, listener); @@ -297,7 +301,7 @@ describe('store() — core reactivity', () => { }); it('unsubscribe from child proxy stops notifications', async () => { - const s = store({user: {name: 'Alice'}}); + const s = createClassyStore({user: {name: 'Alice'}}); const listener = mock(() => {}); const unsub = subscribe(s.user, listener); @@ -316,7 +320,7 @@ describe('store() — core reactivity', () => { describe('deleteProperty', () => { it('deleting a property triggers listener', async () => { - const s = store({a: 1, b: 2} as Record); + const s = createClassyStore({a: 1, b: 2} as Record); const listener = mock(() => {}); subscribe(s, listener); @@ -348,7 +352,7 @@ describe('store() — core reactivity', () => { } it('base method mutates state reactively on a derived instance', async () => { - const s = store(new Derived()); + const s = createClassyStore(new Derived()); const listener = mock(() => {}); subscribe(s, listener); @@ -360,7 +364,7 @@ describe('store() — core reactivity', () => { }); it('derived method works alongside inherited method', async () => { - const s = store(new Derived()); + const s = createClassyStore(new Derived()); const listener = mock(() => {}); subscribe(s, listener); @@ -383,7 +387,7 @@ describe('store() — core reactivity', () => { } } - const s = store(new Extended()); + const s = createClassyStore(new Extended()); const listener = mock(() => {}); subscribe(s, listener); @@ -405,7 +409,7 @@ describe('store() — core reactivity', () => { } } - const s = store(new Overrider()); + const s = createClassyStore(new Overrider()); const listener = mock(() => {}); subscribe(s, listener); @@ -442,7 +446,7 @@ describe('store() — core reactivity', () => { } } - const s = store(new LevelC()); + const s = createClassyStore(new LevelC()); const listener = mock(() => {}); subscribe(s, listener); diff --git a/src/core/core.ts b/src/core/core.ts index bb08983..6ef6131 100644 --- a/src/core/core.ts +++ b/src/core/core.ts @@ -113,7 +113,7 @@ function evaluateComputed( /** * Retrieve the internal bookkeeping for a store proxy. - * Throws if the object was not created with `store()`. + * Throws if the object was not created with `createClassyStore()`. */ export function getInternal(proxy: object): StoreInternal { const internal = internalsMap.get(proxy); @@ -122,13 +122,6 @@ export function getInternal(proxy: object): StoreInternal { return internal; } -/** - * Returns `true` if the given object is a store proxy. - */ -export function isStoreProxy(value: unknown): boolean { - return typeof value === 'object' && value !== null && internalsMap.has(value); -} - // ── Notification batching ───────────────────────────────────────────────────── /** @@ -289,8 +282,13 @@ function createStoreProxy( * * @param instance - A class instance (or plain object) to make reactive. * @returns The same object wrapped in a reactive Proxy. + * + * @example + * ```ts + * const myStore = createClassyStore(new MyClass()); + * ``` */ -export function store(instance: T): T { +export function createClassyStore(instance: T): T { return createStoreProxy(instance, null); } @@ -298,7 +296,7 @@ export function store(instance: T): T { * Subscribe to store changes. The callback fires once per batched mutation * (coalesced via `queueMicrotask`), not once per individual property write. * - * @param proxy - A reactive proxy created by `store()`. + * @param proxy - A reactive proxy created by `createClassyStore()`. * @param callback - Invoked after each batched mutation. * @returns An unsubscribe function. Call it to stop receiving notifications. */ diff --git a/src/index.ts b/src/index.ts index b9cef8f..31fd8a0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,7 @@ */ export type {ReactiveMap, ReactiveSet} from './collections/collections'; export {reactiveMap, reactiveSet} from './collections/collections'; -export {getVersion, store, subscribe} from './core/core'; +export {createClassyStore, getVersion, subscribe} from './core/core'; export {snapshot} from './snapshot/snapshot'; export type {Snapshot} from './types'; export {shallowEqual} from './utils/equality/equality'; diff --git a/src/react/react.behavior.test.tsx b/src/react/react.behavior.test.tsx index 7359ec8..a38367a 100644 --- a/src/react/react.behavior.test.tsx +++ b/src/react/react.behavior.test.tsx @@ -1,7 +1,7 @@ import {afterEach, describe, expect, it, mock} from 'bun:test'; import {act, type ReactNode} from 'react'; import {createRoot, type Root} from 'react-dom/client'; -import {store} from '../core/core'; +import {createClassyStore} from '../core/core'; import {shallowEqual} from '../utils/equality/equality'; import {useStore} from './react'; @@ -82,7 +82,7 @@ describe('Batching — multi-prop methods cause single re-render', () => { afterEach(teardown); it('method writing two props (selector mode)', async () => { - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCount = mock(() => {}); function Display() { @@ -110,7 +110,7 @@ describe('Batching — multi-prop methods cause single re-render', () => { }); it('method writing two props (auto-tracked mode)', async () => { - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCount = mock(() => {}); function Display() { @@ -137,7 +137,7 @@ describe('Batching — multi-prop methods cause single re-render', () => { }); it('method writing two props, only one selected', async () => { - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCount = mock(() => {}); function CountOnly() { @@ -166,7 +166,7 @@ describe('Batching — rapid mutations in a loop', () => { afterEach(teardown); it('100 increments in for-loop (selector mode)', async () => { - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCount = mock(() => {}); function Display() { @@ -189,7 +189,7 @@ describe('Batching — rapid mutations in a loop', () => { }); it('100 increments in for-loop (auto-tracked mode)', async () => { - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCount = mock(() => {}); function Display() { @@ -212,7 +212,7 @@ describe('Batching — rapid mutations in a loop', () => { }); it('50 array pushes in for-loop (selector on length)', async () => { - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCount = mock(() => {}); function Display() { @@ -242,7 +242,7 @@ describe('Set-then-revert — no re-render when value returns to original', () = afterEach(teardown); it('primitive revert (selector mode)', async () => { - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCount = mock(() => {}); function Display() { @@ -269,7 +269,7 @@ describe('Set-then-revert — no re-render when value returns to original', () = // Unlike selector mode (which compares the extracted value), auto-tracked mode // detects that the snapshot *object* is a new reference (version was bumped), // so `getAutoTrackSnapshot` builds a new tracking proxy → re-render. - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCount = mock(() => {}); function Display() { @@ -293,7 +293,7 @@ describe('Set-then-revert — no re-render when value returns to original', () = }); it('nested object prop revert (selector on user.name)', async () => { - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCount = mock(() => {}); function Display() { @@ -318,7 +318,7 @@ describe('Set-then-revert — no re-render when value returns to original', () = }); it('unrelated prop revert — structural sharing preserves reference', async () => { - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCount = mock(() => {}); function Display() { @@ -351,7 +351,7 @@ describe('Async methods — re-renders split across await boundaries', () => { it('1 await, mutations before + after (selector)', async () => { // React batches both microtask notifications within act() into a single // commit, so we see 1 re-render (not 2) for mutations on both sides of an await. - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCount = mock(() => {}); function Display() { @@ -376,7 +376,7 @@ describe('Async methods — re-renders split across await boundaries', () => { it('2 awaits, mutations between each (selector)', async () => { // 3 mutations across 3 turns — React batches some of the microtask // notifications, resulting in 2 re-renders rather than 3. - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCount = mock(() => {}); function Display() { @@ -399,7 +399,7 @@ describe('Async methods — re-renders split across await boundaries', () => { }); it('mutations only before await (selector)', async () => { - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCount = mock(() => {}); function Display() { @@ -422,7 +422,7 @@ describe('Async methods — re-renders split across await boundaries', () => { }); it('mutations only after await (selector)', async () => { - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCount = mock(() => {}); function Display() { @@ -445,7 +445,7 @@ describe('Async methods — re-renders split across await boundaries', () => { }); it('1 await, mutations before + after (auto-tracked)', async () => { - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCount = mock(() => {}); function Display() { @@ -474,7 +474,7 @@ describe('Multiple components sharing the same store', () => { afterEach(teardown); it('different selectors, independent re-render', async () => { - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCountA = mock(() => {}); const renderCountB = mock(() => {}); @@ -510,7 +510,7 @@ describe('Multiple components sharing the same store', () => { }); it('same selector, both re-render', async () => { - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCountA = mock(() => {}); const renderCountB = mock(() => {}); @@ -549,7 +549,7 @@ describe('Multiple components sharing the same store', () => { // In auto-tracked mode, when the store notifies all subscribers, each // component's getSnapshot produces a new snapshot ref — both components // re-render, unlike selector mode which compares extracted values. - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCountA = mock(() => {}); const renderCountB = mock(() => {}); @@ -585,7 +585,7 @@ describe('Multiple components sharing the same store', () => { }); it('batched multi-prop, both affected', async () => { - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCountA = mock(() => {}); const renderCountB = mock(() => {}); @@ -627,7 +627,7 @@ describe('Unmount safety', () => { afterEach(teardown); it('unmount during pending microtask does not crash', async () => { - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); function Display() { const count = useStore(s, (snap) => snap.count); @@ -649,7 +649,7 @@ describe('Unmount safety', () => { }); it('unmount and remount with same store shows latest state', async () => { - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); function Display() { const count = useStore(s, (snap) => snap.count); @@ -683,7 +683,7 @@ describe('shallowEqual as useStore isEqual — integration', () => { afterEach(teardown); it('prevents re-render when shallow values are unchanged', async () => { - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCount = mock(() => {}); function Display() { @@ -714,7 +714,7 @@ describe('shallowEqual as useStore isEqual — integration', () => { }); it('re-renders when shallow values differ', async () => { - const s = store(new TestStore()); + const s = createClassyStore(new TestStore()); const renderCount = mock(() => {}); function Display() { diff --git a/src/react/react.test.tsx b/src/react/react.test.tsx index e966d04..b57ba5a 100644 --- a/src/react/react.test.tsx +++ b/src/react/react.test.tsx @@ -1,7 +1,7 @@ import {afterEach, describe, expect, it, mock} from 'bun:test'; import {act, type ReactNode} from 'react'; import {createRoot} from 'react-dom/client'; -import {store} from '../core/core'; +import {createClassyStore} from '../core/core'; import {useStore} from './react'; // ── Test harness ──────────────────────────────────────────────────────────── @@ -37,7 +37,7 @@ describe('useStore — selector mode', () => { class Counter { count = 42; } - const s = store(new Counter()); + const s = createClassyStore(new Counter()); function Display() { const count = useStore(s, (snap) => snap.count); @@ -56,7 +56,7 @@ describe('useStore — selector mode', () => { this.count++; } } - const s = store(new Counter()); + const s = createClassyStore(new Counter()); const renderCount = mock(() => {}); function Display() { @@ -80,7 +80,7 @@ describe('useStore — selector mode', () => { }); it('does NOT re-render when unrelated prop changes', async () => { - const s = store({count: 0, name: 'hello'}); + const s = createClassyStore({count: 0, name: 'hello'}); const renderCount = mock(() => {}); function CountDisplay() { @@ -111,7 +111,7 @@ describe('useStore — selector mode', () => { this.user.name = name; } } - const s = store(new Store()); + const s = createClassyStore(new Store()); const renderCount = mock(() => {}); function UserDisplay() { @@ -136,7 +136,7 @@ describe('useStore — selector mode', () => { }); it('handles array selectors', async () => { - const s = store({items: ['a', 'b']}); + const s = createClassyStore({items: ['a', 'b']}); const renderCount = mock(() => {}); function List() { @@ -169,7 +169,7 @@ describe('useStore — selector mode', () => { this.count = value; } } - const s = store(new Store()); + const s = createClassyStore(new Store()); function Display() { const doubled = useStore(s, (snap) => snap.doubled); @@ -189,7 +189,7 @@ describe('useStore — selector mode', () => { }); it('supports custom isEqual for selector', async () => { - const s = store({items: [{id: 1, name: 'a'}]}); + const s = createClassyStore({items: [{id: 1, name: 'a'}]}); const renderCount = mock(() => {}); // Selector always returns a new array reference, but custom isEqual does shallow comparison. @@ -224,7 +224,7 @@ describe('useStore — auto-tracked mode', () => { afterEach(teardown); it('renders accessed properties', () => { - const s = store({count: 42, name: 'hello'}); + const s = createClassyStore({count: 42, name: 'hello'}); function Display() { const snap = useStore(s); @@ -237,7 +237,7 @@ describe('useStore — auto-tracked mode', () => { }); it('re-renders when accessed property changes', async () => { - const s = store({count: 0, name: 'hello'}); + const s = createClassyStore({count: 0, name: 'hello'}); const renderCount = mock(() => {}); function Display() { @@ -261,7 +261,7 @@ describe('useStore — auto-tracked mode', () => { }); it('does NOT re-render when non-accessed property changes', async () => { - const s = store({count: 0, name: 'hello'}); + const s = createClassyStore({count: 0, name: 'hello'}); const renderCount = mock(() => {}); function CountOnly() { @@ -284,7 +284,10 @@ describe('useStore — auto-tracked mode', () => { }); it('tracks nested object property access', async () => { - const s = store({user: {name: 'Alice', age: 30}, theme: 'dark'}); + const s = createClassyStore({ + user: {name: 'Alice', age: 30}, + theme: 'dark', + }); const renderCount = mock(() => {}); function UserName() { @@ -315,7 +318,7 @@ describe('useStore — auto-tracked mode', () => { }); it('tracks array length and element access', async () => { - const s = store({items: ['a', 'b'], other: 'x'}); + const s = createClassyStore({items: ['a', 'b'], other: 'x'}); const renderCount = mock(() => {}); function ItemCount() { @@ -348,7 +351,7 @@ describe('useStore — auto-tracked mode', () => { this.count = value; } } - const s = store(new Store()); + const s = createClassyStore(new Store()); function Display() { const snap = useStore(s); diff --git a/src/snapshot/snapshot.test.ts b/src/snapshot/snapshot.test.ts index 450c3c1..fcbdb54 100644 --- a/src/snapshot/snapshot.test.ts +++ b/src/snapshot/snapshot.test.ts @@ -1,5 +1,5 @@ import {describe, expect, it} from 'bun:test'; -import {store} from '../core/core'; +import {createClassyStore} from '../core/core'; import {snapshot} from './snapshot'; /** Helper: flush the queueMicrotask-based batching. */ @@ -10,7 +10,7 @@ describe('snapshot()', () => { describe('freezing', () => { it('returns a deeply frozen object', () => { - const s = store({user: {name: 'Alice'}, items: [1, 2, 3]}); + const s = createClassyStore({user: {name: 'Alice'}, items: [1, 2, 3]}); const snap = snapshot(s); expect(Object.isFrozen(snap)).toBe(true); @@ -19,7 +19,7 @@ describe('snapshot()', () => { }); it('throws when attempting to mutate a snapshot', () => { - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const snap = snapshot(s); expect(() => { @@ -32,7 +32,7 @@ describe('snapshot()', () => { describe('version cache', () => { it('returns the same snapshot object when version has not changed', () => { - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const snap1 = snapshot(s); const snap2 = snapshot(s); @@ -40,7 +40,7 @@ describe('snapshot()', () => { }); it('returns a new snapshot after mutation + flush', async () => { - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const snap1 = snapshot(s); s.count = 1; @@ -57,7 +57,7 @@ describe('snapshot()', () => { describe('structural sharing', () => { it('unchanged nested objects retain the same reference across snapshots', async () => { - const s = store({ + const s = createClassyStore({ user: {name: 'Alice'}, settings: {theme: 'dark'}, }); @@ -74,7 +74,7 @@ describe('snapshot()', () => { }); it('unchanged array elements retain the same reference', async () => { - const s = store({ + const s = createClassyStore({ items: [ {id: 1, name: 'a'}, {id: 2, name: 'b'}, @@ -112,7 +112,7 @@ describe('snapshot()', () => { } it('getters evaluate correctly on the snapshot', () => { - const s = store(new Store()); + const s = createClassyStore(new Store()); const snap = snapshot(s); expect(snap.doubled).toBe(10); @@ -120,7 +120,7 @@ describe('snapshot()', () => { }); it('getters reflect mutations in subsequent snapshots', async () => { - const s = store(new Store()); + const s = createClassyStore(new Store()); s.setCount(10); await flush(); @@ -135,7 +135,7 @@ describe('snapshot()', () => { describe('array snapshots', () => { it('array push is reflected in new snapshot', async () => { - const s = store({items: ['a', 'b']}); + const s = createClassyStore({items: ['a', 'b']}); const snap1 = snapshot(s); s.items.push('c'); @@ -147,7 +147,7 @@ describe('snapshot()', () => { }); it('array splice is reflected in new snapshot', async () => { - const s = store({items: ['a', 'b', 'c']}); + const s = createClassyStore({items: ['a', 'b', 'c']}); const snap1 = snapshot(s); s.items.splice(1, 1); @@ -159,7 +159,7 @@ describe('snapshot()', () => { }); it('replacing array by reference triggers new snapshot', async () => { - const s = store({items: [1, 2, 3]}); + const s = createClassyStore({items: [1, 2, 3]}); const snap1 = snapshot(s); s.items = [4, 5]; @@ -175,14 +175,14 @@ describe('snapshot()', () => { describe('edge cases', () => { it('snapshot of an empty store', () => { - const s = store({}); + const s = createClassyStore({}); const snap = snapshot(s); expect(snap).toEqual({}); expect(Object.isFrozen(snap)).toBe(true); }); it('snapshot captures null and undefined values', () => { - const s = store({ + const s = createClassyStore({ a: null as string | null, b: undefined as string | undefined, }); @@ -221,7 +221,7 @@ describe('snapshot()', () => { } it('snapshot preserves instanceof for the derived class', () => { - const s = store(new Derived()); + const s = createClassyStore(new Derived()); const snap = snapshot(s); expect(snap instanceof Derived).toBe(true); @@ -229,7 +229,7 @@ describe('snapshot()', () => { }); it('snapshot includes own properties from all inheritance levels', () => { - const s = store(new Derived()); + const s = createClassyStore(new Derived()); const snap = snapshot(s); // Base-level properties @@ -240,7 +240,7 @@ describe('snapshot()', () => { }); it('structural sharing works across inheritance levels', async () => { - const s = store(new Derived()); + const s = createClassyStore(new Derived()); const snap1 = snapshot(s); // Mutate only derived-level property, leave base-level nested object untouched @@ -254,7 +254,7 @@ describe('snapshot()', () => { }); it('getters from multiple inheritance levels evaluate correctly in snapshot', () => { - const s = store(new Derived()); + const s = createClassyStore(new Derived()); s.count = 5; s.extra = 'tag'; diff --git a/src/snapshot/snapshot.ts b/src/snapshot/snapshot.ts index f1d5fd8..2e4177d 100644 --- a/src/snapshot/snapshot.ts +++ b/src/snapshot/snapshot.ts @@ -8,7 +8,7 @@ import {canProxy, findGetterDescriptor} from '../utils/internal/internal'; * Version-stamped snapshot cache for tracked (proxied) sub-trees. * Key: raw target object → [version, frozen snapshot]. */ -const snapCache = new WeakMap(); +const snapshotCache = new WeakMap(); /** * Cache for untracked nested objects (never accessed through the proxy). @@ -50,7 +50,7 @@ type GetterMemoEntry = {deps: GetterDep[]; result: unknown}; * boundaries: if `this.items` hasn't changed between snapshots (same * reference via structural sharing), the getter returns the same result. */ -const crossSnapMemo = new WeakMap< +const crossSnapshotMemo = new WeakMap< object, Map >(); @@ -95,7 +95,7 @@ function computeWithTracking( * guarantees stable refs for unchanged sub-trees). For getter properties * this invokes the getter (which is itself memoized), then compares. */ -function areMemoedDepsValid(currentSnap: object, deps: GetterDep[]): boolean { +function areMemoizedDepsValid(currentSnap: object, deps: GetterDep[]): boolean { for (const dep of deps) { const currentValue = Reflect.get(currentSnap, dep.prop, currentSnap); if (!Object.is(currentValue, dep.value)) return false; @@ -123,12 +123,12 @@ function evaluateSnapshotGetter( if (perSnapCache?.has(key)) return perSnapCache.get(key); // ── Cross-snapshot memo ── - let memoMap = crossSnapMemo.get(target); + let memoMap = crossSnapshotMemo.get(target); const prev = memoMap?.get(key); let result: unknown; - if (prev && areMemoedDepsValid(currentSnap, prev.deps)) { + if (prev && areMemoizedDepsValid(currentSnap, prev.deps)) { // Dependencies unchanged → reuse previous result. result = prev.result; } else { @@ -139,7 +139,7 @@ function evaluateSnapshotGetter( // Save cross-snapshot memo. if (!memoMap) { memoMap = new Map(); - crossSnapMemo.set(target, memoMap); + crossSnapshotMemo.set(target, memoMap); } memoMap.set(key, {deps: computation.deps, result}); } @@ -286,7 +286,7 @@ function createSnapshotRecursive( internal: StoreInternal, ): T { // Cache hit: version unchanged → return the same frozen snapshot reference. - const cached = snapCache.get(target); + const cached = snapshotCache.get(target); if (cached && cached[0] === internal.version) { return cached[1] as T; } @@ -321,7 +321,7 @@ function createSnapshotRecursive( Object.freeze(snap); // Cache AFTER populating + freezing. The reference is stable. - snapCache.set(target, [internal.version, snap]); + snapshotCache.set(target, [internal.version, snap]); return snap as T; } @@ -340,7 +340,7 @@ function createSnapshotRecursive( * per snapshot and their results are stable across snapshots when dependencies * haven't changed (cross-snapshot memoization). * - * @param proxyStore - A reactive proxy created by `store()`. + * @param proxyStore - A reactive proxy created by `createClassyStore()`. * @returns A deeply frozen plain-JS object (Snapshot). */ export function snapshot(proxyStore: T): Snapshot { diff --git a/src/utils/persist/persist.test.ts b/src/utils/persist/persist.test.ts index ac0c8af..16385f9 100644 --- a/src/utils/persist/persist.test.ts +++ b/src/utils/persist/persist.test.ts @@ -1,7 +1,7 @@ import {describe, expect, it, mock} from 'bun:test'; import type {ReactiveMap} from '../../collections/collections'; import {reactiveMap} from '../../collections/collections'; -import {store} from '../../core/core'; +import {createClassyStore} from '../../core/core'; import type {StorageAdapter} from './persist'; import {persist} from './persist'; @@ -71,7 +71,7 @@ describe('persist()', () => { describe('basic round-trip', () => { it('saves store state to storage on mutation', async () => { const storage = createMockStorage(); - const s = store({count: 0, name: 'hello'}); + const s = createClassyStore({count: 0, name: 'hello'}); persist(s, {name: 'test', storage}); s.count = 42; @@ -90,7 +90,7 @@ describe('persist()', () => { JSON.stringify({version: 0, state: {count: 99, name: 'restored'}}), ); - const s = store({count: 0, name: 'initial'}); + const s = createClassyStore({count: 0, name: 'initial'}); const handle = persist(s, {name: 'test', storage}); await handle.hydrated; @@ -100,7 +100,7 @@ describe('persist()', () => { it('wraps state in a versioned envelope', async () => { const storage = createMockStorage(); - const s = store({value: 1}); + const s = createClassyStore({value: 1}); persist(s, {name: 'test', storage, version: 3}); s.value = 2; @@ -120,7 +120,7 @@ describe('persist()', () => { } const storage = createMockStorage(); - const s = store(new MyStore()); + const s = createClassyStore(new MyStore()); persist(s, {name: 'test', storage}); s.count = 10; @@ -140,7 +140,7 @@ describe('persist()', () => { } const storage = createMockStorage(); - const s = store(new MyStore()); + const s = createClassyStore(new MyStore()); persist(s, {name: 'test', storage}); s.increment(); @@ -157,7 +157,7 @@ describe('persist()', () => { describe('properties option', () => { it('persists only the specified properties', async () => { const storage = createMockStorage(); - const s = store({count: 0, name: 'hello', secret: 'hidden'}); + const s = createClassyStore({count: 0, name: 'hello', secret: 'hidden'}); persist(s, {name: 'test', storage, properties: ['count', 'name']}); s.count = 10; @@ -180,7 +180,11 @@ describe('persist()', () => { }), ); - const s = store({count: 0, name: 'initial', secret: 'original'}); + const s = createClassyStore({ + count: 0, + name: 'initial', + secret: 'original', + }); const handle = persist(s, { name: 'test', storage, @@ -204,7 +208,7 @@ describe('persist()', () => { } const storage = createMockStorage(); - const s = store(new SessionStore()); + const s = createClassyStore(new SessionStore()); // Save persist(s, { @@ -229,7 +233,7 @@ describe('persist()', () => { expect(stored?.state.expiresAt).toBe('2026-06-15T12:00:00.000Z'); // Restore - const s2 = store(new SessionStore()); + const s2 = createClassyStore(new SessionStore()); const handle = persist(s2, { name: 'session', storage, @@ -255,7 +259,7 @@ describe('persist()', () => { } const storage = createMockStorage(); - const s = store(new UserStore()); + const s = createClassyStore(new UserStore()); persist(s, { name: 'users', @@ -283,7 +287,7 @@ describe('persist()', () => { ]); // Restore - const s2 = store(new UserStore()); + const s2 = createClassyStore(new UserStore()); const handle = persist(s2, { name: 'users', storage, @@ -314,7 +318,7 @@ describe('persist()', () => { const setItemSpy = mock(storage.setItem.bind(storage)); storage.setItem = setItemSpy; - const s = store({count: 0}); + const s = createClassyStore({count: 0}); persist(s, {name: 'test', storage, debounce: 50}); s.count = 1; @@ -337,7 +341,7 @@ describe('persist()', () => { it('save() bypasses debounce and writes immediately', async () => { const storage = createMockStorage(); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage, debounce: 5000}); s.count = 42; @@ -363,7 +367,9 @@ describe('persist()', () => { JSON.stringify({version: 0, state: {items: ['buy milk']}}), ); - const s = store({todos: [] as {text: string; done: boolean}[]}); + const s = createClassyStore({ + todos: [] as {text: string; done: boolean}[], + }); const handle = persist(s, { name: 'test', storage, @@ -390,7 +396,7 @@ describe('persist()', () => { const storage = createMockStorage(); storage.data.set('test', JSON.stringify({version: 1, state: {count: 5}})); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, { name: 'test', storage, @@ -414,7 +420,11 @@ describe('persist()', () => { JSON.stringify({version: 0, state: {theme: 'dark'}}), ); - const s = store({theme: 'light', fontSize: 14, sidebar: true}); + const s = createClassyStore({ + theme: 'light', + fontSize: 14, + sidebar: true, + }); const handle = persist(s, {name: 'test', storage}); await handle.hydrated; @@ -430,7 +440,7 @@ describe('persist()', () => { JSON.stringify({version: 0, state: {theme: 'dark'}}), ); - const s = store({theme: 'light', fontSize: 14}); + const s = createClassyStore({theme: 'light', fontSize: 14}); const handle = persist(s, {name: 'test', storage, merge: 'replace'}); await handle.hydrated; @@ -445,7 +455,7 @@ describe('persist()', () => { JSON.stringify({version: 0, state: {count: 10, extra: 'from-storage'}}), ); - const s = store({count: 0, name: 'default'}); + const s = createClassyStore({count: 0, name: 'default'}); const handle = persist(s, { name: 'test', storage, @@ -471,7 +481,7 @@ describe('persist()', () => { JSON.stringify({version: 0, state: {count: 99}}), ); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, { name: 'test', storage, @@ -491,7 +501,7 @@ describe('persist()', () => { JSON.stringify({version: 0, state: {count: 99}}), ); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, { name: 'test', storage, @@ -510,7 +520,7 @@ describe('persist()', () => { JSON.stringify({version: 0, state: {count: 42}}), ); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, { name: 'test', storage, @@ -529,7 +539,7 @@ describe('persist()', () => { describe('unsubscribe', () => { it('stops writing to storage after unsubscribe()', async () => { const storage = createMockStorage(); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage}); s.count = 1; @@ -547,7 +557,7 @@ describe('persist()', () => { it('cancels pending debounce on unsubscribe()', async () => { const storage = createMockStorage(); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage, debounce: 100}); s.count = 42; @@ -566,7 +576,7 @@ describe('persist()', () => { describe('save / clear / rehydrate', () => { it('save() writes immediately', async () => { const storage = createMockStorage(); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage}); s.count = 42; @@ -578,7 +588,7 @@ describe('persist()', () => { it('clear() removes the stored data', async () => { const storage = createMockStorage(); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage}); s.count = 5; @@ -591,7 +601,7 @@ describe('persist()', () => { it('clear() does not affect in-memory state', async () => { const storage = createMockStorage(); - const s = store({count: 42}); + const s = createClassyStore({count: 42}); const handle = persist(s, {name: 'test', storage}); s.count = 42; @@ -603,7 +613,7 @@ describe('persist()', () => { it('rehydrate() re-reads from storage', async () => { const storage = createMockStorage(); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage}); await handle.hydrated; @@ -623,7 +633,7 @@ describe('persist()', () => { describe('hydration state', () => { it('isHydrated is false before hydration completes', () => { const storage = createMockStorage(); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage}); // Sync storage hydrates quickly, but the promise is async. @@ -636,7 +646,7 @@ describe('persist()', () => { const storage = createMockStorage(); storage.data.set('test', JSON.stringify({version: 0, state: {count: 5}})); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage}); await handle.hydrated; @@ -650,7 +660,7 @@ describe('persist()', () => { describe('async storage', () => { it('works with an async storage adapter', async () => { const storage = createAsyncMockStorage(); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); persist(s, {name: 'test', storage, syncTabs: false}); s.count = 42; @@ -669,7 +679,7 @@ describe('persist()', () => { JSON.stringify({version: 0, state: {count: 99}}), ); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage, syncTabs: false}); await handle.hydrated; @@ -682,7 +692,7 @@ describe('persist()', () => { describe('cross-tab sync', () => { it('applies state when a storage event fires for the correct key', async () => { const storage = createMockStorage(); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage, syncTabs: true}); await handle.hydrated; @@ -700,7 +710,7 @@ describe('persist()', () => { it('ignores storage events for other keys', async () => { const storage = createMockStorage(); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage, syncTabs: true}); await handle.hydrated; @@ -717,7 +727,7 @@ describe('persist()', () => { it('stops listening after unsubscribe()', async () => { const storage = createMockStorage(); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage, syncTabs: true}); await handle.hydrated; @@ -734,7 +744,7 @@ describe('persist()', () => { it('does not listen when syncTabs is false', async () => { const storage = createMockStorage(); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage, syncTabs: false}); await handle.hydrated; @@ -764,7 +774,7 @@ describe('persist()', () => { }), ); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage, expireIn: 60_000}); await handle.hydrated; @@ -783,7 +793,7 @@ describe('persist()', () => { }), ); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage, expireIn: 60_000}); await handle.hydrated; @@ -802,7 +812,7 @@ describe('persist()', () => { }), ); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, { name: 'test', storage, @@ -826,7 +836,7 @@ describe('persist()', () => { }), ); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage, expireIn: 60_000}); await handle.hydrated; await tick(); @@ -836,7 +846,7 @@ describe('persist()', () => { it('cross-tab sync rejects expired envelopes', async () => { const storage = createMockStorage(); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, { name: 'test', storage, @@ -868,7 +878,7 @@ describe('persist()', () => { JSON.stringify({version: 0, state: {count: 77}}), ); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage, expireIn: 60_000}); await handle.hydrated; @@ -878,7 +888,7 @@ describe('persist()', () => { it('TTL resets on every write (envelope timestamp refreshes)', async () => { const storage = createMockStorage(); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); persist(s, {name: 'test', storage, expireIn: 30_000}); const before = Date.now(); @@ -914,7 +924,7 @@ describe('persist()', () => { }), ); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage, expireIn: 60_000}); await handle.hydrated; expect(s.count).toBe(42); @@ -941,7 +951,7 @@ describe('persist()', () => { describe('edge cases', () => { it('handles empty/missing storage gracefully', async () => { const storage = createMockStorage(); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage}); await handle.hydrated; @@ -952,7 +962,7 @@ describe('persist()', () => { const storage = createMockStorage(); storage.data.set('test', 'not-valid-json!!!'); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage}); await handle.hydrated; @@ -963,7 +973,7 @@ describe('persist()', () => { const storage = createMockStorage(); storage.data.set('test', JSON.stringify({bad: 'shape'})); - const s = store({count: 0}); + const s = createClassyStore({count: 0}); const handle = persist(s, {name: 'test', storage}); await handle.hydrated; @@ -981,7 +991,7 @@ describe('persist()', () => { configurable: true, }); try { - const s = store({count: 0}); + const s = createClassyStore({count: 0}); expect(() => persist(s, {name: 'test'})).toThrow(/storage adapter/i); } finally { if (desc) { @@ -993,7 +1003,7 @@ describe('persist()', () => { it('multiple persists on the same store with different keys', async () => { const storage1 = createMockStorage(); const storage2 = createMockStorage(); - const s = store({count: 0, name: 'hello'}); + const s = createClassyStore({count: 0, name: 'hello'}); persist(s, { name: 'store-count', diff --git a/src/utils/persist/persist.ts b/src/utils/persist/persist.ts index a9d04db..84a418e 100644 --- a/src/utils/persist/persist.ts +++ b/src/utils/persist/persist.ts @@ -263,7 +263,7 @@ function getDefaultStorage(): StorageAdapter | undefined { * On init (or manual rehydrate), reads from storage and applies the state back * to the store proxy. * - * @param proxyStore - A reactive proxy created by `store()`. + * @param proxyStore - A reactive proxy created by `createClassyStore()`. * @param options - Persistence configuration. * @returns A handle with lifecycle controls (unsubscribe, save, clear, rehydrate, hydrated). */ diff --git a/website/docs/ARCHITECTURE.md b/website/docs/ARCHITECTURE.md index ff36566..639d8c6 100644 --- a/website/docs/ARCHITECTURE.md +++ b/website/docs/ARCHITECTURE.md @@ -13,7 +13,7 @@ flowchart TB end subgraph layer1 ["Layer 1: Write Proxy (core.ts)"] - storeFn["store(instance)"] + storeFn["createClassyStore(instance)"] SetTrap["SET trap: forward write → bump version → schedule notify"] GetTrap["GET trap: return value, bind methods, lazy-wrap nested objects, memoize getters"] DeleteTrap["DELETE trap: same as SET"] @@ -65,12 +65,12 @@ flowchart TB ``` packages/store/ ├── src/ -│ ├── index.ts # Barrel export: store, useStore, snapshot, subscribe, getVersion, shallowEqual, Snapshot, reactiveMap, reactiveSet, ReactiveMap, ReactiveSet +│ ├── index.ts # Barrel export: createClassyStore, useStore, snapshot, subscribe, getVersion, shallowEqual, Snapshot, reactiveMap, reactiveSet, ReactiveMap, ReactiveSet │ ├── collections/ │ │ ├── collections.ts # ReactiveMap and ReactiveSet implementations │ │ └── collections.test.ts # tests: ReactiveMap, ReactiveSet, class store integration │ ├── core/ -│ │ ├── core.ts # Layer 1: Write Proxy — store(), subscribe(), getVersion() +│ │ ├── core.ts # Layer 1: Write Proxy — createClassyStore(), subscribe(), getVersion() │ │ ├── core.test.ts # tests: mutations, batching, methods, getters, arrays │ │ └── computed.test.tsx # tests: write proxy + snapshot memoization, useStore integration │ ├── react/ @@ -108,7 +108,7 @@ packages/store/ ### Overview -The `store()` function wraps a class instance in an ES6 Proxy. All mutations — property writes, array operations, nested object changes — are intercepted and batched into a single notification per microtask. +The `createClassyStore()` function wraps a class instance in an ES6 Proxy. All mutations — property writes, array operations, nested object changes — are intercepted and batched into a single notification per microtask. ### Data Flow: Mutation → Notification @@ -124,7 +124,7 @@ sequenceDiagram User->>Proxy: store.count = 5 Proxy->>Target: Reflect.set(target, 'count', 5) Proxy->>Internal: bumpVersion(internal) up to root - Proxy->>Internal: snapCache = null (invalidate) + Proxy->>Internal: snapshotCache = null (invalidate) Proxy->>Micro: scheduleNotify() (if not already scheduled) Note over Micro: Coalesces all sync mutations User->>Proxy: store.name = 'new' @@ -147,7 +147,7 @@ type StoreInternal = { childInternals: Map; parent: StoreInternal | null; // For version propagation notifyScheduled: boolean; // Batch dedup flag - snapCache: [number, object] | null; // Version-stamped snapshot cache + snapshotCache: [number, object] | null; // Version-stamped snapshot cache computedCache: Map; // Memoized getter cache }; ``` @@ -204,7 +204,7 @@ This enables structural sharing: when the snapshot layer sees that `settings` ha ```mermaid flowchart TD - Start["snapshot(proxy)"] --> CheckCache{"Version match\nin snapCache?"} + Start["snapshot(proxy)"] --> CheckCache{"Version match\nin snapshotCache?"} CheckCache -->|Yes| ReturnCached["Return cached snapshot (O(1))"] CheckCache -->|No| CreateSnap["Create new snapshot object"] CreateSnap --> IterateKeys["For each own key"] @@ -218,13 +218,13 @@ flowchart TD CopyValue --> AssignProp AssignProp --> IterateKeys IterateKeys -->|Done| Freeze["Object.freeze(snapshot)"] - Freeze --> Cache["Cache: snapCache.set(target, [version, snap])"] + Freeze --> Cache["Cache: snapshotCache.set(target, [version, snap])"] Cache --> Return["Return frozen snapshot"] ``` ### Two Cache Strategies -1. **Tracked sub-trees** (`snapCache` — `WeakMap`): +1. **Tracked sub-trees** (`snapshotCache` — `WeakMap`): Objects that have been accessed through the proxy have a `StoreInternal` with version tracking. The snapshot recurses into them and uses version-stamped caching for structural sharing. 2. **Untracked sub-trees** (`untrackedCache` — `WeakMap`): @@ -256,7 +256,7 @@ class Store { get doubled() { return this.count * 2; } } -const snap = snapshot(store(new Store())); +const snap = snapshot(createClassyStore(new Store())); snap.doubled; // 10 — computed once, cached snap.doubled; // 10 — same reference (per-snapshot cache hit) ``` diff --git a/website/docs/PERSIST_ARCHITECTURE.md b/website/docs/PERSIST_ARCHITECTURE.md index 0b9ed76..08167e9 100644 --- a/website/docs/PERSIST_ARCHITECTURE.md +++ b/website/docs/PERSIST_ARCHITECTURE.md @@ -265,7 +265,7 @@ graph LR ### Why a function, not a decorator or middleware? -Decorators require class modifications and don't compose well with the existing `store()` wrapping. Middleware requires an interception layer that doesn't exist in the architecture (the proxy IS the middleware). A standalone function that takes a proxy and returns a handle is the simplest, most composable API. +Decorators require class modifications and don't compose well with the existing `createClassyStore()` wrapping. Middleware requires an interception layer that doesn't exist in the architecture (the proxy IS the middleware). A standalone function that takes a proxy and returns a handle is the simplest, most composable API. ### Why per-property transforms instead of global serialize/deserialize? diff --git a/website/docs/PERSIST_TUTORIAL.md b/website/docs/PERSIST_TUTORIAL.md index 927148b..0b961cf 100644 --- a/website/docs/PERSIST_TUTORIAL.md +++ b/website/docs/PERSIST_TUTORIAL.md @@ -1,13 +1,13 @@ # Persist Tutorial -`persist()` saves your store's state to storage and restores it on page load. It's a standalone utility function that works with any `store()` instance -- no plugins, no middleware, no configuration files. Import it from `@codebelt/classy-store/utils`, call it once, and your store survives page refreshes. +`persist()` saves your store's state to storage and restores it on page load. It's a standalone utility function that works with any `createClassyStore()` instance -- no plugins, no middleware, no configuration files. Import it from `@codebelt/classy-store/utils`, call it once, and your store survives page refreshes. ## Getting Started ### 1. Create a store ```ts -import { store } from '@codebelt/classy-store'; +import { createClassyStore } from '@codebelt/classy-store'; class TodoStore { todos: { text: string; done: boolean }[] = []; @@ -26,7 +26,7 @@ class TodoStore { } } -export const todoStore = store(new TodoStore()); +export const todoStore = createClassyStore(new TodoStore()); ``` ### 2. Persist it @@ -91,7 +91,7 @@ class SessionStore { } } -const sessionStore = store(new SessionStore()); +const sessionStore = createClassyStore(new SessionStore()); persist(sessionStore, { name: 'session', @@ -125,7 +125,7 @@ On restore, `deserialize` converts the ISO string back into a `Date` object befo `reactiveMap()` instances are backed by internal arrays that aren't directly JSON-serializable: ```ts -import { store, reactiveMap } from '@codebelt/classy-store'; +import { createClassyStore, reactiveMap } from '@codebelt/classy-store'; import { persist } from '@codebelt/classy-store/utils'; class UserStore { @@ -136,7 +136,7 @@ class UserStore { } } -const userStore = store(new UserStore()); +const userStore = createClassyStore(new UserStore()); persist(userStore, { name: 'user-store', @@ -250,7 +250,7 @@ class SettingsStore { language = 'en'; // NEW -- not in old persisted data } -persist(store(new SettingsStore()), { +persist(createClassyStore(new SettingsStore()), { name: 'settings', // merge: 'shallow', // default }); @@ -491,7 +491,7 @@ async function resetToDefaults() { Call `persist()` multiple times with different options to persist different parts of a store to different locations: ```ts -const todoStore = store(new TodoStore()); +const todoStore = createClassyStore(new TodoStore()); // Persist todos to localStorage (survives page close) persist(todoStore, { @@ -512,7 +512,7 @@ persist(todoStore, { Here's a complete example combining several features: ```ts -import { store, reactiveMap } from '@codebelt/classy-store'; +import { createClassyStore, reactiveMap } from '@codebelt/classy-store'; import { persist } from '@codebelt/classy-store/utils'; class AppStore { @@ -534,7 +534,7 @@ class AppStore { } } -const appStore = store(new AppStore()); +const appStore = createClassyStore(new AppStore()); const handle = persist(appStore, { name: 'app-store', diff --git a/website/docs/TUTORIAL.md b/website/docs/TUTORIAL.md index 54a065e..ddba289 100644 --- a/website/docs/TUTORIAL.md +++ b/website/docs/TUTORIAL.md @@ -1,6 +1,6 @@ # Classy Store Tutorial -`Classy Store` is a reactive state library for React built on ES proxies. You define state as plain classes, wrap them with `store()`, and read them with `useStore()`. There are no Providers, no observers, no reducers, and no extra TypeScript interfaces to define your state shape — just classes and a hook. The library is ~3.5 KB gzipped, batches synchronous mutations into a single re-render, and uses structural sharing for efficient change detection. +`Classy Store` is a reactive state library for React built on ES proxies. You define state as plain classes, wrap them with `createClassyStore()`, and read them with `useStore()`. There are no Providers, no observers, no reducers, and no extra TypeScript interfaces to define your state shape — just classes and a hook. The library is ~3.5 KB gzipped, batches synchronous mutations into a single re-render, and uses structural sharing for efficient change detection. ## Quick Start @@ -8,7 +8,7 @@ ```ts // stores.ts -import { store } from '@codebelt/classy-store'; +import { createClassyStore } from '@codebelt/classy-store'; class Counter { count = 0; @@ -18,14 +18,14 @@ class Counter { } } -export const counterStore = store(new Counter()); +export const counterStore = createClassyStore(new Counter()); ``` **2. Use it in a component:** ```tsx // Counter.tsx -import { useStore } from '@codebelt/classy-store'; +import { useStore } from '@codebelt/classy-store/react'; import { counterStore } from './stores'; export function Counter() { @@ -38,10 +38,10 @@ That's it. No Provider wrapping your app. The store is a module-level singleton ## Defining Stores -A store is any class instance wrapped with `store()`. State lives as properties, mutations are plain assignments, and computed values are `get` accessors. +A store is any class instance wrapped with `createClassyStore()`. State lives as properties, mutations are plain assignments, and computed values are `get` accessors. ```ts -import { store } from '@codebelt/classy-store'; +import { createClassyStore } from '@codebelt/classy-store'; class TodoStore { items: { id: number; text: string; done: boolean }[] = []; @@ -71,7 +71,7 @@ class TodoStore { } } -export const todoStore = store(new TodoStore()); +export const todoStore = createClassyStore(new TodoStore()); ``` Key points: @@ -122,7 +122,7 @@ The problem shows up when your selector **derives a new value** — calling `.fi Without `shallowEqual` — `.filter()` creates a new array every time the snapshot updates, even if the todo items haven't changed: ```ts -import { useStore } from '@codebelt/classy-store'; +import { useStore } from '@codebelt/classy-store/react'; // ❌ New array reference every snapshot → unnecessary re-renders const active = useStore(todoStore, (s) => s.items.filter((i) => !i.done)); @@ -131,7 +131,8 @@ const active = useStore(todoStore, (s) => s.items.filter((i) => !i.done)); With `shallowEqual` — compares the array contents, not the reference: ```ts -import { useStore, shallowEqual } from '@codebelt/classy-store'; +import { shallowEqual } from '@codebelt/classy-store'; +import { useStore } from '@codebelt/classy-store/react'; // ✅ Only re-renders when the filtered items actually change const active = useStore( @@ -238,7 +239,7 @@ class DocStore { ]; } -const docStore = store(new DocStore()); +const docStore = createClassyStore(new DocStore()); // Deep mutation — triggers a notification docStore.metadata.title = 'My Document'; @@ -256,7 +257,7 @@ Structural sharing means that when you mutate `metadata.title`, the snapshot for Native `Map` and `Set` aren't plain objects — the proxy can't intercept their internal methods. Use `reactiveMap()` and `reactiveSet()` instead. ```ts -import { store, reactiveMap } from '@codebelt/classy-store'; +import { createClassyStore, reactiveMap } from '@codebelt/classy-store'; class UserStore { users = reactiveMap(); @@ -274,7 +275,7 @@ class UserStore { } } -export const userStore = store(new UserStore()); +export const userStore = createClassyStore(new UserStore()); ``` `ReactiveMap` and `ReactiveSet` mirror the native API (`get`, `set`, `has`, `delete`, `clear`, `forEach`, iteration) but are backed by plain arrays so the proxy can track mutations. @@ -286,7 +287,7 @@ export const userStore = store(new UserStore()); Subclasses work out of the box — no special API or configuration needed. Methods, getters, and properties from all inheritance levels are fully reactive. ```ts -import { store } from '@codebelt/classy-store'; +import { createClassyStore } from '@codebelt/classy-store'; class BaseStore { loading = false; @@ -326,7 +327,7 @@ class UserStore extends BaseStore { } } -export const userStore = store(new UserStore()); +export const userStore = createClassyStore(new UserStore()); ``` How it works: diff --git a/website/docs/index.md b/website/docs/index.md index b7668d7..2b5fd0a 100644 --- a/website/docs/index.md +++ b/website/docs/index.md @@ -61,15 +61,15 @@ class TodoStore { ### 2. Create a reactive store ```typescript -import { store } from '@codebelt/classy-store'; +import { createClassyStore } from '@codebelt/classy-store'; -const todoStore = store(new TodoStore()); +const todoStore = createClassyStore(new TodoStore()); ``` ### 3. Use in React components ```tsx -import { useStore } from '@codebelt/classy-store'; +import { useStore } from '@codebelt/classy-store/react'; // Selector mode: explicit control over what triggers re-renders function TodoCount() { @@ -97,12 +97,12 @@ function AddButton() { ## API Reference -### `store(instance)` +### `createClassyStore(instance)` Wraps a class instance in a reactive Proxy. Mutations are intercepted, batched via `queueMicrotask`, and subscribers are notified. ```typescript -const myStore = store(new MyClass()); +const myStore = createClassyStore(new MyClass()); ``` - **Methods** are automatically bound so `this` mutations go through the proxy @@ -136,7 +136,8 @@ Returns a tracking proxy. Properties your component reads are automatically trac **Custom equality:** ```typescript -import { shallowEqual, useStore } from '@codebelt/classy-store'; +import { shallowEqual } from '@codebelt/classy-store'; +import { useStore } from '@codebelt/classy-store/react'; const userData = useStore(myStore, s => ({ name: s.user.name, @@ -190,10 +191,11 @@ Shallow equality comparison for objects and arrays. Useful as a custom `isEqual` ### `reactiveMap(initial?)` -Creates a reactive Map-like collection backed by a plain array. Use inside a `store()` for full reactivity. +Creates a reactive Map-like collection backed by a plain array. Use inside a `createClassyStore()` for full reactivity. ```typescript -import { reactiveMap, store, useStore } from '@codebelt/classy-store'; +import { reactiveMap, createClassyStore } from '@codebelt/classy-store'; +import { useStore } from '@codebelt/classy-store/react'; class UserStore { users = reactiveMap(); @@ -207,7 +209,7 @@ class UserStore { } } -const userStore = store(new UserStore()); +const userStore = createClassyStore(new UserStore()); function UserList() { const snap = useStore(userStore, s => [...s.users.entries()]); @@ -225,10 +227,11 @@ Supports: `.get()`, `.set()`, `.has()`, `.delete()`, `.clear()`, `.size`, `.keys ### `reactiveSet(initial?)` -Creates a reactive Set-like collection backed by a plain array. Use inside a `store()` for full reactivity. +Creates a reactive Set-like collection backed by a plain array. Use inside a `createClassyStore()` for full reactivity. ```typescript -import { reactiveSet, store, useStore } from '@codebelt/classy-store'; +import { reactiveSet, createClassyStore } from '@codebelt/classy-store'; +import { useStore } from '@codebelt/classy-store/react'; class TagStore { tags = reactiveSet(); @@ -238,7 +241,7 @@ class TagStore { } } -const tagStore = store(new TagStore()); +const tagStore = createClassyStore(new TagStore()); function TagList() { const tags = useStore(tagStore, s => [...s.tags]); @@ -274,7 +277,7 @@ import { persist } from '@codebelt/classy-store/utils'; Persist store state to `localStorage`, `sessionStorage`, `AsyncStorage`, or any custom storage adapter. Subscribes to store mutations, serializes selected properties into a versioned JSON envelope, and writes to storage. On init (or manual rehydrate), reads from storage and applies the state back. ```typescript -import { store } from '@codebelt/classy-store'; +import { createClassyStore } from '@codebelt/classy-store'; import { persist } from '@codebelt/classy-store/utils'; class TodoStore { @@ -290,7 +293,7 @@ class TodoStore { } } -const todoStore = store(new TodoStore()); +const todoStore = createClassyStore(new TodoStore()); // Persist all data properties to localStorage. // Getters (remaining) and methods (addTodo) are automatically excluded. @@ -366,8 +369,8 @@ useEffect(() => { handle.rehydrate(); }, []); ### Multiple stores ```typescript -const authStore = store(new AuthStore()); -const uiStore = store(new UiStore()); +const authStore = createClassyStore(new AuthStore()); +const uiStore = createClassyStore(new UiStore()); function Header() { const user = useStore(authStore, s => s.currentUser); @@ -400,7 +403,7 @@ class UserStore extends BaseStore { get count() { return this.users.length; } } -const userStore = store(new UserStore()); +const userStore = createClassyStore(new UserStore()); // Base methods, derived methods, base getters, derived getters — all reactive. // snapshot(userStore) instanceof UserStore === true @@ -425,7 +428,7 @@ class SettingsStore { } } -const settingsStore = store(new SettingsStore()); +const settingsStore = createClassyStore(new SettingsStore()); // Only re-renders when push notification setting changes function PushToggle() { From 493abdd2c66ef293f6e26e3bc6fb13e24ea3ba5a Mon Sep 17 00:00:00 2001 From: codeBelt Date: Mon, 16 Feb 2026 09:32:44 -0600 Subject: [PATCH 2/3] refactor: replace `store` with `createClassyStore` across the codebase --- .changeset/tiny-knives-drop.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tiny-knives-drop.md diff --git a/.changeset/tiny-knives-drop.md b/.changeset/tiny-knives-drop.md new file mode 100644 index 0000000..5c12f13 --- /dev/null +++ b/.changeset/tiny-knives-drop.md @@ -0,0 +1,5 @@ +--- +"@codebelt/classy-store": minor +--- + +replace `store` with `createClassyStore` across the codebase From d59867fca3bbe15ef6a925cdd7cbe1896724d1f6 Mon Sep 17 00:00:00 2001 From: codeBelt Date: Mon, 16 Feb 2026 09:39:45 -0600 Subject: [PATCH 3/3] refactor: standardize selector function parameter naming for `useStore` across codebase --- examples/rendering/src/AsyncDemo.tsx | 8 +++--- examples/rendering/src/CollectionsDemo.tsx | 4 +-- .../src/ReactiveFundamentalsDemo.tsx | 6 ++--- .../rendering/src/StructuralSharingDemo.tsx | 24 ++++++++++++----- examples/rendering/src/stores.ts | 2 +- website/docs/ARCHITECTURE.md | 4 +-- website/docs/TUTORIAL.md | 22 ++++++++-------- website/docs/index.md | 26 +++++++++---------- website/src/pages/index.tsx | 2 +- 9 files changed, 55 insertions(+), 43 deletions(-) diff --git a/examples/rendering/src/AsyncDemo.tsx b/examples/rendering/src/AsyncDemo.tsx index e2b9076..9f27057 100644 --- a/examples/rendering/src/AsyncDemo.tsx +++ b/examples/rendering/src/AsyncDemo.tsx @@ -5,7 +5,7 @@ import {postStore} from './stores'; import {useRenderCount} from './useRenderCount'; function LoadingIndicator() { - const loading = useStore(postStore, (s) => s.loading); + const loading = useStore(postStore, (store) => store.loading); const renders = useRenderCount(); return (
@@ -51,7 +51,7 @@ function LoadingIndicator() { } function ErrorDisplay() { - const error = useStore(postStore, (s) => s.error); + const error = useStore(postStore, (store) => store.error); const renders = useRenderCount(); return (
@@ -71,7 +71,7 @@ function ErrorDisplay() { } function PostList() { - const posts = useStore(postStore, (s) => s.posts); + const posts = useStore(postStore, (store) => store.posts); const renders = useRenderCount(); return (
@@ -102,7 +102,7 @@ function PostList() { } function PostCount() { - const count = useStore(postStore, (s) => s.count); + const count = useStore(postStore, (store) => store.count); const renders = useRenderCount(); return (
diff --git a/examples/rendering/src/CollectionsDemo.tsx b/examples/rendering/src/CollectionsDemo.tsx index 232368e..86f20f5 100644 --- a/examples/rendering/src/CollectionsDemo.tsx +++ b/examples/rendering/src/CollectionsDemo.tsx @@ -107,8 +107,8 @@ function TagList() { } function CountDisplay() { - const userCount = useStore(collectionStore, (s) => s.userCount); - const tagCount = useStore(collectionStore, (s) => s.tagCount); + const userCount = useStore(collectionStore, (store) => store.userCount); + const tagCount = useStore(collectionStore, (store) => store.tagCount); const renders = useRenderCount(); return ( diff --git a/examples/rendering/src/ReactiveFundamentalsDemo.tsx b/examples/rendering/src/ReactiveFundamentalsDemo.tsx index 4a5aa5f..c178972 100644 --- a/examples/rendering/src/ReactiveFundamentalsDemo.tsx +++ b/examples/rendering/src/ReactiveFundamentalsDemo.tsx @@ -12,7 +12,7 @@ const names = ['World', 'Bun', 'React', 'Store']; let nameIndex = 0; function CountCard() { - const count = useStore(counterStore, (s) => s.count); + const count = useStore(counterStore, (store) => store.count); const renders = useRenderCount(); return (
@@ -30,7 +30,7 @@ function CountCard() { } function NameCard() { - const name = useStore(counterStore, (s) => s.name); + const name = useStore(counterStore, (store) => store.name); const renders = useRenderCount(); return (
@@ -48,7 +48,7 @@ function NameCard() { } function BatchCard() { - const count = useStore(batchStore, (s) => s.count); + const count = useStore(batchStore, (store) => store.count); const renders = useRenderCount(); return (
diff --git a/examples/rendering/src/StructuralSharingDemo.tsx b/examples/rendering/src/StructuralSharingDemo.tsx index 1d282d5..9bd986f 100644 --- a/examples/rendering/src/StructuralSharingDemo.tsx +++ b/examples/rendering/src/StructuralSharingDemo.tsx @@ -85,21 +85,33 @@ function SnapshotTree() {
Snapshot Reference Tree
- s}> - s.metadata} depth={1} /> - s.content} depth={1}> + store}> + store.metadata} + depth={1} + /> + store.content} + depth={1} + > s.content.sections[0]} + getValue={(store) => store.content.sections[0]} depth={2} /> s.content.sections[1]} + getValue={(store) => store.content.sections[1]} depth={2} /> - s.settings} depth={1} /> + store.settings} + depth={1} + />
); diff --git a/examples/rendering/src/stores.ts b/examples/rendering/src/stores.ts index ad440e0..a83f1c0 100644 --- a/examples/rendering/src/stores.ts +++ b/examples/rendering/src/stores.ts @@ -88,7 +88,7 @@ export class DocumentStore { } updateSectionBody(id: number, body: string) { - const section = this.content.sections.find((s) => s.id === id); + const section = this.content.sections.find((store) => store.id === id); if (section) section.body = body; } diff --git a/website/docs/ARCHITECTURE.md b/website/docs/ARCHITECTURE.md index 639d8c6..7b0e155 100644 --- a/website/docs/ARCHITECTURE.md +++ b/website/docs/ARCHITECTURE.md @@ -319,7 +319,7 @@ flowchart TD 4. For getters reading other getters (e.g., `get filteredCount() { return this.filtered.length; }`): the inner getter is itself memoized, so it returns a stable reference, which the outer getter's dep check recognizes as unchanged. **Impact on `useStore`:** The existing hook benefits automatically — no changes needed: -- **Selector mode:** `useStore(store, s => s.filtered)` gets a stable reference from the memoized snapshot getter. `Object.is` correctly detects "no change" without `shallowEqual`. +- **Selector mode:** `useStore(store, (store) => store.filtered)` gets a stable reference from the memoized snapshot getter. `Object.is` correctly detects "no change" without `shallowEqual`. - **Auto-tracked mode:** `proxy-compare`'s `isChanged` gets stable references from snapshot getters, reducing false positives. ## Layer 3: React Hook (`useStore.ts`) @@ -331,7 +331,7 @@ flowchart TD ### Mode 1: Selector ```typescript -const count = useStore(myStore, s => s.count); +const count = useStore(myStore, (store) => store.count); ``` ```mermaid diff --git a/website/docs/TUTORIAL.md b/website/docs/TUTORIAL.md index ddba289..633dacb 100644 --- a/website/docs/TUTORIAL.md +++ b/website/docs/TUTORIAL.md @@ -29,7 +29,7 @@ import { useStore } from '@codebelt/classy-store/react'; import { counterStore } from './stores'; export function Counter() { - const count = useStore(counterStore, (s) => s.count); + const count = useStore(counterStore, (store) => store.count); return ; } ``` @@ -87,7 +87,7 @@ Key points: ### Selector mode ```ts -const count = useStore(counterStore, (s) => s.count); +const count = useStore(counterStore, (store) => store.count); ``` The selector receives an immutable snapshot and returns the slice you need. The component re-renders only when the selected value changes (compared with `Object.is`). @@ -111,8 +111,8 @@ By default, `useStore` compares the selector's return value with `Object.is`, wh ```ts // ✅ No shallowEqual needed — structural sharing keeps the reference stable -const count = useStore(todoStore, (s) => s.items.length); -const todos = useStore(todoStore, (s) => s.items); +const count = useStore(todoStore, (store) => store.items.length); +const todos = useStore(todoStore, (store) => store.items); ``` The problem shows up when your selector **derives a new value** — calling `.filter()`, `.map()`, or using object spread always allocates a new array or object, even when the underlying data hasn't changed. `Object.is` sees a different reference and triggers a re-render. @@ -125,7 +125,7 @@ Without `shallowEqual` — `.filter()` creates a new array every time the snapsh import { useStore } from '@codebelt/classy-store/react'; // ❌ New array reference every snapshot → unnecessary re-renders -const active = useStore(todoStore, (s) => s.items.filter((i) => !i.done)); +const active = useStore(todoStore, (store) => store.items.filter((i) => !i.done)); ``` With `shallowEqual` — compares the array contents, not the reference: @@ -137,7 +137,7 @@ import { useStore } from '@codebelt/classy-store/react'; // ✅ Only re-renders when the filtered items actually change const active = useStore( todoStore, - (s) => s.items.filter((i) => !i.done), + (store) => store.items.filter((i) => !i.done), shallowEqual, ); ``` @@ -146,10 +146,10 @@ const active = useStore( ```ts // ✅ No shallowEqual needed — cross-snapshot memoization keeps getter results stable -const filtered = useStore(todoStore, (s) => s.filtered); +const filtered = useStore(todoStore, (store) => store.filtered); // ✅ Primitives from getters also work fine -const remaining = useStore(todoStore, (s) => s.remaining); +const remaining = useStore(todoStore, (store) => store.remaining); ``` `shallowEqual` is only needed when the **selector itself** derives a new value (via `.filter()`, `.map()`, object spread, etc.) — not when selecting a getter that does the derivation internally. @@ -250,7 +250,7 @@ docStore.sections[0].body = 'Updated intro'; While modifying nested properties directly through the proxy works, the same best practice applies here: prefer store methods over inline mutations. When nested updates are scattered across components, it becomes difficult to trace how deeply nested state changes. Methods give you a single place to look. -Structural sharing means that when you mutate `metadata.title`, the snapshot for `sections` is reused from the previous snapshot (same reference). A component selecting `s => s.sections` won't re-render because its selected value hasn't changed. +Structural sharing means that when you mutate `metadata.title`, the snapshot for `sections` is reused from the previous snapshot (same reference). A component selecting `(store) => store.sections` won't re-render because its selected value hasn't changed. ## Collections @@ -365,7 +365,7 @@ class PostStore { } ``` -This means a component using `useStore(postStore, s => s.loading)` will re-render twice: once when loading starts, once when it ends. That's the correct behavior. +This means a component using `useStore(postStore, (store) => store.loading)` will re-render twice: once when loading starts, once when it ends. That's the correct behavior. ## Tips & Gotchas @@ -416,7 +416,7 @@ class Counter { } // ✅ Selector mode: no re-render (Object.is sees same count value) -const count = useStore(counterStore, (s) => s.count); +const count = useStore(counterStore, (store) => store.count); // ⚠️ Auto-tracked mode: re-renders (snapshot reference changed) const snap = useStore(counterStore); diff --git a/website/docs/index.md b/website/docs/index.md index 2b5fd0a..0c735b1 100644 --- a/website/docs/index.md +++ b/website/docs/index.md @@ -73,7 +73,7 @@ import { useStore } from '@codebelt/classy-store/react'; // Selector mode: explicit control over what triggers re-renders function TodoCount() { - const remaining = useStore(todoStore, s => s.remaining); + const remaining = useStore(todoStore, (store) => store.remaining); return {remaining} left; } @@ -116,9 +116,9 @@ React hook that subscribes to store changes via `useSyncExternalStore`. **Selector mode:** ```typescript -const count = useStore(myStore, s => s.count); -const user = useStore(myStore, s => s.user); -const items = useStore(myStore, s => s.items); +const count = useStore(myStore, (store) => store.count); +const user = useStore(myStore, (store) => store.user); +const items = useStore(myStore, (store) => store.items); ``` The selector receives an immutable snapshot. Re-renders only when the selected value changes (via `Object.is` by default, or a custom `isEqual`). @@ -139,9 +139,9 @@ Returns a tracking proxy. Properties your component reads are automatically trac import { shallowEqual } from '@codebelt/classy-store'; import { useStore } from '@codebelt/classy-store/react'; -const userData = useStore(myStore, s => ({ - name: s.user.name, - role: s.user.role, +const userData = useStore(myStore, (store) => ({ + name: store.user.name, + role: store.user.role, }), shallowEqual); ``` @@ -212,7 +212,7 @@ class UserStore { const userStore = createClassyStore(new UserStore()); function UserList() { - const snap = useStore(userStore, s => [...s.users.entries()]); + const snap = useStore(userStore, (store) => [...store.users.entries()]); return (
    {snap.map(([id, user]) => ( @@ -244,7 +244,7 @@ class TagStore { const tagStore = createClassyStore(new TagStore()); function TagList() { - const tags = useStore(tagStore, s => [...s.tags]); + const tags = useStore(tagStore, (store) => [...store.tags]); return tags.map(tag => {tag}); } ``` @@ -373,8 +373,8 @@ const authStore = createClassyStore(new AuthStore()); const uiStore = createClassyStore(new UiStore()); function Header() { - const user = useStore(authStore, s => s.currentUser); - const theme = useStore(uiStore, s => s.theme); + const user = useStore(authStore, (store) => store.currentUser); + const theme = useStore(uiStore, (store) => store.theme); return
    {user?.name}
    ; } ``` @@ -432,7 +432,7 @@ const settingsStore = createClassyStore(new SettingsStore()); // Only re-renders when push notification setting changes function PushToggle() { - const push = useStore(settingsStore, s => s.settings.notifications.push); + const push = useStore(settingsStore, (store) => store.settings.notifications.push); return settingsStore.togglePush()} />; } ``` @@ -488,7 +488,7 @@ class Store { ```typescript // Stable reference across re-renders when items/filter haven't changed. // No shallowEqual required! -const filtered = useStore(myStore, s => s.filtered); +const filtered = useStore(myStore, (store) => store.filtered); ``` ### Working with Date and RegExp diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx index 0fef8bf..46083cc 100644 --- a/website/src/pages/index.tsx +++ b/website/src/pages/index.tsx @@ -21,7 +21,7 @@ const counterStore = store(new CounterStore()); // 3. Use it in React with automatic updates function Counter() { - const count = useStore(counterStore, (s) => s.count); + const count = useStore(counterStore, (store) => store.count); return (