@sladg/apex-state 3.3.0 → 3.4.1

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/README.md CHANGED
@@ -2,11 +2,71 @@
2
2
 
3
3
  Reactive state management for React built on [Valtio](https://github.com/pmndrs/valtio). Declare what your fields need — validation, conditional UI, sync, listeners — and the store handles the rest. Optional Rust/WASM accelerator for complex workloads (up to 367x faster).
4
4
 
5
+ ## Quick Start
6
+
5
7
  ```bash
6
8
  npm install @sladg/apex-state valtio zod react
7
9
  ```
8
10
 
9
- ## Example
11
+ ```tsx
12
+ import { createGenericStore } from '@sladg/apex-state'
13
+ import { z } from 'zod'
14
+
15
+ // 1. Define your state shape
16
+ type FormState = {
17
+ user: { name: string; email: string; age: number }
18
+ preferences: { newsletter: boolean; theme: 'light' | 'dark' }
19
+ }
20
+
21
+ // 2. Create a typed store (WASM-accelerated by default)
22
+ const store = createGenericStore<FormState>()
23
+
24
+ // 3. Wrap your app with Provider
25
+ const App = () => (
26
+ <store.Provider initialState={{
27
+ user: { name: '', email: '', age: 0 },
28
+ preferences: { newsletter: false, theme: 'light' },
29
+ }}>
30
+ <UserForm />
31
+ </store.Provider>
32
+ )
33
+
34
+ // 4. Use hooks to read/write state and declare concerns
35
+ const UserForm = () => {
36
+ store.useConcerns('user-form', {
37
+ 'user.email': {
38
+ validationState: { schema: z.string().email('Invalid email') },
39
+ },
40
+ })
41
+
42
+ const { value, setValue, validationState } = store.useFieldStore('user.email')
43
+
44
+ return (
45
+ <div>
46
+ <input
47
+ value={value}
48
+ onChange={(e) => setValue(e.target.value)}
49
+ className={validationState?.isError ? 'error' : ''}
50
+ />
51
+ {validationState?.isError && <span>{validationState.errors[0]}</span>}
52
+ </div>
53
+ )
54
+ }
55
+ ```
56
+
57
+ ## Features
58
+
59
+ | Feature | Description | Details |
60
+ |---|---|---|
61
+ | **Type-safe paths** | `DeepKey<T>` / `DeepValue<T, P>` — compile-time path safety | |
62
+ | **Concerns** | Validation (Zod), BoolLogic conditions, dynamic text | [Concerns Guide](docs/guides/CONCERNS_GUIDE.md) |
63
+ | **Side effects** | Sync paths, flip paths, aggregations, listeners | [Side Effects Guide](docs/SIDE_EFFECTS_GUIDE.md) |
64
+ | **WASM mode** | Rust-powered pipeline for bulk operations (up to 367x faster) | [Architecture](docs/WASM_ARCHITECTURE.md) |
65
+ | **Composable hooks** | Buffered, throttled, transformed field wrappers | [Store & Hooks](docs/guides/STORE_HOOKS.md) |
66
+ | **Record/wildcard** | `Record<string, V>` with `_()` hash key paths | [Wildcard Guide](docs/WILD_FUNCTION_GUIDE.md) |
67
+ | **Testing mock** | Drop-in `vi.mock` replacement with call tracking | [Testing Mock](docs/TESTING_MOCK.md) |
68
+
69
+ ## Full Example
10
70
 
11
71
  ```tsx
12
72
  import { createGenericStore } from '@sladg/apex-state'
@@ -14,7 +74,7 @@ import { z } from 'zod'
14
74
 
15
75
  type OrderState = {
16
76
  product: { name: string; quantity: number; price: number }
17
- shipping: { address: string; express: boolean }
77
+ shipping: { address: string; express: boolean; standard: boolean }
18
78
  payment: { method: 'card' | 'cash'; cardNumber: string }
19
79
  status: 'draft' | 'submitted'
20
80
  }
@@ -22,21 +82,20 @@ type OrderState = {
22
82
  const store = createGenericStore<OrderState>()
23
83
 
24
84
  const OrderForm = () => {
25
- // Declare side effects
85
+ // Side effects: auto-flip booleans
26
86
  store.useSideEffects('order', {
27
- syncPaths: [['product.price', 'shipping.basePrice']],
28
87
  flipPaths: [['shipping.express', 'shipping.standard']],
29
88
  })
30
89
 
31
- // Declare concerns just data, no logic to test
90
+ // Concerns: declarative validation and conditional UI
32
91
  store.useConcerns('order', {
33
92
  'product.quantity': {
34
93
  validationState: { schema: z.number().min(1).max(100) },
35
- disabledWhen: { condition: { IS_EQUAL: ['status', 'submitted'] } },
94
+ disabledWhen: { boolLogic: { IS_EQUAL: ['status', 'submitted'] } },
36
95
  },
37
96
  'payment.cardNumber': {
38
97
  validationState: { schema: z.string().regex(/^\d{16}$/) },
39
- visibleWhen: { condition: { IS_EQUAL: ['payment.method', 'card'] } },
98
+ visibleWhen: { boolLogic: { IS_EQUAL: ['payment.method', 'card'] } },
40
99
  },
41
100
  })
42
101
 
@@ -57,7 +116,7 @@ const OrderForm = () => {
57
116
  const App = () => (
58
117
  <store.Provider initialState={{
59
118
  product: { name: 'Widget', quantity: 1, price: 29.99 },
60
- shipping: { address: '', express: false },
119
+ shipping: { address: '', express: false, standard: true },
61
120
  payment: { method: 'card', cardNumber: '' },
62
121
  status: 'draft',
63
122
  }}>
@@ -66,123 +125,416 @@ const App = () => (
66
125
  )
67
126
  ```
68
127
 
69
- ## Features
128
+ ## Reading and Writing State
70
129
 
71
- | Feature | Description | Details |
72
- |---|---|---|
73
- | **Type-safe paths** | `DeepKey<T>` / `DeepValue<T, P>` — compile-time path safety | |
74
- | **Concerns** | Validation, BoolLogic conditions, dynamic text | [Concerns Guide](docs/guides/CONCERNS_GUIDE.md) |
75
- | **Side effects** | Sync paths, flip paths, aggregations, listeners | [Side Effects Guide](docs/SIDE_EFFECTS_GUIDE.md) |
76
- | **WASM mode** | Rust-powered pipeline for bulk operations | [Architecture](docs/WASM_ARCHITECTURE.md) |
77
- | **Composable hooks** | Buffered, throttled, transformed field wrappers | [Store & Hooks](docs/guides/STORE_HOOKS.md) |
78
- | **Record/wildcard** | `Record<string, V>` with `[*]` wildcard paths | [Wildcard Guide](docs/WILD_FUNCTION_GUIDE.md) |
79
- | **Testing mock** | Drop-in `vi.mock` replacement with call tracking | [Testing Mock](docs/TESTING_MOCK.md) |
130
+ ```tsx
131
+ const store = createGenericStore<MyState>()
80
132
 
81
- ## Architecture
133
+ // useStore — simple [value, setter] tuple (like useState)
134
+ const NameInput = () => {
135
+ const [name, setName] = store.useStore('user.name')
136
+ return <input value={name} onChange={(e) => setName(e.target.value)} />
137
+ }
82
138
 
139
+ // useFieldStore — object API with concerns merged in
140
+ const EmailInput = () => {
141
+ const { value, setValue, validationState, disabledWhen } =
142
+ store.useFieldStore('user.email')
143
+ return (
144
+ <input
145
+ value={value}
146
+ onChange={(e) => setValue(e.target.value)}
147
+ disabled={disabledWhen}
148
+ className={validationState?.isError ? 'error' : ''}
149
+ />
150
+ )
151
+ }
152
+
153
+ // useJitStore — bulk operations and non-reactive reads
154
+ const ImportButton = () => {
155
+ const { setChanges, getState } = store.useJitStore()
156
+
157
+ const handleImport = (data: Record<string, unknown>) => {
158
+ setChanges([
159
+ ['user.name', data.name, {}],
160
+ ['user.email', data.email, {}],
161
+ ['user.age', data.age, {}],
162
+ ])
163
+ }
164
+
165
+ return <button onClick={() => handleImport({ name: 'Alice', email: 'a@b.com', age: 30 })}>Import</button>
166
+ }
83
167
  ```
84
- setValue("email", "alice@example.com")
85
-
86
- ├─[Legacy JS]──▶ sync → flip → listeners → applyBatch
87
-
88
- └─[WASM/Rust]──▶ shadow state + sync + flip + BoolLogic (Rust)
89
-
90
-
91
- execute listeners + Zod validators (JS)
92
-
93
-
94
- pipelineFinalize → diff → final changes (Rust)
95
-
96
-
97
- valtio proxy React re-render
168
+
169
+ ## Validation with Zod
170
+
171
+ ```tsx
172
+ import { createGenericStore } from '@sladg/apex-state'
173
+ import { z } from 'zod'
174
+
175
+ type ProfileState = {
176
+ user: { name: string; email: string; age: number; bio: string }
177
+ }
178
+
179
+ const store = createGenericStore<ProfileState>()
180
+
181
+ const ProfileForm = () => {
182
+ // Register validation schemas for multiple fields at once
183
+ store.useConcerns('profile-validation', {
184
+ 'user.name': {
185
+ validationState: { schema: z.string().min(2, 'Name too short').max(50) },
186
+ },
187
+ 'user.email': {
188
+ validationState: { schema: z.string().email('Enter a valid email') },
189
+ },
190
+ 'user.age': {
191
+ validationState: { schema: z.number().min(18, 'Must be 18+').max(120) },
192
+ },
193
+ 'user.bio': {
194
+ validationState: { schema: z.string().max(500, 'Bio too long') },
195
+ },
196
+ })
197
+
198
+ const email = store.useFieldStore('user.email')
199
+ const age = store.useFieldStore('user.age')
200
+
201
+ return (
202
+ <form>
203
+ <div>
204
+ <input value={email.value} onChange={(e) => email.setValue(e.target.value)} />
205
+ {email.validationState?.isError && (
206
+ <ul>{email.validationState.errors.map((err, i) => <li key={i}>{err}</li>)}</ul>
207
+ )}
208
+ </div>
209
+ <div>
210
+ <input
211
+ type="number"
212
+ value={age.value}
213
+ onChange={(e) => age.setValue(Number(e.target.value))}
214
+ />
215
+ {age.validationState?.isError && <span>{age.validationState.errors[0]}</span>}
216
+ </div>
217
+ </form>
218
+ )
219
+ }
98
220
  ```
99
221
 
100
- **Dual-layer design:** JS/React owns reactivity and rendering. Rust/WASM owns heavy computation (graphs, diffing, pipeline orchestration). The boundary is thin: paths cross as strings, values as JSON. WASM decides the execution plan, JS executes user functions.
222
+ ## Conditional UI with BoolLogic
101
223
 
102
- See [docs/WASM_ARCHITECTURE.md](docs/WASM_ARCHITECTURE.md) for the full specification.
224
+ ```tsx
225
+ store.useConcerns('conditional-ui', {
226
+ // Disable when another field has a specific value
227
+ 'user.email': {
228
+ disabledWhen: { boolLogic: { IS_EQUAL: ['status', 'submitted'] } },
229
+ },
103
230
 
104
- ## WASM Mode
231
+ // Show only when multiple conditions are true
232
+ 'payment.cardNumber': {
233
+ visibleWhen: {
234
+ boolLogic: {
235
+ AND: [
236
+ { IS_EQUAL: ['payment.method', 'card'] },
237
+ { EXISTS: 'user.email' },
238
+ ],
239
+ },
240
+ },
241
+ },
105
242
 
106
- WASM is the default. Pass `{ useLegacyImplementation: true }` for pure JS:
243
+ // Make readonly when value exceeds threshold
244
+ 'order.total': {
245
+ readonlyWhen: { boolLogic: { GT: ['order.total', 10000] } },
246
+ },
247
+
248
+ // Complex nested logic with OR, NOT
249
+ 'shipping.express': {
250
+ disabledWhen: {
251
+ boolLogic: {
252
+ OR: [
253
+ { IS_EQUAL: ['status', 'shipped'] },
254
+ { NOT: { EXISTS: 'shipping.address' } },
255
+ ],
256
+ },
257
+ },
258
+ },
259
+ })
260
+
261
+ // Available BoolLogic operators:
262
+ // IS_EQUAL, EXISTS, IS_EMPTY, GT, LT, GTE, LTE, IN, AND, OR, NOT
263
+ ```
264
+
265
+ ## Side Effects
266
+
267
+ ### Sync Paths
107
268
 
108
269
  ```tsx
109
- const store = createGenericStore<MyState>() // WASM (default)
110
- const store = createGenericStore<MyState>({ useLegacyImplementation: true }) // Legacy JS
270
+ store.useSideEffects('sync', {
271
+ syncPaths: [
272
+ ['billing.email', 'shipping.email'], // changing one updates the other
273
+ ['billing.phone', 'shipping.phone'],
274
+ ],
275
+ })
276
+ // When user types in billing.email, shipping.email updates automatically
111
277
  ```
112
278
 
113
- ### Performance
279
+ ### Flip Paths
114
280
 
115
- Benchmarked with 60 variants across 3 Record layers, 75 syncs, 40 flips, 100 BoolLogic conditions, 85 listeners:
281
+ ```tsx
282
+ store.useSideEffects('flips', {
283
+ flipPaths: [
284
+ ['isActive', 'isInactive'], // setting isActive=true -> isInactive=false
285
+ ['isExpanded', 'isCollapsed'],
286
+ ],
287
+ })
288
+ ```
116
289
 
117
- | Operation | Legacy | WASM | Winner |
118
- |---|---|---|---|
119
- | Single field edit | **0.5us** | 1.4us | Legacy 2.6x |
120
- | 7 changes + cascading listeners | 41.8ms | **0.11ms** | WASM 367x |
121
- | 60 bulk price changes | 596ms | **2.9ms** | WASM 207x |
122
- | 135 changes (full catalog refresh) | 621ms | **2.99ms** | WASM 208x |
290
+ ### Aggregations
123
291
 
124
- Both modes produce **identical state** — verified across all 16 benchmark scenarios. See [docs/BENCHMARK_COMPARISON.md](docs/BENCHMARK_COMPARISON.md) for the full analysis.
292
+ ```tsx
293
+ store.useSideEffects('agg', {
294
+ aggregations: [
295
+ // Target is ALWAYS first. Multiple pairs with same target form a group.
296
+ ['summary.price', 'legs.0.price'],
297
+ ['summary.price', 'legs.1.price'],
298
+ ['summary.price', 'legs.2.price'],
299
+ ],
300
+ })
301
+ // summary.price = leg price if ALL legs match, undefined if they differ
302
+ ```
125
303
 
126
- ### Why WASM is faster
304
+ ### Listeners
127
305
 
128
- - **Pre-computed topic routing** — listener dispatch is O(1) lookup vs O(changes x listeners) string matching
129
- - **Shadow state diffing** — fast Rust HashMap vs valtio Proxy trap overhead
130
- - **Single-pass pipeline** — aggregation + sync + flip + BoolLogic in one Rust call
131
- - **BoolLogic in pipeline** — evaluated in Rust before listeners fire; Legacy defers to async `effect()`
306
+ ```tsx
307
+ store.useSideEffects('listeners', {
308
+ listeners: [
309
+ {
310
+ path: 'user.profile', // watch changes under this path
311
+ scope: 'user.profile', // receive scoped state and relative paths
312
+ fn: (changes, state) => {
313
+ // changes: [['name', 'Alice', {}]] — paths relative to scope
314
+ // state: { name: 'Alice', email: '...' } — user.profile sub-object
315
+ return [['audit.lastEdit', Date.now(), {}]] // return FULL paths for new changes
316
+ },
317
+ },
318
+ ],
319
+ })
320
+ ```
132
321
 
133
- ### Why Legacy is faster for small ops
322
+ ## Dynamic Text
134
323
 
135
- Every WASM call pays a fixed cost: JSON serialization, wasm-bindgen marshalling, and two round trips (`processChanges` + `pipelineFinalize`). When the actual work is trivial, this ~1us overhead dominates.
324
+ ```tsx
325
+ store.useConcerns('dynamic-text', {
326
+ 'legs.0.strike': {
327
+ dynamicTooltip: { template: 'Current strike: {{legs.0.strike}}' },
328
+ dynamicLabel: { template: 'Strike for {{legs.0.product}}' },
329
+ dynamicPlaceholder: { template: 'Enter value (min {{legs.0.minStrike}})' },
330
+ },
331
+ })
136
332
 
137
- ## API Quick Reference
333
+ const { dynamicTooltip, dynamicLabel } = store.useFieldConcerns('legs.0.strike')
334
+ // dynamicTooltip -> "Current strike: 105"
335
+ // dynamicLabel -> "Strike for AAPL"
336
+ ```
138
337
 
139
- ### Store
338
+ ## Composable Field Hooks
140
339
 
141
340
  ```tsx
142
- const {
143
- Provider, // React context — accepts initialState
144
- useFieldStore, // { value, setValue, ...concerns } for a path
145
- useStore, // [value, setValue] tuple for a path
146
- useJitStore, // { proxyValue, setChanges, getState } for bulk ops
147
- useSideEffects, // register sync/flip/aggregation/listeners
148
- useConcerns, // register validation/BoolLogic/custom concerns
149
- withConcerns, // typed concern selection
150
- } = createGenericStore<MyState>(config?)
341
+ import { useBufferedField, useThrottledField, useTransformedField } from '@sladg/apex-state'
342
+
343
+ // Buffer edits locally, commit/cancel explicitly
344
+ const PriceEditor = () => {
345
+ const raw = store.useFieldStore('product.price')
346
+ const buffered = useBufferedField(raw)
347
+
348
+ return (
349
+ <div>
350
+ <input value={buffered.value} onChange={(e) => buffered.setValue(Number(e.target.value))} />
351
+ <button onClick={buffered.commit} disabled={!buffered.isDirty}>Save</button>
352
+ <button onClick={buffered.cancel}>Cancel</button>
353
+ </div>
354
+ )
355
+ }
356
+
357
+ // Throttle rapid setValue calls (e.g., slider input)
358
+ const VolumeSlider = () => {
359
+ const raw = store.useFieldStore('audio.volume')
360
+ const throttled = useThrottledField(raw, { ms: 100 })
361
+ return <input type="range" value={throttled.value} onChange={(e) => throttled.setValue(Number(e.target.value))} />
362
+ }
363
+
364
+ // Transform display format (cents <-> dollars)
365
+ const CurrencyInput = () => {
366
+ const raw = store.useFieldStore('price')
367
+ const formatted = useTransformedField(raw, {
368
+ to: (cents: number) => (cents / 100).toFixed(2), // store -> display
369
+ from: (dollars: string) => Math.round(parseFloat(dollars) * 100), // display -> store
370
+ })
371
+ return <input value={formatted.value} onChange={(e) => formatted.setValue(e.target.value)} />
372
+ }
373
+
374
+ // Chain them: buffered + transformed
375
+ const raw = store.useFieldStore('price')
376
+ const buffered = useBufferedField(raw)
377
+ const display = useTransformedField(buffered, {
378
+ to: (cents) => (cents / 100).toFixed(2),
379
+ from: (dollars) => Math.round(parseFloat(dollars) * 100),
380
+ })
381
+ // display has: value, setValue, commit, cancel, isDirty
151
382
  ```
152
383
 
153
- ### Concerns
384
+ ## Hash Key Paths for Record Types
154
385
 
155
386
  ```tsx
156
- useConcerns('id', {
157
- 'user.email': {
158
- validationState: { schema: z.string().email() },
159
- disabledWhen: { condition: { IS_EQUAL: ['tosAccepted', false] } },
160
- visibleWhen: { condition: { AND: [{ EXISTS: 'user.name' }, { IS_EQUAL: ['step', 2] }] } },
387
+ import { _ } from '@sladg/apex-state'
388
+
389
+ // _() marks a segment as a hash key for Record-typed paths
390
+ const strikePath = `portfolio.legs.${_('l1')}.strike`
391
+ // -> typed string containing HASH_KEY marker
392
+
393
+ // Use with concerns — applies to ALL keys in the Record
394
+ store.useConcerns('wildcards', {
395
+ [strikePath]: {
396
+ validationState: { schema: z.number().min(0) },
397
+ disabledWhen: { boolLogic: { IS_EQUAL: ['status', 'locked'] } },
161
398
  },
162
399
  })
400
+
401
+ // Multiple hash keys for deeply nested Records
402
+ const nestedPath = `books.${_('b1')}.products.${_('p1')}.legs.${_('l1')}.notional`
163
403
  ```
164
404
 
165
- Built-in concerns: `validationState`, `disabledWhen`, `readonlyWhen`, `visibleWhen`, `dynamicLabel`, `dynamicTooltip`, `dynamicPlaceholder`.
405
+ ## Testing with the Mock Module
406
+
407
+ ```tsx
408
+ // __mocks__/@sladg/apex-state.ts
409
+ export * from '@sladg/apex-state/testing'
166
410
 
167
- BoolLogic operators: `IS_EQUAL`, `EXISTS`, `IS_EMPTY`, `GT`, `LT`, `GTE`, `LTE`, `IN`, `AND`, `OR`, `NOT`.
411
+ // your-test.test.ts
412
+ import { vi, describe, it, expect, beforeEach } from 'vitest'
413
+ import { __mocked, createGenericStore } from '@sladg/apex-state/testing'
414
+ import { renderHook } from '@testing-library/react'
168
415
 
169
- See [Concerns Guide](docs/guides/CONCERNS_GUIDE.md) for lifecycle, custom concerns, and testing.
416
+ vi.mock('@sladg/apex-state')
170
417
 
171
- ### Side Effects
418
+ type FormState = { user: { email: string; name: string }; count: number }
172
419
 
173
- ```tsx
174
- useSideEffects('id', {
175
- syncPaths: [['source', 'target']],
176
- flipPaths: [['active', 'inactive']],
177
- // Aggregation: target reflects the common value when all sources agree, null otherwise.
178
- // Multiple pairs with the same target form a group.
179
- // Currently supports consensus (all-equal) mode only — SUM, AVG, COUNT planned (see Roadmap).
180
- aggregations: [['summary.price', 'legs.0.price'], ['summary.price', 'legs.1.price']],
181
- listeners: [{ path: 'orders', scope: 'orders', fn: handler }],
420
+ beforeEach(() => __mocked.reset())
421
+
422
+ it('seeds state with type-safe chaining', () => {
423
+ __mocked
424
+ .set<FormState>({ user: { email: '', name: '' }, count: 0 })
425
+ .set('user.email', 'alice@test.com')
426
+ .set('count', 42)
427
+
428
+ expect(__mocked.getState()).toEqual({
429
+ user: { email: 'alice@test.com', name: '' },
430
+ count: 42,
431
+ })
432
+ })
433
+
434
+ it('tracks setValue calls from hooks', () => {
435
+ const store = createGenericStore<FormState>()
436
+ const hook = renderHook(() => store.useStore('user.email'))
437
+
438
+ hook.result.current[1]('bob@test.com')
439
+
440
+ expect(__mocked.state.calls).toContainEqual({
441
+ path: 'user.email',
442
+ value: 'bob@test.com',
443
+ meta: undefined,
444
+ })
445
+ })
446
+
447
+ it('tracks concern and side effect registrations', () => {
448
+ const store = createGenericStore<FormState>()
449
+ store.useConcerns('form', { 'user.email': { disabledWhen: { boolLogic: { AND: [] } } } })
450
+ store.useSideEffects('effects', { listeners: [] })
451
+
452
+ expect(__mocked.state.effects).toHaveLength(2)
453
+ expect(__mocked.state.effects[0]?.type).toBe('concerns')
454
+ expect(__mocked.state.effects[1]?.type).toBe('sideEffects')
182
455
  })
183
456
  ```
184
457
 
185
- See [Side Effects Guide](docs/SIDE_EFFECTS_GUIDE.md) for the full API.
458
+ ## WASM vs Legacy Mode
459
+
460
+ WASM is the default. Pass `{ useLegacyImplementation: true }` for pure JS:
461
+
462
+ ```tsx
463
+ // WASM (default) — Rust-powered pipeline, faster for complex state
464
+ const wasmStore = createGenericStore<MyState>()
465
+
466
+ // Legacy JS — pure JavaScript, no WASM binary needed
467
+ const legacyStore = createGenericStore<MyState>({ useLegacyImplementation: true })
468
+ ```
469
+
470
+ ### Performance
471
+
472
+ Benchmarked with 60 variants across 3 Record layers, 75 syncs, 40 flips, 100 BoolLogic conditions, 85 listeners:
473
+
474
+ | Operation | Legacy | WASM | Winner |
475
+ |---|---|---|---|
476
+ | Single field edit | **0.5us** | 1.4us | Legacy 2.6x |
477
+ | 7 changes + cascading listeners | 41.8ms | **0.11ms** | WASM 367x |
478
+ | 60 bulk price changes | 596ms | **2.9ms** | WASM 207x |
479
+ | 135 changes (full catalog refresh) | 621ms | **2.99ms** | WASM 208x |
480
+
481
+ Both modes produce **identical state** — verified across all 16 benchmark scenarios. See [Benchmark Comparison](docs/BENCHMARK_COMPARISON.md) for the full analysis.
482
+
483
+ ## API Overview
484
+
485
+ ### Store Creation
486
+
487
+ `createGenericStore<T>(config?)` returns all hooks:
488
+
489
+ | Hook | Returns | Use case |
490
+ |---|---|---|
491
+ | `Provider` | React context | Wraps component tree with `initialState` |
492
+ | `useStore(path)` | `[value, setValue]` | Simple read/write (like `useState`) |
493
+ | `useFieldStore(path)` | `{ value, setValue, ...concerns }` | Form fields with merged concerns |
494
+ | `useJitStore()` | `{ proxyValue, setChanges, getState }` | Bulk updates, non-reactive reads |
495
+ | `useConcerns(id, config)` | `void` | Register validation/BoolLogic/dynamic text |
496
+ | `useSideEffects(id, config)` | `void` | Register sync/flip/aggregation/listeners |
497
+ | `useFieldConcerns(path)` | `EvaluatedConcerns` | Read concern results for a path |
498
+ | `withConcerns(selection)` | `{ useFieldStore }` | Scoped field store with selected concerns |
499
+
500
+ ### Built-in Concerns
501
+
502
+ | Concern | Returns | Config |
503
+ |---|---|---|
504
+ | `validationState` | `{ isError, errors[] }` | `{ schema: ZodSchema }` |
505
+ | `disabledWhen` | `boolean` | `{ boolLogic: BoolLogic }` |
506
+ | `visibleWhen` | `boolean` | `{ boolLogic: BoolLogic }` |
507
+ | `readonlyWhen` | `boolean` | `{ boolLogic: BoolLogic }` |
508
+ | `dynamicLabel` | `string` | `{ template: '{{path}}' }` |
509
+ | `dynamicTooltip` | `string` | `{ template: '{{path}}' }` |
510
+ | `dynamicPlaceholder` | `string` | `{ template: '{{path}}' }` |
511
+
512
+ ### BoolLogic Operators
513
+
514
+ `IS_EQUAL`, `EXISTS`, `IS_EMPTY`, `GT`, `LT`, `GTE`, `LTE`, `IN`, `AND`, `OR`, `NOT`
515
+
516
+ ## Architecture
517
+
518
+ ```
519
+ setValue("email", "alice@example.com")
520
+ |
521
+ +--[WASM/Rust]--> shadow state + sync + flip + BoolLogic (Rust)
522
+ | |
523
+ | v
524
+ | execute listeners + Zod validators (JS)
525
+ | |
526
+ | v
527
+ | pipelineFinalize -> diff -> final changes (Rust)
528
+ |
529
+ +--[Legacy JS]--> sync -> flip -> listeners -> applyBatch
530
+ |
531
+ v
532
+ valtio proxy -> React re-render
533
+ ```
534
+
535
+ **Dual-layer design:** JS/React owns reactivity and rendering. Rust/WASM owns heavy computation (graphs, diffing, pipeline orchestration). The boundary is thin: paths cross as strings, values as JSON. WASM decides the execution plan, JS executes user functions.
536
+
537
+ See [WASM Architecture](docs/WASM_ARCHITECTURE.md) for the full specification.
186
538
 
187
539
  ## Development
188
540
 
@@ -198,7 +550,6 @@ npm run wasm:check # Rust lint + check
198
550
  ### WASM Prerequisites
199
551
 
200
552
  ```bash
201
- # Rust toolchain
202
553
  curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
203
554
  rustup target add wasm32-unknown-unknown
204
555
  cargo install wasm-pack
@@ -208,22 +559,22 @@ cargo install wasm-pack
208
559
 
209
560
  | Document | Covers |
210
561
  |---|---|
211
- | [WASM Architecture](docs/WASM_ARCHITECTURE.md) | JS/WASM boundary, data flow, ownership model |
212
- | [Benchmark Comparison](docs/BENCHMARK_COMPARISON.md) | Legacy vs WASM performance with 16 scenarios |
562
+ | [Store & Hooks](docs/guides/STORE_HOOKS.md) | Hook reference, composable hooks, patterns |
213
563
  | [Concerns Guide](docs/guides/CONCERNS_GUIDE.md) | Concern lifecycle, built-ins, custom concerns |
214
564
  | [Side Effects Guide](docs/SIDE_EFFECTS_GUIDE.md) | Sync, flip, aggregation, listener API |
215
- | [Store & Hooks](docs/guides/STORE_HOOKS.md) | Hook reference and patterns |
216
- | [Debug Timing](docs/DEBUG_TIMING.md) | Performance debugging utilities |
217
- | [Wildcard Paths](docs/WILD_FUNCTION_GUIDE.md) | `Wild()` template utility for Record types |
218
- | [Record Migration](docs/RECORD_MIGRATION.md) | Migration patterns for dynamic Record types |
565
+ | [WASM Architecture](docs/WASM_ARCHITECTURE.md) | JS/WASM boundary, data flow, ownership model |
566
+ | [Benchmark Comparison](docs/BENCHMARK_COMPARISON.md) | Legacy vs WASM across 16 scenarios |
567
+ | [Wildcard Paths](docs/WILD_FUNCTION_GUIDE.md) | `_()` hash key utility for Record types |
568
+ | [String Interpolation](docs/INTERPOLATION.md) | Template helpers for dynamic text concerns |
219
569
  | [Testing Mock](docs/TESTING_MOCK.md) | Mock module for consumer tests (`vi.mock`) |
570
+ | [Record Migration](docs/RECORD_MIGRATION.md) | Migration patterns for dynamic Record types |
571
+ | [Debug Timing](docs/DEBUG_TIMING.md) | Performance debugging utilities |
220
572
  | [Full Index](docs/README.md) | Complete documentation index |
221
573
 
222
574
  ## Roadmap
223
575
 
224
- - **Aggregation modes** — Aggregations currently use consensus (all-equal) mode. Planned: `SUM`, `AVG`, `COUNT`, `MIN`, `MAX`, and custom reducer functions, declared per-target alongside the source pairs.
225
- - **Nested sub-stores** — Allow a parent store to contain child stores, enabling component-level state that participates in the parent's pipeline (concerns, listeners, sync).
226
- - **Technical debt resolution** — See [TECHNICAL_DEBT.md](TECHNICAL_DEBT.md) for tracked items.
576
+ - **Aggregation modes** — Planned: `SUM`, `AVG`, `COUNT`, `MIN`, `MAX`, and custom reducer functions (currently consensus mode only).
577
+ - **Nested sub-stores** — Component-level state that participates in the parent's pipeline.
227
578
 
228
579
  ## License
229
580