@uistate/core 5.4.0 → 5.5.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 +54 -387
- package/package.json +5 -7
- package/LICENSE-eventTest.md +0 -26
- package/eventTest.js +0 -196
package/README.md
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @uistate/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Path-based state management with wildcard subscriptions and async support.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@uistate/core)
|
|
6
|
-
[](./LICENSE)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
## What's New in v5
|
|
11
|
-
|
|
12
|
-
- **EventState Core**: Path-based subscriptions with wildcard support (~80 LOC)
|
|
13
|
-
- **Slot Orchestration**: Atomic component swapping without layout shift
|
|
14
|
-
- **Intent-Driven**: Declarative command handling for decoupled architecture
|
|
15
|
-
- **Zero Dependencies**: Pure JavaScript, works in any modern browser
|
|
16
|
-
- **Framework-Free**: No build step required (but works great with Vite)
|
|
17
|
-
|
|
18
|
-
## Installation
|
|
8
|
+
## Install
|
|
19
9
|
|
|
20
10
|
```bash
|
|
21
11
|
npm install @uistate/core
|
|
@@ -26,424 +16,101 @@ npm install @uistate/core
|
|
|
26
16
|
```javascript
|
|
27
17
|
import { createEventState } from '@uistate/core';
|
|
28
18
|
|
|
29
|
-
const store = createEventState({
|
|
30
|
-
user: { name: 'Alice' },
|
|
31
|
-
count: 0
|
|
32
|
-
});
|
|
19
|
+
const store = createEventState({ user: { name: 'Alice' }, count: 0 });
|
|
33
20
|
|
|
34
|
-
//
|
|
35
|
-
store.subscribe('
|
|
36
|
-
console.log('Name changed:', value);
|
|
37
|
-
});
|
|
21
|
+
// Exact path
|
|
22
|
+
store.subscribe('count', (value) => console.log('Count:', value));
|
|
38
23
|
|
|
39
|
-
//
|
|
40
|
-
store.subscribe('user.*', ({ path, value }) =>
|
|
41
|
-
|
|
42
|
-
|
|
24
|
+
// Wildcard — fires on any child of 'user'
|
|
25
|
+
store.subscribe('user.*', ({ path, value }) => console.log(path, value));
|
|
26
|
+
|
|
27
|
+
// Global — fires on every change
|
|
28
|
+
store.subscribe('*', ({ path, value }) => console.log(path, value));
|
|
43
29
|
|
|
44
|
-
|
|
45
|
-
store.set('user.name', 'Bob'); // → Name changed: Bob
|
|
30
|
+
store.set('user.name', 'Bob');
|
|
46
31
|
store.set('count', 1);
|
|
47
32
|
```
|
|
48
33
|
|
|
49
|
-
##
|
|
50
|
-
|
|
51
|
-
### `createEventState(initial?)`
|
|
34
|
+
## API
|
|
52
35
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
```javascript
|
|
56
|
-
const store = createEventState({ count: 0 });
|
|
57
|
-
```
|
|
36
|
+
### `createEventState(initialState)`
|
|
58
37
|
|
|
59
|
-
|
|
38
|
+
Returns a store with `get`, `set`, `subscribe`, `setAsync`, `cancel`, `destroy`.
|
|
60
39
|
|
|
61
|
-
|
|
40
|
+
### `store.get(path?)`
|
|
62
41
|
|
|
63
42
|
```javascript
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
43
|
+
store.get('user.name'); // 'Alice'
|
|
44
|
+
store.get('user'); // { name: 'Alice' }
|
|
45
|
+
store.get(); // entire state
|
|
67
46
|
```
|
|
68
47
|
|
|
69
48
|
### `store.set(path, value)`
|
|
70
49
|
|
|
71
|
-
Sets a value and
|
|
50
|
+
Sets a value and notifies subscribers. Creates intermediate objects if needed.
|
|
72
51
|
|
|
73
52
|
```javascript
|
|
74
|
-
store.set('count', 42);
|
|
75
53
|
store.set('user.email', 'alice@example.com');
|
|
54
|
+
// state.user.email now exists even if it didn't before
|
|
76
55
|
```
|
|
77
56
|
|
|
78
57
|
### `store.subscribe(path, handler)`
|
|
79
58
|
|
|
80
|
-
|
|
59
|
+
Returns an unsubscribe function. Exact subscribers get `(value, detail)`. Wildcard/global subscribers get `(detail)` where detail is `{ path, value, oldValue }`.
|
|
81
60
|
|
|
82
61
|
```javascript
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
console.log('Count:', value);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// Wildcard (child changes)
|
|
89
|
-
store.subscribe('user.*', ({ path, value }) => {
|
|
90
|
-
console.log(`User property ${path} changed`);
|
|
62
|
+
const unsub = store.subscribe('count', (value, { oldValue }) => {
|
|
63
|
+
console.log(`${oldValue} → ${value}`);
|
|
91
64
|
});
|
|
92
|
-
|
|
93
|
-
// Global wildcard (all changes)
|
|
94
|
-
store.subscribe('*', ({ path, value }) => {
|
|
95
|
-
console.log('Something changed');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// Cleanup
|
|
99
|
-
unsub();
|
|
65
|
+
unsub(); // cleanup
|
|
100
66
|
```
|
|
101
67
|
|
|
102
|
-
### `store.
|
|
103
|
-
|
|
104
|
-
Cleans up the event bus and prevents further updates.
|
|
105
|
-
|
|
106
|
-
```javascript
|
|
107
|
-
store.destroy();
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
# Changelog
|
|
111
|
-
|
|
112
|
-
## [5.0.0] - 2025-12-09
|
|
68
|
+
### `store.setAsync(path, fetcher)`
|
|
113
69
|
|
|
114
|
-
|
|
115
|
-
- eventState is now primary export (was cssState)
|
|
116
|
-
- DOM element event bus replaced with EventTarget
|
|
117
|
-
- ...
|
|
118
|
-
|
|
119
|
-
### Added
|
|
120
|
-
- Slot orchestration pattern
|
|
121
|
-
- Intent-driven architecture
|
|
122
|
-
- Event-sequence testing (experimental)
|
|
123
|
-
- Example 009: Financial dashboard
|
|
124
|
-
- Example 010: Event testing
|
|
125
|
-
|
|
126
|
-
## Architecture Patterns
|
|
127
|
-
|
|
128
|
-
UIstate v5 introduces battle-tested patterns from many experiments:
|
|
129
|
-
|
|
130
|
-
### 1. **Intent-Driven Updates** (Declarative Commands)
|
|
131
|
-
|
|
132
|
-
Intents are just regular state paths—no special API needed. Components subscribe to intent paths and execute logic when triggered:
|
|
70
|
+
Manages async state at `${path}.status`, `${path}.data`, `${path}.error`. Supports abort on re-call.
|
|
133
71
|
|
|
134
72
|
```javascript
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
store.set('todos.items', [...items, { id: Date.now(), text, done: false }]);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// Trigger intent from UI by setting the path
|
|
142
|
-
button.addEventListener('click', () => {
|
|
143
|
-
store.set('intent.todo.add', { text: input.value });
|
|
73
|
+
await store.setAsync('users', async (signal) => {
|
|
74
|
+
const res = await fetch('/api/users', { signal });
|
|
75
|
+
return res.json();
|
|
144
76
|
});
|
|
77
|
+
// store.get('users.data') → [...]
|
|
78
|
+
// store.get('users.status') → 'success'
|
|
145
79
|
```
|
|
146
80
|
|
|
147
|
-
|
|
148
|
-
- Subscribe once at component mount
|
|
149
|
-
- Multiple components can listen to the same intent
|
|
150
|
-
- Easy to test (just `set()` the intent path)
|
|
151
|
-
- No special event emitter needed
|
|
152
|
-
|
|
153
|
-
### 2. **Slot Orchestration** (Flicker-free component loading)
|
|
154
|
-
|
|
155
|
-
Load layouts and components separately for atomic, layout-shift-free updates:
|
|
156
|
-
|
|
157
|
-
```javascript
|
|
158
|
-
// 1. Mount persistent layout once
|
|
159
|
-
async function mountMaster(master) {
|
|
160
|
-
const layout = await fetchHTML(master.layout);
|
|
161
|
-
document.getElementById('app').innerHTML = layout;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// 2. Load components into slots atomically
|
|
165
|
-
async function loadSlot({ slot, url }) {
|
|
166
|
-
const config = await fetchJSON(url);
|
|
167
|
-
const html = await fetchHTML(config.component);
|
|
168
|
-
|
|
169
|
-
// Find slot container
|
|
170
|
-
const slotHost = document.querySelector(`[data-slot="${slot}"]`);
|
|
171
|
-
|
|
172
|
-
// Atomic replacement - no layout shift!
|
|
173
|
-
const fragment = createFragment(html);
|
|
174
|
-
slotHost.replaceChildren(fragment);
|
|
175
|
-
|
|
176
|
-
// Wire up component subscriptions
|
|
177
|
-
wireComponent(slotHost, config.bindings);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Trigger slot loading via intent
|
|
181
|
-
store.subscribe('intent.slot.load', loadSlot);
|
|
182
|
-
store.set('intent.slot.load', { slot: 'main', url: '/slots/todo.json' });
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
**Key insight**: Separate layout (skeleton) from content (slots):
|
|
186
|
-
- Layout provides stable structure
|
|
187
|
-
- Slots swap atomically without reflow
|
|
188
|
-
- Each component manages its own subscriptions
|
|
189
|
-
- Clean separation of concerns
|
|
190
|
-
|
|
191
|
-
### 3. **Component Bindings** (Path-based reactivity)
|
|
192
|
-
|
|
193
|
-
Components subscribe to specific state paths, not global wildcards:
|
|
194
|
-
|
|
195
|
-
```javascript
|
|
196
|
-
function wireComponent(root, bindings) {
|
|
197
|
-
const input = root.querySelector('#input');
|
|
198
|
-
const list = root.querySelector('#list');
|
|
199
|
-
const itemsPath = bindings.items || 'domain.items';
|
|
200
|
-
|
|
201
|
-
// Subscribe to specific path for this component
|
|
202
|
-
const unsubscribe = store.subscribe(itemsPath, (items) => {
|
|
203
|
-
list.replaceChildren();
|
|
204
|
-
items.forEach(item => {
|
|
205
|
-
const li = document.createElement('li');
|
|
206
|
-
li.textContent = item.text;
|
|
207
|
-
list.appendChild(li);
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// Initial render
|
|
212
|
-
const items = store.get(itemsPath);
|
|
213
|
-
if (items) store.set(itemsPath, items);
|
|
214
|
-
|
|
215
|
-
// Return cleanup function
|
|
216
|
-
return unsubscribe;
|
|
217
|
-
}
|
|
218
|
-
```
|
|
81
|
+
### `store.cancel(path)` / `store.destroy()`
|
|
219
82
|
|
|
220
|
-
|
|
221
|
-
- No global `*` wildcards in production code
|
|
222
|
-
- Components are isolated and testable
|
|
223
|
-
- Easy cleanup when unmounting
|
|
224
|
-
- Clear data dependencies
|
|
83
|
+
Cancel an in-flight async operation, or tear down the entire store.
|
|
225
84
|
|
|
226
|
-
|
|
85
|
+
## Query Client
|
|
227
86
|
|
|
228
|
-
|
|
87
|
+
Convenience wrapper around `setAsync` for data-fetching patterns.
|
|
229
88
|
|
|
230
89
|
```javascript
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
// Load and mount new component
|
|
241
|
-
const config = await fetchJSON(url);
|
|
242
|
-
const html = await fetchHTML(config.component);
|
|
243
|
-
const slotHost = document.querySelector(`[data-slot="${slot}"]`);
|
|
244
|
-
slotHost.replaceChildren(createFragment(html));
|
|
245
|
-
|
|
246
|
-
// Wire up with cleanup function
|
|
247
|
-
const cleanup = wireComponent(slotHost, config.bindings);
|
|
248
|
-
|
|
249
|
-
// Store for later cleanup
|
|
250
|
-
componentRegistry.set(slot, { cleanup });
|
|
251
|
-
}
|
|
90
|
+
import { createQueryClient } from '@uistate/core/query';
|
|
91
|
+
|
|
92
|
+
const qc = createQueryClient(store);
|
|
93
|
+
await qc.query('users', (signal) => fetch('/api/users', { signal }).then(r => r.json()));
|
|
94
|
+
qc.getData('users');
|
|
95
|
+
qc.getStatus('users');
|
|
96
|
+
qc.cancel('users');
|
|
97
|
+
qc.invalidate('users');
|
|
252
98
|
```
|
|
253
99
|
|
|
254
|
-
|
|
255
|
-
- Prevents memory leaks
|
|
256
|
-
- Allows safe component swapping
|
|
257
|
-
- Components are truly hot-swappable
|
|
258
|
-
- No zombie subscriptions
|
|
100
|
+
## Ecosystem
|
|
259
101
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
// ...existing button wiring...
|
|
267
|
-
|
|
268
|
-
// Layout adapts based on which slot is active
|
|
269
|
-
store.subscribe('ui.view.active', (active) => {
|
|
270
|
-
const kpiCol = root.querySelector('[data-slot="kpi"]').closest('.col-12');
|
|
271
|
-
const newsCol = root.querySelector('[data-slot="news"]').closest('.col-12');
|
|
272
|
-
const positionsCol = root.querySelector('[data-slot="positions"]').closest('.col-12');
|
|
273
|
-
|
|
274
|
-
if (active === 'todos') {
|
|
275
|
-
// Full-width editing mode
|
|
276
|
-
kpiCol.classList.add('d-none');
|
|
277
|
-
newsCol.classList.add('d-none');
|
|
278
|
-
positionsCol.classList.remove('col-md-6', 'col-lg-4');
|
|
279
|
-
positionsCol.classList.add('col-md-12', 'col-lg-8');
|
|
280
|
-
} else {
|
|
281
|
-
// Multi-column dashboard mode
|
|
282
|
-
kpiCol.classList.remove('d-none');
|
|
283
|
-
newsCol.classList.remove('d-none');
|
|
284
|
-
positionsCol.classList.remove('col-md-12', 'col-lg-8');
|
|
285
|
-
positionsCol.classList.add('col-md-6', 'col-lg-4');
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
**Key insight**: Content configures layout, not the other way around:
|
|
292
|
-
- One layout HTML handles all scenarios
|
|
293
|
-
- Panels show/hide based on active content
|
|
294
|
-
- Sections expand/collapse responsively
|
|
295
|
-
- Pure CSS class manipulation - no DOM restructuring
|
|
296
|
-
- Declarative layout rules via state subscription
|
|
297
|
-
|
|
298
|
-
**Real-world applications**:
|
|
299
|
-
```javascript
|
|
300
|
-
// E-commerce: hide sidebar, expand product grid
|
|
301
|
-
if (active === 'products') hideSlots(['filters', 'cart']); expandSlot('grid');
|
|
302
|
-
|
|
303
|
-
// Admin: hide navigation, expand table
|
|
304
|
-
if (active === 'report') hideSlots(['nav', 'sidebar']); expandSlot('table');
|
|
305
|
-
|
|
306
|
-
// Editor: focus mode - hide everything except content
|
|
307
|
-
if (active === 'editor') hideSlots(['toolbar', 'preview']); expandSlot('content');
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
This pattern eliminates the "rigid layout" criticism - UIstate layouts are as flexible as any framework, with zero VDOM overhead.
|
|
311
|
-
|
|
312
|
-
## Complete Example
|
|
313
|
-
|
|
314
|
-
See `examples/009-financial-dashboard` for a real-world application featuring:
|
|
315
|
-
|
|
316
|
-
- Multi-slot dashboard layout
|
|
317
|
-
- TodoList with filtering
|
|
318
|
-
- Financial data tables (KPI, Positions, Debts, Sales, News)
|
|
319
|
-
- Intent-driven architecture
|
|
320
|
-
- Atomic slot swapping
|
|
321
|
-
- State inspector
|
|
322
|
-
|
|
323
|
-
Run the example:
|
|
324
|
-
|
|
325
|
-
```bash
|
|
326
|
-
cd node_modules/@uistate/core/examples/009-financial-dashboard
|
|
327
|
-
npx serve .
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
Open http://localhost:3000 and explore the multi-slot dashboard!
|
|
331
|
-
|
|
332
|
-
## Why UIstate v5?
|
|
333
|
-
|
|
334
|
-
### vs. React/Vue
|
|
335
|
-
|
|
336
|
-
- ✅ **No VDOM overhead** - Direct DOM manipulation with surgical updates
|
|
337
|
-
- ✅ **No build step required** - Works in browsers directly
|
|
338
|
-
- ✅ **Simpler mental model** - Paths + subscriptions, not components + lifecycle hooks
|
|
339
|
-
- ✅ **Smaller bundle** - ~3KB core vs ~40KB+ frameworks
|
|
340
|
-
- ✅ **Explicit reactivity** - Know exactly what triggers what
|
|
341
|
-
- ❌ No component ecosystem
|
|
342
|
-
- ❌ Manual DOM updates (but that's the point!)
|
|
343
|
-
|
|
344
|
-
### vs. Alpine.js
|
|
345
|
-
|
|
346
|
-
- ✅ **Better for complex state** - Nested paths, cross-component communication
|
|
347
|
-
- ✅ **First-class SPA patterns** - Slot orchestration, intent-driven updates
|
|
348
|
-
- ✅ **More powerful subscriptions** - Path-based, wildcard support, granular control
|
|
349
|
-
- ✅ **Explicit data flow** - No magic, clear cause and effect
|
|
350
|
-
- ❌ More JavaScript required (Alpine is more declarative HTML)
|
|
351
|
-
|
|
352
|
-
### vs. Zustand/Jotai
|
|
353
|
-
|
|
354
|
-
- ✅ **Framework-agnostic** - Not tied to React
|
|
355
|
-
- ✅ **Path-based state** - More intuitive for deeply nested data
|
|
356
|
-
- ✅ **Wildcard subscriptions** - Subscribe to entire subtrees when needed
|
|
357
|
-
- ✅ **Simpler API** - Just get/set/subscribe, no atoms or stores
|
|
358
|
-
- ❌ Smaller ecosystem
|
|
359
|
-
- ❌ No React DevTools integration
|
|
360
|
-
|
|
361
|
-
## Use Cases
|
|
362
|
-
|
|
363
|
-
UIstate v5 excels at:
|
|
364
|
-
|
|
365
|
-
- ✅ **Internal dashboards** - Complex state, multiple data sources
|
|
366
|
-
- ✅ **Admin panels** - CRUD operations, forms, data tables
|
|
367
|
-
- ✅ **Financial applications** - Real-time updates, calculations
|
|
368
|
-
- ✅ **Data-heavy UIs** - Large datasets, filtered views
|
|
369
|
-
- ✅ **Multi-pane interfaces** - Independent but coordinated components
|
|
370
|
-
- ✅ **Progressive enhancement** - Add interactivity to server-rendered HTML
|
|
371
|
-
|
|
372
|
-
## Browser Support
|
|
373
|
-
|
|
374
|
-
- Chrome 60+
|
|
375
|
-
- Firefox 54+
|
|
376
|
-
- Safari 10.1+
|
|
377
|
-
- Edge 79+
|
|
378
|
-
|
|
379
|
-
## Migration from v4
|
|
380
|
-
|
|
381
|
-
UIstate v5 shifts focus from CSS-driven state to event-driven state. Key changes:
|
|
382
|
-
|
|
383
|
-
1. **eventState is now primary** - Import from root or `/eventState`
|
|
384
|
-
2. **cssState still available** - Import from `/cssState`
|
|
385
|
-
3. **New examples** - See `009-financial-dashboard`
|
|
386
|
-
4. **Breaking changes** - Major version bump
|
|
387
|
-
|
|
388
|
-
## Philosophy
|
|
389
|
-
|
|
390
|
-
UIstate challenges traditional assumptions:
|
|
391
|
-
|
|
392
|
-
- **State should be simple** - Paths + subscriptions, not selectors + reducers
|
|
393
|
-
- **Reactivity should be explicit** - Know what updates what
|
|
394
|
-
- **DOM updates can be fast** - Atomic replacements beat VDOM for many use cases
|
|
395
|
-
- **Components should own their subscriptions** - No global wildcards in production
|
|
396
|
-
- **Frameworks aren't always needed** - Pure JS + patterns can go far
|
|
397
|
-
- **Build steps should be optional** - ESM works in browsers
|
|
398
|
-
- **Intents are just paths** - No special event system needed
|
|
399
|
-
|
|
400
|
-
UIstate v5 represents lessons learned from building:
|
|
401
|
-
- Data tables with 1M+ rows
|
|
402
|
-
- Multi-tab synchronized state
|
|
403
|
-
- Workflowy-style nested lists
|
|
404
|
-
- Financial dashboards
|
|
405
|
-
- Admin panels
|
|
406
|
-
|
|
407
|
-
The core insight: **Most web UIs need reactive state and component orchestration, not a full framework.**
|
|
408
|
-
|
|
409
|
-
## Testing (Experimental)
|
|
410
|
-
|
|
411
|
-
UIstate includes `eventTest.js` for event-sequence testing:
|
|
412
|
-
|
|
413
|
-
**Note:** `eventTest.js` is dual-licensed. Free for personal/OSS use. Commercial use requires a separate license. See LICENSE-eventTest.md for details.
|
|
414
|
-
|
|
415
|
-
```javascript
|
|
416
|
-
import { createEventTest } from '@uistate/core/eventTest';
|
|
417
|
-
|
|
418
|
-
const test = createEventTest({ count: 0 });
|
|
419
|
-
|
|
420
|
-
test
|
|
421
|
-
.trigger('intent.increment')
|
|
422
|
-
.assertPath('count', 1)
|
|
423
|
-
.assertEventFired('count', 1);
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
See `examples/010-event-testing` for more.
|
|
102
|
+
| Package | Description |
|
|
103
|
+
|---|---|
|
|
104
|
+
| [@uistate/core](https://www.npmjs.com/package/@uistate/core) | This package — event-driven state management |
|
|
105
|
+
| [@uistate/css](https://www.npmjs.com/package/@uistate/css) | CSS-native state via custom properties and data attributes |
|
|
106
|
+
| [@uistate/event-test](https://www.npmjs.com/package/@uistate/event-test) | Event-sequence testing (proprietary license) |
|
|
107
|
+
| [@uistate/examples](https://www.npmjs.com/package/@uistate/examples) | Example applications and patterns |
|
|
427
108
|
|
|
428
109
|
## License
|
|
429
110
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
**Exception:** `eventTest.js` is licensed under a **proprietary license** that permits:
|
|
433
|
-
- ✅ Personal use
|
|
434
|
-
- ✅ Open-source projects
|
|
435
|
-
- ✅ Educational use
|
|
436
|
-
|
|
437
|
-
For **commercial use** of `eventTest.js`, please contact: [ajdika@live.com](mailto:ajdika@live.com)
|
|
438
|
-
|
|
439
|
-
See the file header in `eventTest.js` for full terms.
|
|
440
|
-
|
|
441
|
-
---
|
|
442
|
-
|
|
443
|
-
Copyright © 2025 Ajdin Imsirovic (ajdika@live.com)
|
|
111
|
+
MIT — see [LICENSE](./LICENSE).
|
|
444
112
|
|
|
445
|
-
|
|
446
|
-
- eventTest.js: Commercial License (free for personal/OSS)
|
|
113
|
+
Copyright © 2025 Ajdin Imsirovic
|
|
447
114
|
|
|
448
115
|
## Links
|
|
449
116
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uistate/core",
|
|
3
|
-
"version": "5.
|
|
4
|
-
"description": "Lightweight event-driven state management with
|
|
3
|
+
"version": "5.5.1",
|
|
4
|
+
"description": "Lightweight event-driven state management with path-based subscriptions, wildcards, and async support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
@@ -15,9 +15,7 @@
|
|
|
15
15
|
"eventState.js",
|
|
16
16
|
"eventStateNew.js",
|
|
17
17
|
"queryClient.js",
|
|
18
|
-
"
|
|
19
|
-
"LICENSE",
|
|
20
|
-
"LICENSE-eventTest.md"
|
|
18
|
+
"LICENSE"
|
|
21
19
|
],
|
|
22
20
|
"keywords": [
|
|
23
21
|
"state-management",
|
|
@@ -28,8 +26,8 @@
|
|
|
28
26
|
"zero-dependency",
|
|
29
27
|
"framework-free",
|
|
30
28
|
"micro-framework",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
29
|
+
"async-state",
|
|
30
|
+
"query-client"
|
|
33
31
|
],
|
|
34
32
|
"author": "Ajdin Imsirovic",
|
|
35
33
|
"license": "MIT",
|
package/LICENSE-eventTest.md
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# Proprietary License for eventTest.js
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Ajdin Imsirovic
|
|
4
|
-
|
|
5
|
-
## Permitted Uses
|
|
6
|
-
|
|
7
|
-
You may use `eventTest.js` for:
|
|
8
|
-
- Personal projects
|
|
9
|
-
- Open-source projects
|
|
10
|
-
- Educational purposes
|
|
11
|
-
|
|
12
|
-
## Restrictions
|
|
13
|
-
|
|
14
|
-
You may NOT:
|
|
15
|
-
- Use this file in commercial products without a license
|
|
16
|
-
- Modify or create derivative works
|
|
17
|
-
- Redistribute this file separately from @uistate/core
|
|
18
|
-
- Remove or alter this license notice
|
|
19
|
-
|
|
20
|
-
## Commercial Licensing
|
|
21
|
-
|
|
22
|
-
For commercial use, please contact: your@email.com
|
|
23
|
-
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED.
|
package/eventTest.js
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* eventTest.js - Event-Sequence Testing for UIstate
|
|
3
|
-
*
|
|
4
|
-
* Copyright (c) 2025 Ajdin Imsirovic
|
|
5
|
-
*
|
|
6
|
-
* This file is licensed under a PROPRIETARY LICENSE.
|
|
7
|
-
*
|
|
8
|
-
* Permission is hereby granted to USE this software for:
|
|
9
|
-
* - Personal projects
|
|
10
|
-
* - Open-source projects
|
|
11
|
-
* - Educational purposes
|
|
12
|
-
*
|
|
13
|
-
* RESTRICTIONS:
|
|
14
|
-
* - Commercial use requires a separate license (contact: your@email.com)
|
|
15
|
-
* - Modification and redistribution of this file are NOT permitted
|
|
16
|
-
* - This file may not be included in derivative works without permission
|
|
17
|
-
*
|
|
18
|
-
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
|
|
19
|
-
*
|
|
20
|
-
* For commercial licensing inquiries: your@email.com
|
|
21
|
-
*
|
|
22
|
-
* eventTest.js - Event-Sequence Testing for EventState
|
|
23
|
-
*
|
|
24
|
-
* Provides TDD-style testing with type extraction capabilities
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { createEventState } from './eventStateNew.js';
|
|
28
|
-
|
|
29
|
-
export function createEventTest(initialState = {}) {
|
|
30
|
-
const store = createEventState(initialState);
|
|
31
|
-
const eventLog = [];
|
|
32
|
-
const typeAssertions = [];
|
|
33
|
-
|
|
34
|
-
// Spy on all events
|
|
35
|
-
store.subscribe('*', (detail) => {
|
|
36
|
-
const { path, value } = detail;
|
|
37
|
-
eventLog.push({ timestamp: Date.now(), path, value });
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const api = {
|
|
41
|
-
store,
|
|
42
|
-
|
|
43
|
-
// Trigger a state change
|
|
44
|
-
trigger(path, value) {
|
|
45
|
-
store.set(path, value);
|
|
46
|
-
return this;
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
// Assert exact value
|
|
50
|
-
assertPath(path, expected) {
|
|
51
|
-
const actual = store.get(path);
|
|
52
|
-
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
53
|
-
throw new Error(`Expected ${path} to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
54
|
-
}
|
|
55
|
-
return this;
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
// Assert type (for type generation)
|
|
59
|
-
assertType(path, expectedType) {
|
|
60
|
-
const actual = store.get(path);
|
|
61
|
-
const actualType = typeof actual;
|
|
62
|
-
|
|
63
|
-
if (actualType !== expectedType) {
|
|
64
|
-
throw new Error(`Expected ${path} to be type ${expectedType}, got ${actualType}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Store for type generation
|
|
68
|
-
typeAssertions.push({ path, type: expectedType });
|
|
69
|
-
return this;
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
// Assert array with element shape (for type generation)
|
|
73
|
-
assertArrayOf(path, elementShape) {
|
|
74
|
-
const actual = store.get(path);
|
|
75
|
-
|
|
76
|
-
if (!Array.isArray(actual)) {
|
|
77
|
-
throw new Error(`Expected ${path} to be an array, got ${typeof actual}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Validate first element matches shape (if array not empty)
|
|
81
|
-
if (actual.length > 0) {
|
|
82
|
-
validateShape(actual[0], elementShape, path);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Store for type generation
|
|
86
|
-
typeAssertions.push({ path, type: 'array', elementShape });
|
|
87
|
-
return this;
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
// Assert object shape (for type generation)
|
|
91
|
-
assertShape(path, objectShape) {
|
|
92
|
-
const actual = store.get(path);
|
|
93
|
-
|
|
94
|
-
if (typeof actual !== 'object' || actual === null || Array.isArray(actual)) {
|
|
95
|
-
throw new Error(`Expected ${path} to be an object, got ${typeof actual}`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
validateShape(actual, objectShape, path);
|
|
99
|
-
|
|
100
|
-
// Store for type generation
|
|
101
|
-
typeAssertions.push({ path, type: 'object', shape: objectShape });
|
|
102
|
-
return this;
|
|
103
|
-
},
|
|
104
|
-
|
|
105
|
-
// Assert array length
|
|
106
|
-
assertArrayLength(path, expectedLength) {
|
|
107
|
-
const actual = store.get(path);
|
|
108
|
-
|
|
109
|
-
if (!Array.isArray(actual)) {
|
|
110
|
-
throw new Error(`Expected ${path} to be an array`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (actual.length !== expectedLength) {
|
|
114
|
-
throw new Error(`Expected ${path} to have length ${expectedLength}, got ${actual.length}`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return this;
|
|
118
|
-
},
|
|
119
|
-
|
|
120
|
-
// Assert event fired N times
|
|
121
|
-
assertEventFired(path, times) {
|
|
122
|
-
const count = eventLog.filter(e => e.path === path).length;
|
|
123
|
-
if (times !== undefined && count !== times) {
|
|
124
|
-
throw new Error(`Expected ${path} to fire ${times} times, fired ${count}`);
|
|
125
|
-
}
|
|
126
|
-
return this;
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
// Get event log
|
|
130
|
-
getEventLog() {
|
|
131
|
-
return [...eventLog];
|
|
132
|
-
},
|
|
133
|
-
|
|
134
|
-
// Get type assertions (for type generation)
|
|
135
|
-
getTypeAssertions() {
|
|
136
|
-
return [...typeAssertions];
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
return api;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Helper to validate object shape
|
|
144
|
-
function validateShape(actual, shape, path) {
|
|
145
|
-
for (const [key, expectedType] of Object.entries(shape)) {
|
|
146
|
-
if (!(key in actual)) {
|
|
147
|
-
throw new Error(`Expected ${path} to have property ${key}`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const actualValue = actual[key];
|
|
151
|
-
|
|
152
|
-
// Handle nested objects
|
|
153
|
-
if (typeof expectedType === 'object' && !Array.isArray(expectedType)) {
|
|
154
|
-
validateShape(actualValue, expectedType, `${path}.${key}`);
|
|
155
|
-
} else {
|
|
156
|
-
// Primitive type check
|
|
157
|
-
const actualType = typeof actualValue;
|
|
158
|
-
if (actualType !== expectedType) {
|
|
159
|
-
throw new Error(`Expected ${path}.${key} to be type ${expectedType}, got ${actualType}`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Simple test runner
|
|
166
|
-
export function test(name, fn) {
|
|
167
|
-
try {
|
|
168
|
-
fn();
|
|
169
|
-
console.log(`✓ ${name}`);
|
|
170
|
-
return true;
|
|
171
|
-
} catch (error) {
|
|
172
|
-
console.error(`✗ ${name}`);
|
|
173
|
-
console.error(` ${error.message}`);
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Run multiple tests
|
|
179
|
-
export function runTests(tests) {
|
|
180
|
-
console.log('\n🧪 Running tests...\n');
|
|
181
|
-
|
|
182
|
-
let passed = 0;
|
|
183
|
-
let failed = 0;
|
|
184
|
-
|
|
185
|
-
for (const [name, fn] of Object.entries(tests)) {
|
|
186
|
-
if (test(name, fn)) {
|
|
187
|
-
passed++;
|
|
188
|
-
} else {
|
|
189
|
-
failed++;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
console.log(`\n📊 Results: ${passed} passed, ${failed} failed\n`);
|
|
194
|
-
|
|
195
|
-
return { passed, failed };
|
|
196
|
-
}
|