@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.
Files changed (196) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/benchmarks/run.ts +102 -0
  3. package/benchmarks/week2.md +9 -0
  4. package/benchmarks/week2.ts +64 -0
  5. package/benchmarks/week4.md +13 -0
  6. package/benchmarks/week4.ts +178 -0
  7. package/benchmarks/week5.md +15 -0
  8. package/benchmarks/week5.ts +184 -0
  9. package/coverage/coverage-summary.json +31 -0
  10. package/dist/adapters/indexedDB.cjs +2 -0
  11. package/dist/adapters/indexedDB.cjs.map +1 -0
  12. package/dist/adapters/indexedDB.mjs +2 -0
  13. package/dist/adapters/indexedDB.mjs.map +1 -0
  14. package/dist/adapters/localStorage.cjs +2 -0
  15. package/dist/adapters/localStorage.cjs.map +1 -0
  16. package/dist/adapters/localStorage.mjs +2 -0
  17. package/dist/adapters/localStorage.mjs.map +1 -0
  18. package/dist/adapters/memory.cjs +2 -0
  19. package/dist/adapters/memory.cjs.map +1 -0
  20. package/dist/adapters/memory.mjs +2 -0
  21. package/dist/adapters/memory.mjs.map +1 -0
  22. package/dist/adapters/sessionStorage.cjs +2 -0
  23. package/dist/adapters/sessionStorage.cjs.map +1 -0
  24. package/dist/adapters/sessionStorage.mjs +2 -0
  25. package/dist/adapters/sessionStorage.mjs.map +1 -0
  26. package/dist/async-entry.d.ts +7 -0
  27. package/dist/async-entry.d.ts.map +1 -0
  28. package/dist/async.cjs +2 -0
  29. package/dist/async.cjs.map +1 -0
  30. package/dist/async.d.ts +52 -0
  31. package/dist/async.d.ts.map +1 -0
  32. package/dist/async.mjs +2 -0
  33. package/dist/async.mjs.map +1 -0
  34. package/dist/batch.d.ts +12 -0
  35. package/dist/batch.d.ts.map +1 -0
  36. package/dist/compose.d.ts +7 -0
  37. package/dist/compose.d.ts.map +1 -0
  38. package/dist/computed-entry.d.ts +7 -0
  39. package/dist/computed-entry.d.ts.map +1 -0
  40. package/dist/computed.cjs +2 -0
  41. package/dist/computed.cjs.map +1 -0
  42. package/dist/computed.d.ts +56 -0
  43. package/dist/computed.d.ts.map +1 -0
  44. package/dist/computed.mjs +2 -0
  45. package/dist/computed.mjs.map +1 -0
  46. package/dist/devtools/history.d.ts +51 -0
  47. package/dist/devtools/history.d.ts.map +1 -0
  48. package/dist/devtools/index.d.ts +5 -0
  49. package/dist/devtools/index.d.ts.map +1 -0
  50. package/dist/devtools/redux-bridge.d.ts +21 -0
  51. package/dist/devtools/redux-bridge.d.ts.map +1 -0
  52. package/dist/devtools/snapshots.d.ts +32 -0
  53. package/dist/devtools/snapshots.d.ts.map +1 -0
  54. package/dist/devtools/withDevtools.d.ts +17 -0
  55. package/dist/devtools/withDevtools.d.ts.map +1 -0
  56. package/dist/devtools.cjs +2 -0
  57. package/dist/devtools.cjs.map +1 -0
  58. package/dist/devtools.mjs +2 -0
  59. package/dist/devtools.mjs.map +1 -0
  60. package/dist/extensions/noop.d.ts +2 -0
  61. package/dist/extensions/noop.d.ts.map +1 -0
  62. package/dist/index.cjs +2 -0
  63. package/dist/index.cjs.js +118 -0
  64. package/dist/index.cjs.js.map +1 -0
  65. package/dist/index.cjs.map +1 -0
  66. package/dist/index.d.ts +5 -0
  67. package/dist/index.d.ts.map +1 -0
  68. package/dist/index.esm.js +116 -0
  69. package/dist/index.esm.js.map +1 -0
  70. package/dist/index.mjs +2 -0
  71. package/dist/index.mjs.map +1 -0
  72. package/dist/persist/adapters/indexedDB.d.ts +12 -0
  73. package/dist/persist/adapters/indexedDB.d.ts.map +1 -0
  74. package/dist/persist/adapters/localStorage.d.ts +11 -0
  75. package/dist/persist/adapters/localStorage.d.ts.map +1 -0
  76. package/dist/persist/adapters/memory.d.ts +11 -0
  77. package/dist/persist/adapters/memory.d.ts.map +1 -0
  78. package/dist/persist/adapters/sessionStorage.d.ts +11 -0
  79. package/dist/persist/adapters/sessionStorage.d.ts.map +1 -0
  80. package/dist/persist/debounce.d.ts +12 -0
  81. package/dist/persist/debounce.d.ts.map +1 -0
  82. package/dist/persist/hydrate.d.ts +15 -0
  83. package/dist/persist/hydrate.d.ts.map +1 -0
  84. package/dist/persist/index.d.ts +34 -0
  85. package/dist/persist/index.d.ts.map +1 -0
  86. package/dist/persist/serialize.d.ts +28 -0
  87. package/dist/persist/serialize.d.ts.map +1 -0
  88. package/dist/persist.cjs +2 -0
  89. package/dist/persist.cjs.map +1 -0
  90. package/dist/persist.mjs +2 -0
  91. package/dist/persist.mjs.map +1 -0
  92. package/dist/proxy.d.ts +2 -0
  93. package/dist/proxy.d.ts.map +1 -0
  94. package/dist/registry-D3X0HSbl.js +26 -0
  95. package/dist/registry-D3X0HSbl.js.map +1 -0
  96. package/dist/registry-RDjbeJdx.js +29 -0
  97. package/dist/registry-RDjbeJdx.js.map +1 -0
  98. package/dist/registry-qtr1UpFU.js +2 -0
  99. package/dist/registry-qtr1UpFU.js.map +1 -0
  100. package/dist/registry-zaKZ1P-s.js +2 -0
  101. package/dist/registry-zaKZ1P-s.js.map +1 -0
  102. package/dist/registry.d.ts +54 -0
  103. package/dist/registry.d.ts.map +1 -0
  104. package/dist/signals/createSignal.d.ts +19 -0
  105. package/dist/signals/createSignal.d.ts.map +1 -0
  106. package/dist/signals/index.d.ts +20 -0
  107. package/dist/signals/index.d.ts.map +1 -0
  108. package/dist/signals/useSignal.d.ts +11 -0
  109. package/dist/signals/useSignal.d.ts.map +1 -0
  110. package/dist/signals.cjs +2 -0
  111. package/dist/signals.cjs.map +1 -0
  112. package/dist/signals.mjs +2 -0
  113. package/dist/signals.mjs.map +1 -0
  114. package/dist/stats.html +4949 -0
  115. package/dist/store.d.ts +12 -0
  116. package/dist/store.d.ts.map +1 -0
  117. package/dist/sync/channel.d.ts +7 -0
  118. package/dist/sync/channel.d.ts.map +1 -0
  119. package/dist/sync/index.d.ts +3 -0
  120. package/dist/sync/index.d.ts.map +1 -0
  121. package/dist/sync/protocol.d.ts +22 -0
  122. package/dist/sync/protocol.d.ts.map +1 -0
  123. package/dist/sync/withSync.d.ts +17 -0
  124. package/dist/sync/withSync.d.ts.map +1 -0
  125. package/dist/sync.cjs +2 -0
  126. package/dist/sync.cjs.map +1 -0
  127. package/dist/sync.mjs +2 -0
  128. package/dist/sync.mjs.map +1 -0
  129. package/dist/types.d.ts +134 -0
  130. package/dist/types.d.ts.map +1 -0
  131. package/package.json +91 -0
  132. package/rollup.config.mjs +44 -0
  133. package/src/async-entry.ts +6 -0
  134. package/src/async.ts +240 -0
  135. package/src/batch.ts +33 -0
  136. package/src/compose.ts +50 -0
  137. package/src/computed-entry.ts +6 -0
  138. package/src/computed.ts +187 -0
  139. package/src/devtools/history.ts +103 -0
  140. package/src/devtools/index.ts +5 -0
  141. package/src/devtools/redux-bridge.ts +70 -0
  142. package/src/devtools/snapshots.ts +54 -0
  143. package/src/devtools/withDevtools.ts +196 -0
  144. package/src/extensions/noop.ts +12 -0
  145. package/src/index.ts +4 -0
  146. package/src/persist/adapters/indexedDB.ts +114 -0
  147. package/src/persist/adapters/localStorage.ts +28 -0
  148. package/src/persist/adapters/memory.ts +26 -0
  149. package/src/persist/adapters/sessionStorage.ts +28 -0
  150. package/src/persist/debounce.ts +28 -0
  151. package/src/persist/hydrate.ts +60 -0
  152. package/src/persist/index.ts +141 -0
  153. package/src/persist/serialize.ts +60 -0
  154. package/src/proxy.ts +87 -0
  155. package/src/registry.ts +67 -0
  156. package/src/signals/createSignal.ts +81 -0
  157. package/src/signals/index.ts +20 -0
  158. package/src/signals/useSignal.ts +18 -0
  159. package/src/store.ts +250 -0
  160. package/src/sync/channel.ts +15 -0
  161. package/src/sync/index.ts +3 -0
  162. package/src/sync/protocol.ts +18 -0
  163. package/src/sync/withSync.ts +147 -0
  164. package/src/types.ts +159 -0
  165. package/tests/async.test.ts +1100 -0
  166. package/tests/batch.test.ts +41 -0
  167. package/tests/compose.test.ts +209 -0
  168. package/tests/computed.test.ts +867 -0
  169. package/tests/devtools.test.ts +1039 -0
  170. package/tests/integration/persist.integration.test.ts +258 -0
  171. package/tests/integration/signals.integration.test.ts +309 -0
  172. package/tests/integration.test.ts +278 -0
  173. package/tests/persist/adapters/indexedDB.adapter.test.ts +185 -0
  174. package/tests/persist/adapters/localStorage.adapter.test.ts +105 -0
  175. package/tests/persist/adapters/memory.adapter.test.ts +112 -0
  176. package/tests/persist/adapters/sessionStorage.adapter.test.ts +128 -0
  177. package/tests/persist/debounce.test.ts +121 -0
  178. package/tests/persist/hydrate.test.ts +120 -0
  179. package/tests/persist/migrate.test.ts +208 -0
  180. package/tests/persist/persist.test.ts +357 -0
  181. package/tests/persist/serialize.test.ts +128 -0
  182. package/tests/proxy.test.ts +473 -0
  183. package/tests/registry.test.ts +67 -0
  184. package/tests/signals/derived.test.ts +244 -0
  185. package/tests/signals/inference.test.ts +108 -0
  186. package/tests/signals/signal.test.ts +348 -0
  187. package/tests/signals/useSignal.test.tsx +275 -0
  188. package/tests/store.test.ts +482 -0
  189. package/tests/stress.test.ts +268 -0
  190. package/tests/sync.test.ts +576 -0
  191. package/tests/types.test.ts +32 -0
  192. package/tests/v0.3.test.ts +813 -0
  193. package/tree-shake-test.js +1 -0
  194. package/tsconfig.json +15 -0
  195. package/vitest.config.ts +22 -0
  196. 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.