@sladg/apex-state 3.0.1 → 3.0.2
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 +228 -0
- package/dist/index.js +26 -17
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# @sladg/apex-state
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @sladg/apex-state valtio zod react
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Example
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { createGenericStore } from '@sladg/apex-state'
|
|
13
|
+
import { z } from 'zod'
|
|
14
|
+
|
|
15
|
+
type OrderState = {
|
|
16
|
+
product: { name: string; quantity: number; price: number }
|
|
17
|
+
shipping: { address: string; express: boolean }
|
|
18
|
+
payment: { method: 'card' | 'cash'; cardNumber: string }
|
|
19
|
+
status: 'draft' | 'submitted'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const store = createGenericStore<OrderState>()
|
|
23
|
+
|
|
24
|
+
const OrderForm = () => {
|
|
25
|
+
// Declare side effects
|
|
26
|
+
store.useSideEffects('order', {
|
|
27
|
+
syncPaths: [['product.price', 'shipping.basePrice']],
|
|
28
|
+
flipPaths: [['shipping.express', 'shipping.standard']],
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Declare concerns — just data, no logic to test
|
|
32
|
+
store.useConcerns('order', {
|
|
33
|
+
'product.quantity': {
|
|
34
|
+
validationState: { schema: z.number().min(1).max(100) },
|
|
35
|
+
disabledWhen: { condition: { IS_EQUAL: ['status', 'submitted'] } },
|
|
36
|
+
},
|
|
37
|
+
'payment.cardNumber': {
|
|
38
|
+
validationState: { schema: z.string().regex(/^\d{16}$/) },
|
|
39
|
+
visibleWhen: { condition: { IS_EQUAL: ['payment.method', 'card'] } },
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const { value, setValue, validationState, disabledWhen } =
|
|
44
|
+
store.useFieldStore('product.quantity')
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<input
|
|
48
|
+
type="number"
|
|
49
|
+
value={value}
|
|
50
|
+
onChange={(e) => setValue(Number(e.target.value))}
|
|
51
|
+
disabled={disabledWhen}
|
|
52
|
+
className={validationState?.isError ? 'error' : ''}
|
|
53
|
+
/>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const App = () => (
|
|
58
|
+
<store.Provider initialState={{
|
|
59
|
+
product: { name: 'Widget', quantity: 1, price: 29.99 },
|
|
60
|
+
shipping: { address: '', express: false },
|
|
61
|
+
payment: { method: 'card', cardNumber: '' },
|
|
62
|
+
status: 'draft',
|
|
63
|
+
}}>
|
|
64
|
+
<OrderForm />
|
|
65
|
+
</store.Provider>
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Features
|
|
70
|
+
|
|
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
|
+
|
|
80
|
+
## Architecture
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
setValue("email", "alice@example.com")
|
|
84
|
+
│
|
|
85
|
+
├─[Legacy JS]──▶ sync → flip → listeners → applyBatch
|
|
86
|
+
│
|
|
87
|
+
└─[WASM/Rust]──▶ shadow state + sync + flip + BoolLogic (Rust)
|
|
88
|
+
│
|
|
89
|
+
▼
|
|
90
|
+
execute listeners + Zod validators (JS)
|
|
91
|
+
│
|
|
92
|
+
▼
|
|
93
|
+
pipelineFinalize → diff → final changes (Rust)
|
|
94
|
+
│
|
|
95
|
+
▼
|
|
96
|
+
valtio proxy → React re-render
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**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.
|
|
100
|
+
|
|
101
|
+
See [docs/WASM_ARCHITECTURE.md](docs/WASM_ARCHITECTURE.md) for the full specification.
|
|
102
|
+
|
|
103
|
+
## WASM Mode
|
|
104
|
+
|
|
105
|
+
WASM is the default. Pass `{ useLegacyImplementation: true }` for pure JS:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
const store = createGenericStore<MyState>() // WASM (default)
|
|
109
|
+
const store = createGenericStore<MyState>({ useLegacyImplementation: true }) // Legacy JS
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Performance
|
|
113
|
+
|
|
114
|
+
Benchmarked with 60 variants across 3 Record layers, 75 syncs, 40 flips, 100 BoolLogic conditions, 85 listeners:
|
|
115
|
+
|
|
116
|
+
| Operation | Legacy | WASM | Winner |
|
|
117
|
+
|---|---|---|---|
|
|
118
|
+
| Single field edit | **0.5us** | 1.4us | Legacy 2.6x |
|
|
119
|
+
| 7 changes + cascading listeners | 41.8ms | **0.11ms** | WASM 367x |
|
|
120
|
+
| 60 bulk price changes | 596ms | **2.9ms** | WASM 207x |
|
|
121
|
+
| 135 changes (full catalog refresh) | 621ms | **2.99ms** | WASM 208x |
|
|
122
|
+
|
|
123
|
+
Both modes produce **identical state** — verified across all 16 benchmark scenarios. See [docs/BENCHMARK_COMPARISON.md](docs/BENCHMARK_COMPARISON.md) for the full analysis.
|
|
124
|
+
|
|
125
|
+
### Why WASM is faster
|
|
126
|
+
|
|
127
|
+
- **Pre-computed topic routing** — listener dispatch is O(1) lookup vs O(changes x listeners) string matching
|
|
128
|
+
- **Shadow state diffing** — fast Rust HashMap vs valtio Proxy trap overhead
|
|
129
|
+
- **Single-pass pipeline** — aggregation + sync + flip + BoolLogic in one Rust call
|
|
130
|
+
- **BoolLogic in pipeline** — evaluated in Rust before listeners fire; Legacy defers to async `effect()`
|
|
131
|
+
|
|
132
|
+
### Why Legacy is faster for small ops
|
|
133
|
+
|
|
134
|
+
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.
|
|
135
|
+
|
|
136
|
+
## API Quick Reference
|
|
137
|
+
|
|
138
|
+
### Store
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
const {
|
|
142
|
+
Provider, // React context — accepts initialState
|
|
143
|
+
useFieldStore, // { value, setValue, ...concerns } for a path
|
|
144
|
+
useStore, // [value, setValue] tuple for a path
|
|
145
|
+
useJitStore, // { proxyValue, setChanges, getState } for bulk ops
|
|
146
|
+
useSideEffects, // register sync/flip/aggregation/listeners
|
|
147
|
+
useConcerns, // register validation/BoolLogic/custom concerns
|
|
148
|
+
withConcerns, // typed concern selection
|
|
149
|
+
} = createGenericStore<MyState>(config?)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Concerns
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
useConcerns('id', {
|
|
156
|
+
'user.email': {
|
|
157
|
+
validationState: { schema: z.string().email() },
|
|
158
|
+
disabledWhen: { condition: { IS_EQUAL: ['tosAccepted', false] } },
|
|
159
|
+
visibleWhen: { condition: { AND: [{ EXISTS: 'user.name' }, { IS_EQUAL: ['step', 2] }] } },
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Built-in concerns: `validationState`, `disabledWhen`, `readonlyWhen`, `visibleWhen`, `dynamicLabel`, `dynamicTooltip`, `dynamicPlaceholder`.
|
|
165
|
+
|
|
166
|
+
BoolLogic operators: `IS_EQUAL`, `EXISTS`, `IS_EMPTY`, `GT`, `LT`, `GTE`, `LTE`, `IN`, `AND`, `OR`, `NOT`.
|
|
167
|
+
|
|
168
|
+
See [Concerns Guide](docs/guides/CONCERNS_GUIDE.md) for lifecycle, custom concerns, and testing.
|
|
169
|
+
|
|
170
|
+
### Side Effects
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
useSideEffects('id', {
|
|
174
|
+
syncPaths: [['source', 'target']],
|
|
175
|
+
flipPaths: [['active', 'inactive']],
|
|
176
|
+
// Aggregation: target reflects the common value when all sources agree, null otherwise.
|
|
177
|
+
// Multiple pairs with the same target form a group.
|
|
178
|
+
// Currently supports consensus (all-equal) mode only — SUM, AVG, COUNT planned (see Roadmap).
|
|
179
|
+
aggregations: [['summary.price', 'legs.0.price'], ['summary.price', 'legs.1.price']],
|
|
180
|
+
listeners: [{ path: 'orders', scope: 'orders', fn: handler }],
|
|
181
|
+
})
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
See [Side Effects Guide](docs/SIDE_EFFECTS_GUIDE.md) for the full API.
|
|
185
|
+
|
|
186
|
+
## Development
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
npm install # Install dependencies
|
|
190
|
+
npm run wasm:build # Compile Rust -> WASM
|
|
191
|
+
npm run build # Bundle TypeScript + WASM
|
|
192
|
+
npm run test # Run tests
|
|
193
|
+
npm run code:check # Lint + type check
|
|
194
|
+
npm run wasm:check # Rust lint + check
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### WASM Prerequisites
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
# Rust toolchain
|
|
201
|
+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
|
202
|
+
rustup target add wasm32-unknown-unknown
|
|
203
|
+
cargo install wasm-pack
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Documentation
|
|
207
|
+
|
|
208
|
+
| Document | Covers |
|
|
209
|
+
|---|---|
|
|
210
|
+
| [WASM Architecture](docs/WASM_ARCHITECTURE.md) | JS/WASM boundary, data flow, ownership model |
|
|
211
|
+
| [Benchmark Comparison](docs/BENCHMARK_COMPARISON.md) | Legacy vs WASM performance with 16 scenarios |
|
|
212
|
+
| [Concerns Guide](docs/guides/CONCERNS_GUIDE.md) | Concern lifecycle, built-ins, custom concerns |
|
|
213
|
+
| [Side Effects Guide](docs/SIDE_EFFECTS_GUIDE.md) | Sync, flip, aggregation, listener API |
|
|
214
|
+
| [Store & Hooks](docs/guides/STORE_HOOKS.md) | Hook reference and patterns |
|
|
215
|
+
| [Debug Timing](docs/DEBUG_TIMING.md) | Performance debugging utilities |
|
|
216
|
+
| [Wildcard Paths](docs/WILD_FUNCTION_GUIDE.md) | `Wild()` template utility for Record types |
|
|
217
|
+
| [Record Migration](docs/RECORD_MIGRATION.md) | Migration patterns for dynamic Record types |
|
|
218
|
+
| [Full Index](docs/README.md) | Complete documentation index |
|
|
219
|
+
|
|
220
|
+
## Roadmap
|
|
221
|
+
|
|
222
|
+
- **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.
|
|
223
|
+
- **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).
|
|
224
|
+
- **Technical debt resolution** — See [TECHNICAL_DEBT.md](TECHNICAL_DEBT.md) for tracked items.
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT
|