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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/silent-peas-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@codebelt/classy-store": patch
---

restructure internal folder hierarchy and module imports

2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.15/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.0/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
Expand Down
2 changes: 1 addition & 1 deletion examples/rendering/src/AsyncDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {useStore} from '@codebelt/classy-store';
import {useStore} from '@codebelt/classy-store/react';
import {Panel} from './Panel';
import {RenderBadge} from './RenderBadge';
import {postStore} from './stores';
Expand Down
2 changes: 1 addition & 1 deletion examples/rendering/src/CollectionsDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {useStore} from '@codebelt/classy-store';
import {useStore} from '@codebelt/classy-store/react';
import {Panel} from './Panel';
import {RenderBadge} from './RenderBadge';
import {collectionStore} from './stores';
Expand Down
2 changes: 1 addition & 1 deletion examples/rendering/src/KitchenSinkPersistDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {useStore} from '@codebelt/classy-store';
import {useStore} from '@codebelt/classy-store/react';
import {useEffect, useState} from 'react';
import {Panel} from './Panel';
import {kitchenSinkHandle, kitchenSinkStore} from './persistStores';
Expand Down
3 changes: 2 additions & 1 deletion examples/rendering/src/ReactiveFundamentalsDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {store, useStore} from '@codebelt/classy-store';
import {store} from '@codebelt/classy-store';
import {useStore} from '@codebelt/classy-store/react';
import {useState} from 'react';
import {Panel} from './Panel';
import {RenderBadge} from './RenderBadge';
Expand Down
2 changes: 1 addition & 1 deletion examples/rendering/src/SimplePersistDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {useStore} from '@codebelt/classy-store';
import {useStore} from '@codebelt/classy-store/react';
import {useState} from 'react';
import {Panel} from './Panel';
import {preferencesHandle, preferencesStore} from './persistStores';
Expand Down
3 changes: 2 additions & 1 deletion examples/rendering/src/StructuralSharingDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {snapshot, subscribe, useStore} from '@codebelt/classy-store';
import {snapshot, subscribe} from '@codebelt/classy-store';
import {useStore} from '@codebelt/classy-store/react';
import {useEffect, useRef, useState} from 'react';
import {Panel} from './Panel';
import {documentStore} from './stores';
Expand Down
20 changes: 18 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@codebelt/classy-store",
"version": "0.0.1",
"description": "Class-based reactive state management for React — ES6 Proxy + immutable snapshots + useSyncExternalStore",
"description": "Class-based reactive state management — ES6 Proxy + immutable snapshots",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
Expand All @@ -17,6 +17,16 @@
"default": "./dist/index.cjs"
}
},
"./react": {
"import": {
"types": "./dist/react/react.d.mts",
"default": "./dist/react/react.mjs"
},
"require": {
"types": "./dist/react/react.d.cts",
"default": "./dist/react/react.cjs"
}
},
"./utils": {
"import": {
"types": "./dist/utils/index.d.mts",
Expand Down Expand Up @@ -66,6 +76,11 @@
"peerDependencies": {
"react": ">=18.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
},
"devDependencies": {
"@biomejs/biome": "2.4.0",
"@changesets/changelog-github": "0.5.2",
Expand All @@ -90,5 +105,6 @@
"react",
"hooks"
],
"license": "MIT"
"license": "MIT",
"sideEffects": false
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {describe, expect, test} from 'bun:test';
import {reactiveMap, reactiveSet, snapshot, store, subscribe} from './index';
import {store, subscribe} from '../core/core';
import {snapshot} from '../snapshot/snapshot';
import {reactiveMap, reactiveSet} from './collections';

// ── helpers ───────────────────────────────────────────────────────────────────

Expand Down
2 changes: 1 addition & 1 deletion src/collections.ts → src/collections/collections.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {PROXYABLE} from './utils';
import {PROXYABLE} from '../utils/internal/internal';

// ── ReactiveMap ───────────────────────────────────────────────────────────────

Expand Down
4 changes: 2 additions & 2 deletions src/computed.test.tsx → src/core/computed.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {afterEach, describe, expect, it, mock} from 'bun:test';
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 {snapshot} from './snapshot';
import {useStore} from './useStore';

/** Helper: flush the queueMicrotask-based batching. */
const flush = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions src/core.ts → src/core/core.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {DepEntry, StoreInternal} from './types';
import {canProxy, findGetterDescriptor} from './utils';
import type {DepEntry, StoreInternal} from '../types';
import {canProxy, findGetterDescriptor} from '../utils/internal/internal';

// ── Global state ──────────────────────────────────────────────────────────────

Expand Down
23 changes: 8 additions & 15 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
/**
* @codebelt/classy-store -- Class-based reactive state management for React.
* @codebelt/classy-store -- Class-based reactive state management (Core/Vanilla).
*
* Public API:
* - `store(instance)` -- wrap a class instance in a reactive proxy
* - `useStore(store, selector?)` -- React hook (selector or auto-tracked mode)
* - `snapshot(store)` -- create an immutable frozen snapshot
* - `subscribe(store, callback)` -- low-level change subscription
* - `getVersion(store)` -- current version number (for debugging/caching)
* - `shallowEqual(a, b)` -- shallow comparison helper for custom equality
* - `reactiveMap(initial?)` / `reactiveSet(initial?)` -- proxy-compatible collections
* This entry point includes only the core logic.
* For React usage, import from `@codebelt/classy-store/react`.
*
* @module @codebelt/classy-store
*/
export type {ReactiveMap, ReactiveSet} from './collections';
export {reactiveMap, reactiveSet} from './collections';
export {getVersion, store, subscribe} from './core';
export {snapshot} from './snapshot';
export type {ReactiveMap, ReactiveSet} from './collections/collections';
export {reactiveMap, reactiveSet} from './collections/collections';
export {getVersion, store, subscribe} from './core/core';
export {snapshot} from './snapshot/snapshot';
export type {Snapshot} from './types';
export {useStore} from './useStore';
export {shallowEqual} from './utils';
export {shallowEqual} from './utils/equality/equality';
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
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';
import {useStore} from './useStore';
import {shallowEqual} from './utils';
import {store} from '../core/core';
import {shallowEqual} from '../utils/equality/equality';
import {useStore} from './react';

// ── Test harness ────────────────────────────────────────────────────────────

Expand Down
4 changes: 2 additions & 2 deletions src/useStore.test.tsx → src/react/react.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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';
import {useStore} from './useStore';
import {store} from '../core/core';
import {useStore} from './react';

// ── Test harness ────────────────────────────────────────────────────────────

Expand Down
6 changes: 3 additions & 3 deletions src/useStore.ts → src/react/react.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {createProxy, isChanged} from 'proxy-compare';
import {useCallback, useRef, useSyncExternalStore} from 'react';
import {subscribe as coreSubscribe, getInternal} from './core';
import {snapshot} from './snapshot';
import type {Snapshot} from './types';
import {subscribe as coreSubscribe, getInternal} from '../core/core';
import {snapshot} from '../snapshot/snapshot';
import type {Snapshot} from '../types';

// ── Overloads ─────────────────────────────────────────────────────────────────

Expand Down
2 changes: 1 addition & 1 deletion src/snapshot.test.ts → src/snapshot/snapshot.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {describe, expect, it} from 'bun:test';
import {store} from './core';
import {store} from '../core/core';
import {snapshot} from './snapshot';

/** Helper: flush the queueMicrotask-based batching. */
Expand Down
6 changes: 3 additions & 3 deletions src/snapshot.ts → src/snapshot/snapshot.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {getInternal} from './core';
import type {Snapshot, StoreInternal} from './types';
import {canProxy, findGetterDescriptor} from './utils';
import {getInternal} from '../core/core';
import type {Snapshot, StoreInternal} from '../types';
import {canProxy, findGetterDescriptor} from '../utils/internal/internal';

// ── Caches ────────────────────────────────────────────────────────────────────

Expand Down
82 changes: 82 additions & 0 deletions src/utils/equality/equality.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {describe, expect, it} from 'bun:test';
import {shallowEqual} from './equality';

// ── shallowEqual ──────────────────────────────────────────────────────────────

describe('shallowEqual', () => {
// ── Identity / reference ────────────────────────────────────────────────

it('returns true for identical references', () => {
const obj = {a: 1, b: 2};
expect(shallowEqual(obj, obj)).toBe(true);
});

// ── Primitive equality (Object.is) ──────────────────────────────────────

it('returns true for equal primitives', () => {
expect(shallowEqual(1, 1)).toBe(true);
expect(shallowEqual('hello', 'hello')).toBe(true);
expect(shallowEqual(true, true)).toBe(true);
expect(shallowEqual(null, null)).toBe(true);
expect(shallowEqual(undefined, undefined)).toBe(true);
});

it('returns false for different primitives', () => {
expect(shallowEqual(1, 2)).toBe(false);
expect(shallowEqual('a', 'b')).toBe(false);
expect(shallowEqual(true, false)).toBe(false);
});

it('returns false for null vs object', () => {
expect(shallowEqual(null, {a: 1})).toBe(false);
expect(shallowEqual({a: 1}, null)).toBe(false);
});

// ── Shallow object equality ─────────────────────────────────────────────

it('returns true for shallow-equal plain objects', () => {
expect(shallowEqual({a: 1, b: 'x'}, {a: 1, b: 'x'})).toBe(true);
});

it('returns false for objects with different values', () => {
expect(shallowEqual({a: 1}, {a: 2})).toBe(false);
});

it('returns false for objects with different key counts', () => {
expect(shallowEqual({a: 1}, {a: 1, b: 2})).toBe(false);
});

it('returns false for objects with different keys', () => {
expect(shallowEqual({a: 1}, {b: 1})).toBe(false);
});

it('compares nested objects by reference only (not deep)', () => {
const inner1 = {x: 1};
const inner2 = {x: 1}; // same shape but different reference
expect(shallowEqual({a: inner1}, {a: inner2})).toBe(false);
});

// ── Array equality ──────────────────────────────────────────────────────

it('returns true for shallow-equal arrays', () => {
expect(shallowEqual([1, 'a', true], [1, 'a', true])).toBe(true);
});

it('returns false for arrays with different lengths', () => {
expect(shallowEqual([1, 2], [1, 2, 3])).toBe(false);
});

it('returns false for arrays with different elements', () => {
expect(shallowEqual([1, 2, 3], [1, 2, 4])).toBe(false);
});

// ── Object.is edge cases ───────────────────────────────────────────────

it('treats NaN as equal to NaN (Object.is semantics)', () => {
expect(shallowEqual(Number.NaN, Number.NaN)).toBe(true);
});

it('treats +0 and -0 as not equal (Object.is semantics)', () => {
expect(shallowEqual(+0, -0)).toBe(false);
});
});
44 changes: 44 additions & 0 deletions src/utils/equality/equality.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Shallow-equal comparison for objects and arrays.
* Useful as a custom `isEqual` for `useStore` selectors that return objects/arrays.
*
* - Primitives compared with `Object.is`.
* - Arrays: length + element-wise `Object.is`.
* - Objects: key count + value-wise `Object.is`.
*/
export function shallowEqual<T>(a: T, b: T): boolean {
if (Object.is(a, b)) return true;
if (
typeof a !== 'object' ||
a === null ||
typeof b !== 'object' ||
b === null
) {
return false;
}

if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (!Object.is(a[i], b[i])) return false;
}
return true;
}

const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;

for (const key of keysA) {
if (
!Object.hasOwn(b, key) ||
!Object.is(
(a as Record<string, unknown>)[key],
(b as Record<string, unknown>)[key],
)
) {
return false;
}
}
return true;
}
4 changes: 2 additions & 2 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ export type {
PersistOptions,
PropertyTransform,
StorageAdapter,
} from './persist';
export {persist} from './persist';
} from './persist/persist';
export {persist} from './persist/persist';
Loading