@storve/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +151 -0
- package/benchmarks/run.ts +102 -0
- package/benchmarks/week2.md +9 -0
- package/benchmarks/week2.ts +64 -0
- package/benchmarks/week4.md +13 -0
- package/benchmarks/week4.ts +178 -0
- package/benchmarks/week5.md +15 -0
- package/benchmarks/week5.ts +184 -0
- package/coverage/coverage-summary.json +31 -0
- package/dist/adapters/indexedDB.cjs +2 -0
- package/dist/adapters/indexedDB.cjs.map +1 -0
- package/dist/adapters/indexedDB.mjs +2 -0
- package/dist/adapters/indexedDB.mjs.map +1 -0
- package/dist/adapters/localStorage.cjs +2 -0
- package/dist/adapters/localStorage.cjs.map +1 -0
- package/dist/adapters/localStorage.mjs +2 -0
- package/dist/adapters/localStorage.mjs.map +1 -0
- package/dist/adapters/memory.cjs +2 -0
- package/dist/adapters/memory.cjs.map +1 -0
- package/dist/adapters/memory.mjs +2 -0
- package/dist/adapters/memory.mjs.map +1 -0
- package/dist/adapters/sessionStorage.cjs +2 -0
- package/dist/adapters/sessionStorage.cjs.map +1 -0
- package/dist/adapters/sessionStorage.mjs +2 -0
- package/dist/adapters/sessionStorage.mjs.map +1 -0
- package/dist/async-entry.d.ts +7 -0
- package/dist/async-entry.d.ts.map +1 -0
- package/dist/async.cjs +2 -0
- package/dist/async.cjs.map +1 -0
- package/dist/async.d.ts +52 -0
- package/dist/async.d.ts.map +1 -0
- package/dist/async.mjs +2 -0
- package/dist/async.mjs.map +1 -0
- package/dist/batch.d.ts +12 -0
- package/dist/batch.d.ts.map +1 -0
- package/dist/compose.d.ts +7 -0
- package/dist/compose.d.ts.map +1 -0
- package/dist/computed-entry.d.ts +7 -0
- package/dist/computed-entry.d.ts.map +1 -0
- package/dist/computed.cjs +2 -0
- package/dist/computed.cjs.map +1 -0
- package/dist/computed.d.ts +56 -0
- package/dist/computed.d.ts.map +1 -0
- package/dist/computed.mjs +2 -0
- package/dist/computed.mjs.map +1 -0
- package/dist/devtools/history.d.ts +51 -0
- package/dist/devtools/history.d.ts.map +1 -0
- package/dist/devtools/index.d.ts +5 -0
- package/dist/devtools/index.d.ts.map +1 -0
- package/dist/devtools/redux-bridge.d.ts +21 -0
- package/dist/devtools/redux-bridge.d.ts.map +1 -0
- package/dist/devtools/snapshots.d.ts +32 -0
- package/dist/devtools/snapshots.d.ts.map +1 -0
- package/dist/devtools/withDevtools.d.ts +17 -0
- package/dist/devtools/withDevtools.d.ts.map +1 -0
- package/dist/devtools.cjs +2 -0
- package/dist/devtools.cjs.map +1 -0
- package/dist/devtools.mjs +2 -0
- package/dist/devtools.mjs.map +1 -0
- package/dist/extensions/noop.d.ts +2 -0
- package/dist/extensions/noop.d.ts.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.js +118 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +116 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/dist/persist/adapters/indexedDB.d.ts +12 -0
- package/dist/persist/adapters/indexedDB.d.ts.map +1 -0
- package/dist/persist/adapters/localStorage.d.ts +11 -0
- package/dist/persist/adapters/localStorage.d.ts.map +1 -0
- package/dist/persist/adapters/memory.d.ts +11 -0
- package/dist/persist/adapters/memory.d.ts.map +1 -0
- package/dist/persist/adapters/sessionStorage.d.ts +11 -0
- package/dist/persist/adapters/sessionStorage.d.ts.map +1 -0
- package/dist/persist/debounce.d.ts +12 -0
- package/dist/persist/debounce.d.ts.map +1 -0
- package/dist/persist/hydrate.d.ts +15 -0
- package/dist/persist/hydrate.d.ts.map +1 -0
- package/dist/persist/index.d.ts +34 -0
- package/dist/persist/index.d.ts.map +1 -0
- package/dist/persist/serialize.d.ts +28 -0
- package/dist/persist/serialize.d.ts.map +1 -0
- package/dist/persist.cjs +2 -0
- package/dist/persist.cjs.map +1 -0
- package/dist/persist.mjs +2 -0
- package/dist/persist.mjs.map +1 -0
- package/dist/proxy.d.ts +2 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/registry-D3X0HSbl.js +26 -0
- package/dist/registry-D3X0HSbl.js.map +1 -0
- package/dist/registry-RDjbeJdx.js +29 -0
- package/dist/registry-RDjbeJdx.js.map +1 -0
- package/dist/registry-qtr1UpFU.js +2 -0
- package/dist/registry-qtr1UpFU.js.map +1 -0
- package/dist/registry-zaKZ1P-s.js +2 -0
- package/dist/registry-zaKZ1P-s.js.map +1 -0
- package/dist/registry.d.ts +54 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/signals/createSignal.d.ts +19 -0
- package/dist/signals/createSignal.d.ts.map +1 -0
- package/dist/signals/index.d.ts +20 -0
- package/dist/signals/index.d.ts.map +1 -0
- package/dist/signals/useSignal.d.ts +11 -0
- package/dist/signals/useSignal.d.ts.map +1 -0
- package/dist/signals.cjs +2 -0
- package/dist/signals.cjs.map +1 -0
- package/dist/signals.mjs +2 -0
- package/dist/signals.mjs.map +1 -0
- package/dist/stats.html +4949 -0
- package/dist/store.d.ts +12 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/sync/channel.d.ts +7 -0
- package/dist/sync/channel.d.ts.map +1 -0
- package/dist/sync/index.d.ts +3 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/protocol.d.ts +22 -0
- package/dist/sync/protocol.d.ts.map +1 -0
- package/dist/sync/withSync.d.ts +17 -0
- package/dist/sync/withSync.d.ts.map +1 -0
- package/dist/sync.cjs +2 -0
- package/dist/sync.cjs.map +1 -0
- package/dist/sync.mjs +2 -0
- package/dist/sync.mjs.map +1 -0
- package/dist/types.d.ts +134 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +91 -0
- package/rollup.config.mjs +44 -0
- package/src/async-entry.ts +6 -0
- package/src/async.ts +240 -0
- package/src/batch.ts +33 -0
- package/src/compose.ts +50 -0
- package/src/computed-entry.ts +6 -0
- package/src/computed.ts +187 -0
- package/src/devtools/history.ts +103 -0
- package/src/devtools/index.ts +5 -0
- package/src/devtools/redux-bridge.ts +70 -0
- package/src/devtools/snapshots.ts +54 -0
- package/src/devtools/withDevtools.ts +196 -0
- package/src/extensions/noop.ts +12 -0
- package/src/index.ts +4 -0
- package/src/persist/adapters/indexedDB.ts +114 -0
- package/src/persist/adapters/localStorage.ts +28 -0
- package/src/persist/adapters/memory.ts +26 -0
- package/src/persist/adapters/sessionStorage.ts +28 -0
- package/src/persist/debounce.ts +28 -0
- package/src/persist/hydrate.ts +60 -0
- package/src/persist/index.ts +141 -0
- package/src/persist/serialize.ts +60 -0
- package/src/proxy.ts +87 -0
- package/src/registry.ts +67 -0
- package/src/signals/createSignal.ts +81 -0
- package/src/signals/index.ts +20 -0
- package/src/signals/useSignal.ts +18 -0
- package/src/store.ts +250 -0
- package/src/sync/channel.ts +15 -0
- package/src/sync/index.ts +3 -0
- package/src/sync/protocol.ts +18 -0
- package/src/sync/withSync.ts +147 -0
- package/src/types.ts +159 -0
- package/tests/async.test.ts +1100 -0
- package/tests/batch.test.ts +41 -0
- package/tests/compose.test.ts +209 -0
- package/tests/computed.test.ts +867 -0
- package/tests/devtools.test.ts +1039 -0
- package/tests/integration/persist.integration.test.ts +258 -0
- package/tests/integration/signals.integration.test.ts +309 -0
- package/tests/integration.test.ts +278 -0
- package/tests/persist/adapters/indexedDB.adapter.test.ts +185 -0
- package/tests/persist/adapters/localStorage.adapter.test.ts +105 -0
- package/tests/persist/adapters/memory.adapter.test.ts +112 -0
- package/tests/persist/adapters/sessionStorage.adapter.test.ts +128 -0
- package/tests/persist/debounce.test.ts +121 -0
- package/tests/persist/hydrate.test.ts +120 -0
- package/tests/persist/migrate.test.ts +208 -0
- package/tests/persist/persist.test.ts +357 -0
- package/tests/persist/serialize.test.ts +128 -0
- package/tests/proxy.test.ts +473 -0
- package/tests/registry.test.ts +67 -0
- package/tests/signals/derived.test.ts +244 -0
- package/tests/signals/inference.test.ts +108 -0
- package/tests/signals/signal.test.ts +348 -0
- package/tests/signals/useSignal.test.tsx +275 -0
- package/tests/store.test.ts +482 -0
- package/tests/stress.test.ts +268 -0
- package/tests/sync.test.ts +576 -0
- package/tests/types.test.ts +32 -0
- package/tests/v0.3.test.ts +813 -0
- package/tree-shake-test.js +1 -0
- package/tsconfig.json +15 -0
- package/vitest.config.ts +22 -0
- package/vitest_play.ts +7 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [1.0.0] - 2026-03-14
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Getting Started in 5 minutes section in README
|
|
9
|
+
- Migration guides — Redux → Storve, Zustand → Storve
|
|
10
|
+
- StackBlitz interactive demo (Counter, Todo, Async)
|
|
11
|
+
- Bundle size and test count badges
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Roadmap updated to reflect shipped features and planned v1.1–v1.3
|
|
15
|
+
- Test count updated to 937 across 29 test files
|
|
16
|
+
- Coverage badge updated to 99%
|
|
17
|
+
|
|
18
|
+
### Notes
|
|
19
|
+
- Stable API — no breaking changes planned until v2.0
|
|
20
|
+
- All subpath imports locked: storve, storve/async, storve/computed,
|
|
21
|
+
storve/persist, storve/signals, storve/devtools, storve/sync
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## [0.7.0] - 2026-03-14
|
|
30
|
+
|
|
31
|
+
### Added — `storve/sync`
|
|
32
|
+
- `withSync` — cross-tab state synchronization using `BroadcastChannel`.
|
|
33
|
+
- Automatic state rehydration across tabs on initialization.
|
|
34
|
+
- Selective key synchronization via `SyncOptions`.
|
|
35
|
+
- Tree-shakable subpath import: `storve/sync`.
|
|
36
|
+
- Full integration with existing extensions (DevTools, Persist).
|
|
37
|
+
- 100% test coverage for sync logic.
|
|
38
|
+
|
|
39
|
+
## [0.6.0] - 2026-03-14
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
- `storve/devtools` entry point for time-travel debugging.
|
|
43
|
+
- `withDevtools` store enhancer for ring-buffer history and Redux DevTools integration.
|
|
44
|
+
- `undo()` and `redo()` API on stores.
|
|
45
|
+
- `canUndo` and `canRedo` flags.
|
|
46
|
+
- `snapshot()` and `restore()` for named state checkpoints.
|
|
47
|
+
- `useDevtools` React hook for reactive access to devtools state.
|
|
48
|
+
- Redux DevTools extension support (JUMP_TO_STATE, RESET).
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
- Internal: extensions now receive the store instance and original definition.
|
|
52
|
+
- Internal: added `Object.defineProperties` support for extension method merging.
|
|
53
|
+
- Internal: triggered initial state change notification for early extension initialization.
|
|
54
|
+
|
|
55
|
+
## [0.5.0] - 2026-03-14
|
|
56
|
+
|
|
57
|
+
### Added — `storve/signals`
|
|
58
|
+
- `signal(store, key)` — fine-grained reactivity for individual store keys
|
|
59
|
+
- `signal(store, key, transform)` — read-only derived signals with automatic dependency filtering
|
|
60
|
+
- `useSignal(signal)` — React hook for zero-overhead subscription to specific state slices
|
|
61
|
+
- `Object.is` value filtering — zero re-renders unless the specific signal value actually changes
|
|
62
|
+
- 100% test coverage for signals package
|
|
63
|
+
- Tree-shakable subpath import: `storve/signals`
|
|
64
|
+
- Bundle size: < 0.6KB for signals entry point
|
|
65
|
+
|
|
66
|
+
## [0.4.0] - 2026-03-12
|
|
67
|
+
|
|
68
|
+
### Added — `storve/persist`
|
|
69
|
+
- `withPersist` — persist store state with pluggable adapters
|
|
70
|
+
- `localStorageAdapter`, `sessionStorageAdapter`, `memoryAdapter`, `indexedDBAdapter`.
|
|
71
|
+
- `compose()` — compose multiple store enhancers cleanly.
|
|
72
|
+
- Version + migration support for schema changes.
|
|
73
|
+
- Configurable debounce on writes.
|
|
74
|
+
- SSR safe — all adapters guard against missing window/indexedDB.
|
|
75
|
+
|
|
76
|
+
### Added — `storve/async`
|
|
77
|
+
- `createAsync(fetcher, options)` — core engine for async state management.
|
|
78
|
+
- Lifecycle management — automatic tracking of `idle`, `loading`, `success`, and `error` states.
|
|
79
|
+
- Advanced caching — TTL (Time-To-Live) and SWR (Stale-While-Revalidate) support.
|
|
80
|
+
- Optimistic updates — immediate UI feedback with automatic rollback on failure.
|
|
81
|
+
- Argument-aware fetching — separate cache entries based on fetcher arguments.
|
|
82
|
+
|
|
83
|
+
### Changed — BREAKING
|
|
84
|
+
- **Barrel import removed.** Use subpath imports:
|
|
85
|
+
- `import { createStore, batch } from 'storve'` — core only (~1.4KB gzipped)
|
|
86
|
+
- `import { createAsync } from 'storve/async'` — async support (~1.1KB gzipped)
|
|
87
|
+
- `import { computed } from 'storve/computed'` — computed support (~0.8KB gzipped)
|
|
88
|
+
- Store without async extension: `fetch()` throws with message to import `storve/async`
|
|
89
|
+
|
|
90
|
+
### Fixed
|
|
91
|
+
- Bundle sizes: core 1.4KB, async 1.1KB, computed 0.8KB (gzipped, with Terser).
|
|
92
|
+
- ESLint: Replaced `@ts-ignore` with `@ts-expect-error` across benchmarks.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## [0.3.0] - 2026-03-01
|
|
97
|
+
### Added — `storve-core`
|
|
98
|
+
- `actions` support inside `createStore()` — define methods inline, auto-bound, excluded from state
|
|
99
|
+
- Async action support — actions can be `async` and notify subscribers on completion
|
|
100
|
+
- Immer integration — `setState(draft => { ... })` mutation style via `{ immer: true }` store option
|
|
101
|
+
- `store.batch(fn)` — multiple `setState()` calls inside a batch fire exactly one subscriber notification
|
|
102
|
+
- Nested batch support via internal `batchCount` counter
|
|
103
|
+
- `batchDirty` flag — empty batch fires zero notifications
|
|
104
|
+
- `store.actions` stable reference — same object across renders, safe to spread in hooks
|
|
105
|
+
- Added types: `StoreState<D>`, `StoreActions<D>`, `StoreOptions`
|
|
106
|
+
- Updated `Store<D>` interface — `getState()` returns state only, actions excluded from state type
|
|
107
|
+
- Added `immer` to `peerDependencies` — not bundled, tree-shaken when unused
|
|
108
|
+
- 85+ new tests across `actions.test.ts`, `immer.test.ts`, `batch.test.ts`
|
|
109
|
+
- Week 4 performance benchmarks: action calls, Immer mutations, batch notification overhead
|
|
110
|
+
|
|
111
|
+
### Fixed — `storve-react`
|
|
112
|
+
- `useStore` regression — hook was merging actions into result even when a selector was provided, causing React to throw "Objects are not valid as a React child" for primitive selector returns
|
|
113
|
+
- Moved selector logic into `getSnapshot` so React correctly detects value changes
|
|
114
|
+
- Selector path now returns selected value directly — no action merging
|
|
115
|
+
- No-selector path merges actions at return time only, after `useSyncExternalStore`
|
|
116
|
+
- Shallow copy in no-selector snapshot fixes Proxy same-reference issue
|
|
117
|
+
- `shallowEqual` on object selector results prevents unnecessary re-renders
|
|
118
|
+
- Store reference change correctly invalidates snapshot cache
|
|
119
|
+
- Confirmed zero hook rule violations — no conditional hook calls
|
|
120
|
+
|
|
121
|
+
### Fixed — `storve-react` (ESLint)
|
|
122
|
+
- Removed unused `useState` import in `lifecycle.test.tsx`
|
|
123
|
+
- Replaced `any` with typed alternatives across `selector.test.tsx` and `useStore.test.tsx`
|
|
124
|
+
- Fixed `no-unused-vars` in `benchmarks/week3.ts` — replaced `const _ = ...` with `void`
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## [0.2.0] - 2026-02-28
|
|
129
|
+
### Added — `storve-react`
|
|
130
|
+
- `useStore()` hook using `useSyncExternalStore` (React 18)
|
|
131
|
+
- Selector support as optional second argument to `useStore()`
|
|
132
|
+
- Concurrent mode safety — verified with React 18 concurrent test suite
|
|
133
|
+
- Auto-cleanup of subscriptions on component unmount
|
|
134
|
+
- Integration tests with React Testing Library
|
|
135
|
+
- Performance benchmarks for subscription setup, cleanup, selector execution
|
|
136
|
+
- 55 tests passing across 6 test files
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## [0.1.0] - 2026-02-28
|
|
141
|
+
### Added — `storve-core`
|
|
142
|
+
- Initial `createStore()` implementation
|
|
143
|
+
- Proxy-based auto-tracking for state mutations with eager wrapping for deep paths
|
|
144
|
+
- Proper interception of arrays (`push`, `pop`, `splice`, index sets)
|
|
145
|
+
- Store subscription mechanism via `subscribe()`
|
|
146
|
+
- `getState()` and `setState()` — plain object and updater function forms
|
|
147
|
+
- Added types: `StoreDefinition`, `Listener`, `Unsubscribe`, `Store`
|
|
148
|
+
- Full test coverage for core store (100% functions, 95%+ statements)
|
|
149
|
+
- Week 3 performance benchmarks and limits verification
|
|
150
|
+
(100% functions, 95%+ statements)
|
|
151
|
+
- Week 3 performance benchmarks and limits verification
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { performance } from 'perf_hooks'
|
|
2
|
+
import { createStore } from '../src/index'
|
|
3
|
+
|
|
4
|
+
type BenchmarkResult = {
|
|
5
|
+
operation: string
|
|
6
|
+
averageMs: string
|
|
7
|
+
status: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function bench(label: string, fn: () => void, iterations = 100_000): BenchmarkResult {
|
|
11
|
+
// Warmup
|
|
12
|
+
for (let i = 0; i < 1000; i++) fn()
|
|
13
|
+
|
|
14
|
+
const start = performance.now()
|
|
15
|
+
for (let i = 0; i < iterations; i++) fn()
|
|
16
|
+
const avg = (performance.now() - start) / iterations
|
|
17
|
+
|
|
18
|
+
const limits: Record<string, number> = {
|
|
19
|
+
'createStore() call': 1,
|
|
20
|
+
'getState() read': 0.1,
|
|
21
|
+
'setState() write + notify (100 subs)': 1,
|
|
22
|
+
'Nested read (3 levels deep)': 0.1,
|
|
23
|
+
'Subscribe + Unsubscribe cycle': 0.1,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const limit = limits[label]
|
|
27
|
+
const status = avg <= limit ? '✅ PASS' : '❌ FAIL'
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
operation: label,
|
|
31
|
+
averageMs: avg.toFixed(8) + 'ms',
|
|
32
|
+
status,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function runBenchmarks(): void {
|
|
37
|
+
console.log('\n⚡ Storve — Benchmark Results\n')
|
|
38
|
+
|
|
39
|
+
const results: BenchmarkResult[] = []
|
|
40
|
+
|
|
41
|
+
// 1. createStore
|
|
42
|
+
results.push(bench('createStore() call', () => {
|
|
43
|
+
createStore({ count: 0, name: 'test' })
|
|
44
|
+
}, 10_000))
|
|
45
|
+
|
|
46
|
+
// 2. getState
|
|
47
|
+
const storeForGet = createStore({ count: 0 })
|
|
48
|
+
results.push(bench('getState() read', () => {
|
|
49
|
+
storeForGet.getState()
|
|
50
|
+
}))
|
|
51
|
+
|
|
52
|
+
// 3. setState + notify with 100 subscribers
|
|
53
|
+
const storeForSet = createStore({ count: 0 })
|
|
54
|
+
for (let i = 0; i < 100; i++) storeForSet.subscribe(() => { })
|
|
55
|
+
let counter = 0
|
|
56
|
+
results.push(bench('setState() write + notify (100 subs)', () => {
|
|
57
|
+
storeForSet.setState({ count: counter++ })
|
|
58
|
+
}, 10_000))
|
|
59
|
+
|
|
60
|
+
// 4. Nested read
|
|
61
|
+
const storeForNested = createStore({
|
|
62
|
+
level1: { level2: { level3: { value: 42 } } },
|
|
63
|
+
})
|
|
64
|
+
results.push(bench('Nested read (3 levels deep)', () => {
|
|
65
|
+
storeForNested.getState().level1.level2.level3.value
|
|
66
|
+
}))
|
|
67
|
+
|
|
68
|
+
// 5. Subscribe + Unsubscribe
|
|
69
|
+
const storeForSub = createStore({ count: 0 })
|
|
70
|
+
results.push(bench('Subscribe + Unsubscribe cycle', () => {
|
|
71
|
+
const unsub = storeForSub.subscribe(() => { })
|
|
72
|
+
unsub()
|
|
73
|
+
}))
|
|
74
|
+
|
|
75
|
+
// Print table
|
|
76
|
+
const colWidths = { operation: 45, averageMs: 20, status: 10 }
|
|
77
|
+
const header =
|
|
78
|
+
'Operation'.padEnd(colWidths.operation) +
|
|
79
|
+
'Average Time'.padEnd(colWidths.averageMs) +
|
|
80
|
+
'Status'
|
|
81
|
+
const divider = '-'.repeat(header.length)
|
|
82
|
+
|
|
83
|
+
console.log(header)
|
|
84
|
+
console.log(divider)
|
|
85
|
+
|
|
86
|
+
let allPassed = true
|
|
87
|
+
for (const r of results) {
|
|
88
|
+
if (r.status.includes('FAIL')) allPassed = false
|
|
89
|
+
console.log(
|
|
90
|
+
r.operation.padEnd(colWidths.operation) +
|
|
91
|
+
r.averageMs.padEnd(colWidths.averageMs) +
|
|
92
|
+
r.status
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(divider)
|
|
97
|
+
console.log(allPassed ? '\n✅ All benchmarks passed!\n' : '\n❌ Some benchmarks failed!\n')
|
|
98
|
+
|
|
99
|
+
if (!allPassed) process.exit(1)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
runBenchmarks()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Week 2 Benchmarks
|
|
2
|
+
|
|
3
|
+
| Operation | Average Time (ms) |
|
|
4
|
+
|---|---|
|
|
5
|
+
| createStore() call | 0.0891ms |
|
|
6
|
+
| getState() read | 0.000001ms |
|
|
7
|
+
| setState() write + notify (100 subs) | 0.001450ms |
|
|
8
|
+
| Nested read (3 levels deep) | 0.000005ms |
|
|
9
|
+
| Subscribe + Unsubscribe cycle | 0.000159ms |
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { performance } from 'perf_hooks';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { createStore } from '../src/store';
|
|
5
|
+
|
|
6
|
+
function measure(name: string, fn: () => void, iterations = 10000) {
|
|
7
|
+
const start = performance.now();
|
|
8
|
+
for (let i = 0; i < iterations; i++) {
|
|
9
|
+
fn();
|
|
10
|
+
}
|
|
11
|
+
const duration = performance.now() - start;
|
|
12
|
+
const avg = duration / iterations;
|
|
13
|
+
return `| ${name} | ${avg.toFixed(6)}ms |`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function run() {
|
|
17
|
+
const results = [
|
|
18
|
+
`# Week 2 Benchmarks`,
|
|
19
|
+
``,
|
|
20
|
+
`| Operation | Average Time (ms) |`,
|
|
21
|
+
`|---|---|`
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// createStore() call (< 1ms)
|
|
25
|
+
const t0 = performance.now();
|
|
26
|
+
const store = createStore({ count: 0 });
|
|
27
|
+
const createTime = performance.now() - t0;
|
|
28
|
+
results.push(`| createStore() call | ${createTime.toFixed(4)}ms |`);
|
|
29
|
+
|
|
30
|
+
// getState() read (< 0.1ms)
|
|
31
|
+
results.push(measure('getState() read', () => {
|
|
32
|
+
store.getState();
|
|
33
|
+
}, 1000000));
|
|
34
|
+
|
|
35
|
+
// setState() write + notify (< 1ms for 100 subscribers)
|
|
36
|
+
const storeWithSubs = createStore({ value: 0 });
|
|
37
|
+
for (let i = 0; i < 100; i++) {
|
|
38
|
+
storeWithSubs.subscribe(() => { });
|
|
39
|
+
}
|
|
40
|
+
results.push(measure('setState() write + notify (100 subs)', () => {
|
|
41
|
+
storeWithSubs.setState({ value: Math.random() });
|
|
42
|
+
}, 1000));
|
|
43
|
+
|
|
44
|
+
// Nested read (3 levels deep) (< 0.1ms)
|
|
45
|
+
const nestedStore = createStore({ root: { level1: { level2: { val: 42 } } } });
|
|
46
|
+
results.push(measure('Nested read (3 levels deep)', () => {
|
|
47
|
+
void nestedStore.getState().root.level1.level2.val;
|
|
48
|
+
}, 1000000));
|
|
49
|
+
|
|
50
|
+
// Subscribe + Unsubscribe cycle (< 0.1ms)
|
|
51
|
+
const subStore = createStore({ x: 1 });
|
|
52
|
+
results.push(measure('Subscribe + Unsubscribe cycle', () => {
|
|
53
|
+
const unsub = subStore.subscribe(() => { });
|
|
54
|
+
unsub();
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
const content = results.join('\n');
|
|
58
|
+
console.log(content);
|
|
59
|
+
|
|
60
|
+
fs.mkdirSync(path.join(process.cwd(), 'benchmarks'), { recursive: true });
|
|
61
|
+
fs.writeFileSync(path.join(process.cwd(), 'benchmarks', 'week2.md'), content);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
run();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Week 4 Benchmarks (v0.3)
|
|
2
|
+
|
|
3
|
+
| Operation | Average Time | Status |
|
|
4
|
+
| :--- | :--- | :--- |
|
|
5
|
+
| action call (no-arg) | 0.00026063ms | ✅ PASS |
|
|
6
|
+
| action call (with arg) | 0.00026390ms | ✅ PASS |
|
|
7
|
+
| action call (async dispatch) | 0.00019697ms | ✅ PASS |
|
|
8
|
+
| setState (immer mutator, primitive) | 0.00077454ms | ✅ PASS |
|
|
9
|
+
| setState (immer mutator, nested object) | 0.00252697ms | ✅ PASS |
|
|
10
|
+
| setState (immer mutator, array push) | 0.00828483ms | ✅ PASS |
|
|
11
|
+
| batch (3x setState, 1 notify) | 0.00259722ms | ✅ PASS |
|
|
12
|
+
| batch (10x setState, 1 notify) | 0.03388838ms | ✅ PASS |
|
|
13
|
+
| action excluded from getState() | 0.00000872ms | ✅ PASS |
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { createStore } from '../src'
|
|
2
|
+
|
|
3
|
+
interface BenchmarkResult {
|
|
4
|
+
operation: string
|
|
5
|
+
averageMs: string
|
|
6
|
+
status: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function bench(
|
|
10
|
+
label: string,
|
|
11
|
+
fn: () => void,
|
|
12
|
+
iterations = 100_000
|
|
13
|
+
): BenchmarkResult {
|
|
14
|
+
// Warmup
|
|
15
|
+
for (let i = 0; i < 1000; i++) fn()
|
|
16
|
+
|
|
17
|
+
const start = performance.now()
|
|
18
|
+
for (let i = 0; i < iterations; i++) fn()
|
|
19
|
+
const end = performance.now()
|
|
20
|
+
|
|
21
|
+
const avg = (end - start) / iterations
|
|
22
|
+
|
|
23
|
+
const limits: Record<string, number> = {
|
|
24
|
+
'action call (no-arg)': 0.5,
|
|
25
|
+
'action call (with arg)': 0.5,
|
|
26
|
+
'action call (async dispatch)': 1.0,
|
|
27
|
+
'setState (immer mutator, primitive)': 1.0,
|
|
28
|
+
'setState (immer mutator, nested object)': 2.0,
|
|
29
|
+
'setState (immer mutator, array push)': 2.0,
|
|
30
|
+
'batch (3x setState, 1 notify)': 1.5,
|
|
31
|
+
'batch (10x setState, 1 notify)': 3.0,
|
|
32
|
+
'action excluded from getState()': 0.2,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const limit = limits[label] ?? 1
|
|
36
|
+
const status = avg <= limit ? '✅ PASS' : '❌ FAIL'
|
|
37
|
+
return { operation: label, averageMs: avg.toFixed(8) + 'ms', status }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function runBenchmarks(): void {
|
|
41
|
+
console.log('\n⚡ Storve Core — Week 4 Benchmark Results (v0.3)\n')
|
|
42
|
+
|
|
43
|
+
const results: BenchmarkResult[] = []
|
|
44
|
+
|
|
45
|
+
// ── 1. Action call — no argument
|
|
46
|
+
const store1 = createStore({
|
|
47
|
+
count: 0,
|
|
48
|
+
actions: {
|
|
49
|
+
increment() { store1.setState(s => ({ count: s.count + 1 })) }
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
results.push(bench('action call (no-arg)', () => {
|
|
53
|
+
store1.increment()
|
|
54
|
+
}))
|
|
55
|
+
|
|
56
|
+
// ── 2. Action call — with argument
|
|
57
|
+
const store2 = createStore({
|
|
58
|
+
count: 0,
|
|
59
|
+
actions: {
|
|
60
|
+
incrementBy(n: number) { store2.setState(s => ({ count: s.count + n })) }
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
results.push(bench('action call (with arg)', () => {
|
|
64
|
+
store2.incrementBy(1)
|
|
65
|
+
}))
|
|
66
|
+
|
|
67
|
+
// ── 3. Async action dispatch (fire, don't await — measures dispatch overhead)
|
|
68
|
+
const store3 = createStore({
|
|
69
|
+
count: 0,
|
|
70
|
+
actions: {
|
|
71
|
+
async incrementAsync() {
|
|
72
|
+
await Promise.resolve()
|
|
73
|
+
store3.setState(s => ({ count: s.count + 1 }))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
results.push(bench('action call (async dispatch)', () => {
|
|
78
|
+
void store3.incrementAsync()
|
|
79
|
+
}, 10_000))
|
|
80
|
+
|
|
81
|
+
// ── 4. Immer — primitive mutation
|
|
82
|
+
const store4 = createStore({ count: 0 }, { immer: true })
|
|
83
|
+
results.push(bench('setState (immer mutator, primitive)', () => {
|
|
84
|
+
store4.setState(draft => { draft.count++ })
|
|
85
|
+
}))
|
|
86
|
+
|
|
87
|
+
// ── 5. Immer — nested object mutation
|
|
88
|
+
const store5 = createStore({
|
|
89
|
+
user: { name: 'Alice', age: 30, score: 0 }
|
|
90
|
+
}, { immer: true })
|
|
91
|
+
results.push(bench('setState (immer mutator, nested object)', () => {
|
|
92
|
+
store5.setState(draft => { draft.user.score++ })
|
|
93
|
+
}))
|
|
94
|
+
|
|
95
|
+
// ── 6. Immer — array push
|
|
96
|
+
const store6 = createStore({ items: [] as number[] }, { immer: true })
|
|
97
|
+
let idCounter = 0
|
|
98
|
+
results.push(bench('setState (immer mutator, array push)', () => {
|
|
99
|
+
store6.setState(draft => { draft.items.push(idCounter++) })
|
|
100
|
+
// Keep array from growing unboundedly
|
|
101
|
+
if (store6.getState().items.length > 100) {
|
|
102
|
+
store6.setState({ items: [] })
|
|
103
|
+
}
|
|
104
|
+
}, 10_000))
|
|
105
|
+
|
|
106
|
+
// ── 7. Batch — 3 setState calls
|
|
107
|
+
const store7 = createStore({ a: 0, b: 0, c: 0 })
|
|
108
|
+
let n7 = 0
|
|
109
|
+
results.push(bench('batch (3x setState, 1 notify)', () => {
|
|
110
|
+
store7.batch(() => {
|
|
111
|
+
store7.setState({ a: n7 })
|
|
112
|
+
store7.setState({ b: n7 })
|
|
113
|
+
store7.setState({ c: n7++ })
|
|
114
|
+
})
|
|
115
|
+
}))
|
|
116
|
+
|
|
117
|
+
// ── 8. Batch — 10 setState calls
|
|
118
|
+
const store8 = createStore({
|
|
119
|
+
v0: 0, v1: 0, v2: 0, v3: 0, v4: 0,
|
|
120
|
+
v5: 0, v6: 0, v7: 0, v8: 0, v9: 0,
|
|
121
|
+
})
|
|
122
|
+
let n8 = 0
|
|
123
|
+
results.push(bench('batch (10x setState, 1 notify)', () => {
|
|
124
|
+
store8.batch(() => {
|
|
125
|
+
store8.setState({ v0: n8 })
|
|
126
|
+
store8.setState({ v1: n8 })
|
|
127
|
+
store8.setState({ v2: n8 })
|
|
128
|
+
store8.setState({ v3: n8 })
|
|
129
|
+
store8.setState({ v4: n8 })
|
|
130
|
+
store8.setState({ v5: n8 })
|
|
131
|
+
store8.setState({ v6: n8 })
|
|
132
|
+
store8.setState({ v7: n8 })
|
|
133
|
+
store8.setState({ v8: n8 })
|
|
134
|
+
store8.setState({ v9: n8++ })
|
|
135
|
+
})
|
|
136
|
+
}))
|
|
137
|
+
|
|
138
|
+
// ── 9. Action excluded from getState — confirms no overhead
|
|
139
|
+
const store9 = createStore({
|
|
140
|
+
count: 0,
|
|
141
|
+
actions: { increment() { store9.setState(s => ({ count: s.count + 1 })) } }
|
|
142
|
+
})
|
|
143
|
+
results.push(bench('action excluded from getState()', () => {
|
|
144
|
+
const state = store9.getState()
|
|
145
|
+
void ('increment' in state)
|
|
146
|
+
}))
|
|
147
|
+
|
|
148
|
+
// ── Print results
|
|
149
|
+
const colWidths = { operation: 48, averageMs: 22, status: 10 }
|
|
150
|
+
const header =
|
|
151
|
+
'Operation'.padEnd(colWidths.operation) +
|
|
152
|
+
'Average Time'.padEnd(colWidths.averageMs) +
|
|
153
|
+
'Status'
|
|
154
|
+
const divider = '─'.repeat(header.length)
|
|
155
|
+
|
|
156
|
+
console.log(header)
|
|
157
|
+
console.log(divider)
|
|
158
|
+
|
|
159
|
+
let allPassed = true
|
|
160
|
+
for (const r of results) {
|
|
161
|
+
if (r.status.includes('FAIL')) allPassed = false
|
|
162
|
+
console.log(
|
|
163
|
+
r.operation.padEnd(colWidths.operation) +
|
|
164
|
+
r.averageMs.padEnd(colWidths.averageMs) +
|
|
165
|
+
r.status
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log(divider)
|
|
170
|
+
console.log(allPassed
|
|
171
|
+
? '\n✅ All Week 4 benchmarks passed!\n'
|
|
172
|
+
: '\n❌ Some benchmarks failed — investigate before merging.\n'
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if (!allPassed) process.exit(1)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
runBenchmarks()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Week 5 Benchmarks (v0.4 Async State)
|
|
2
|
+
|
|
3
|
+
| Operation | Average Time | Status |
|
|
4
|
+
| :--- | :--- | :--- |
|
|
5
|
+
| createAsync() initialization | 0.00023803ms | ✅ PASS |
|
|
6
|
+
| fetch() - cache hit (TTL) | 0.00055767ms | ✅ PASS |
|
|
7
|
+
| fetch() - cache miss (resolved) | 0.00344446ms | ✅ PASS |
|
|
8
|
+
| refetch() - overhead | 0.00452113ms | ✅ PASS |
|
|
9
|
+
| optimistic update - immediate state change | 0.00286880ms | ✅ PASS |
|
|
10
|
+
|
|
11
|
+
## Observations
|
|
12
|
+
- **Engine Initialization**: `createAsync` is extremely lightweight as it only returns a definition object. The actual engine is lazily initialized when the store is created.
|
|
13
|
+
- **Cache Performance**: TTL cache hits are highly optimized (~0.5ns), adding negligible overhead to state reads.
|
|
14
|
+
- **Engine Overhead**: Cache misses and refetches show roughly 3-4µs overhead. This is well within the 1ms budget for store operations, even considering the overhead of Promise resolution in the benchmark.
|
|
15
|
+
- **Optimistic Updates**: Immediate state updates via optimistic results are fast (~2.8µs), ensuring UI responsiveness during async triggers.
|