@jucie.io/state 1.0.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/LICENSE +61 -0
- package/core/README.md +635 -0
- package/dist/Plugin.js +36 -0
- package/dist/State.d.ts +44 -0
- package/dist/State.js +243 -0
- package/dist/admin/binary.js +90 -0
- package/dist/admin/buffer.js +174 -0
- package/dist/admin/pack.js +67 -0
- package/dist/admin/unpack.js +88 -0
- package/dist/lib/TOKENS.js +18 -0
- package/dist/lib/change.js +94 -0
- package/dist/lib/global.js +42 -0
- package/dist/lib/gsru.js +125 -0
- package/dist/lib/marker.js +233 -0
- package/dist/lib/pathEncoder.js +89 -0
- package/dist/lib/tree/mutate.js +193 -0
- package/dist/lib/tree/seek.js +66 -0
- package/dist/lib/tree/traverse.js +38 -0
- package/dist/main.js +5 -0
- package/dist/main.js.map +7 -0
- package/dist/plugins/history.js +2 -0
- package/dist/plugins/history.js.map +7 -0
- package/dist/plugins/matcher.js +2 -0
- package/dist/plugins/matcher.js.map +7 -0
- package/dist/plugins/on-change.js +2 -0
- package/dist/plugins/on-change.js.map +7 -0
- package/dist/utils/clone.js +7 -0
- package/dist/utils/convertStringToExpression.js +23 -0
- package/dist/utils/convertStringToFunction.js +17 -0
- package/dist/utils/defer.js +24 -0
- package/dist/utils/isAsync.js +4 -0
- package/dist/utils/isPrimitive.js +12 -0
- package/dist/utils/isPromise.js +1 -0
- package/dist/utils/nextIdleTick.js +23 -0
- package/package.json +81 -0
- package/plugins/history/README.md +320 -0
- package/plugins/matcher/README.md +402 -0
- package/plugins/on-change/README.md +444 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# @jucio.io/state/matcher
|
|
2
|
+
|
|
3
|
+
Path-based state watching plugin for @jucio.io/state that allows you to watch specific paths in your state tree and react to changes. **Your handlers receive the current data at the watched path, not change objects.**
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎯 **Path Matching**: Watch specific paths in your state tree
|
|
8
|
+
- 📊 **Data Values**: Handlers receive actual data at the path, not change objects
|
|
9
|
+
- 🌳 **Hierarchical Watching**: Match exact paths, parent paths, or child paths
|
|
10
|
+
- 📦 **Automatic Batching**: Changes are automatically batched and debounced
|
|
11
|
+
- 🔄 **Smart Consolidation**: Multiple changes to the same path are consolidated
|
|
12
|
+
- 🎬 **Declarative Setup**: Define matchers during state initialization
|
|
13
|
+
- 🔌 **Plugin Architecture**: Seamlessly integrates with @jucie-state/core
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @jucio.io/state
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Note:** Matcher plugin is included in the main package.
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
import { createState } from '@jucio.io/state';
|
|
27
|
+
import { Matcher, createMatcher } from '@jucio.io/state/matcher';
|
|
28
|
+
|
|
29
|
+
// Create a matcher
|
|
30
|
+
const userMatcher = createMatcher(['user'], (userData) => {
|
|
31
|
+
console.log('User data:', userData);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Create state and install matcher plugin
|
|
35
|
+
const state = createState({
|
|
36
|
+
user: { name: 'Alice', age: 30 },
|
|
37
|
+
settings: { theme: 'dark' }
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Install with initial matchers
|
|
41
|
+
state.install(Matcher.configure({
|
|
42
|
+
matchers: [userMatcher]
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
// Change user data - matcher receives the NEW data
|
|
46
|
+
state.set(['user', 'name'], 'Bob');
|
|
47
|
+
// Console: "User data: { name: 'Bob', age: 30 }"
|
|
48
|
+
|
|
49
|
+
// Change settings - matcher doesn't fire (different path)
|
|
50
|
+
state.set(['settings', 'theme'], 'light');
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## API Reference
|
|
54
|
+
|
|
55
|
+
### Creating Matchers
|
|
56
|
+
|
|
57
|
+
#### `createMatcher(path, handler)`
|
|
58
|
+
|
|
59
|
+
Create a matcher that watches a specific path in the state tree.
|
|
60
|
+
|
|
61
|
+
**The handler receives the current data at the watched path, not change objects.**
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
import { createMatcher } from '@jucio.io/state/matcher';
|
|
65
|
+
|
|
66
|
+
const matcher = createMatcher(['users', 'profile'], (profileData) => {
|
|
67
|
+
console.log('Profile is now:', profileData);
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Parameters:**
|
|
72
|
+
- `path` (Array): Path to watch (e.g., `['user']`, `['users', 'profile']`)
|
|
73
|
+
- `handler` (Function): Callback function that receives the **current data** at the path
|
|
74
|
+
|
|
75
|
+
**Returns:** Matcher function that can be added to the plugin
|
|
76
|
+
|
|
77
|
+
### Plugin Actions
|
|
78
|
+
|
|
79
|
+
When using the Matcher plugin with a state instance, you get access to these actions:
|
|
80
|
+
|
|
81
|
+
#### `state.matcher.createMatcher(path, handler)`
|
|
82
|
+
|
|
83
|
+
Create and automatically register a matcher.
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
import { createState } from '@jucio.io/state';
|
|
87
|
+
import { Matcher } from '@jucio.io/state/matcher';
|
|
88
|
+
|
|
89
|
+
const state = createState({ user: { name: 'Alice' } });
|
|
90
|
+
state.install(Matcher);
|
|
91
|
+
|
|
92
|
+
const unsubscribe = state.matcher.createMatcher(['user'], (userData) => {
|
|
93
|
+
console.log('User is now:', userData);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Later: remove the matcher
|
|
97
|
+
unsubscribe();
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Returns:** Unsubscribe function
|
|
101
|
+
|
|
102
|
+
#### `state.matcher.addMatcher(matcher)`
|
|
103
|
+
|
|
104
|
+
Add an existing matcher to the plugin.
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
const matcher = createMatcher(['user'], (userData) => {
|
|
108
|
+
console.log('User:', userData);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
state.matcher.addMatcher(matcher);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### `state.matcher.removeMatcher(matcher)`
|
|
115
|
+
|
|
116
|
+
Remove a matcher from the plugin.
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
state.matcher.removeMatcher(matcher);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Match Types
|
|
123
|
+
|
|
124
|
+
Matchers use hierarchical matching with three types. **The handler always receives the current data at the matched path:**
|
|
125
|
+
|
|
126
|
+
### Exact Match
|
|
127
|
+
|
|
128
|
+
Watches the exact path specified:
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
const matcher = createMatcher(['user', 'profile'], (profileData) => {
|
|
132
|
+
console.log('Profile data:', profileData);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
state.set(['user', 'profile'], { bio: 'Hello' }); // ✅ Fires with { bio: 'Hello' }
|
|
136
|
+
state.set(['user', 'profile', 'bio'], 'Hi'); // ✅ Fires with { bio: 'Hi' }
|
|
137
|
+
state.set(['user'], { profile: { bio: 'Hi' } }); // ✅ Fires with { bio: 'Hi' }
|
|
138
|
+
state.set(['user', 'settings'], {}); // ❌ Doesn't fire
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Parent Match
|
|
142
|
+
|
|
143
|
+
Fires when a parent path or any descendant changes:
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
const matcher = createMatcher(['user'], (userData) => {
|
|
147
|
+
console.log('User data:', userData);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
state.set(['user', 'name'], 'Alice'); // ✅ Fires with entire user object
|
|
151
|
+
state.set(['user', 'profile', 'bio'], 'Hello'); // ✅ Fires with entire user object
|
|
152
|
+
state.set(['user'], { name: 'Bob' }); // ✅ Fires with { name: 'Bob' }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Child Match
|
|
156
|
+
|
|
157
|
+
When watching a parent and children change, child changes are consolidated:
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
const matcher = createMatcher(['users'], (usersData) => {
|
|
161
|
+
console.log('Users data:', usersData);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
state.set(['users', 'alice'], { name: 'Alice' });
|
|
165
|
+
state.set(['users', 'bob'], { name: 'Bob' });
|
|
166
|
+
|
|
167
|
+
// Both changes are batched. Handler receives the full current state:
|
|
168
|
+
// { alice: { name: 'Alice' }, bob: { name: 'Bob' } }
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Configuration
|
|
172
|
+
|
|
173
|
+
### Initialize with Matchers
|
|
174
|
+
|
|
175
|
+
```javascript
|
|
176
|
+
import { createState } from '@jucio.io/state';
|
|
177
|
+
import { Matcher, createMatcher } from '@jucio.io/state/matcher';
|
|
178
|
+
|
|
179
|
+
const userMatcher = createMatcher(['user'], (user) => {
|
|
180
|
+
console.log('User:', user);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const settingsMatcher = createMatcher(['settings'], (settings) => {
|
|
184
|
+
console.log('Settings:', settings);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const state = createState({ user: {}, settings: {} });
|
|
188
|
+
state.install(Matcher.configure({
|
|
189
|
+
matchers: [userMatcher, settingsMatcher]
|
|
190
|
+
}));
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Define Matchers as Objects
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
import { createState } from '@jucio.io/state';
|
|
197
|
+
import { Matcher } from '@jucio.io/state/matcher';
|
|
198
|
+
|
|
199
|
+
const state = createState({ user: {}, settings: {} });
|
|
200
|
+
state.install(Matcher.configure({
|
|
201
|
+
matchers: [
|
|
202
|
+
{
|
|
203
|
+
path: ['user'],
|
|
204
|
+
handler: (user) => console.log('User:', user)
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
path: ['settings'],
|
|
208
|
+
handler: (settings) => console.log('Settings:', settings)
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
}));
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Advanced Usage
|
|
215
|
+
|
|
216
|
+
### Multiple Matchers on Same Path
|
|
217
|
+
|
|
218
|
+
```javascript
|
|
219
|
+
const logger = createMatcher(['user'], (userData) => {
|
|
220
|
+
console.log('User changed:', userData);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const validator = createMatcher(['user'], (userData) => {
|
|
224
|
+
if (!userData.email) {
|
|
225
|
+
console.warn('User has no email!');
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
state.matcher.addMatcher(logger);
|
|
230
|
+
state.matcher.addMatcher(validator);
|
|
231
|
+
|
|
232
|
+
state.set(['user'], { name: 'Alice' });
|
|
233
|
+
// Both matchers fire with { name: 'Alice' }
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Dynamic Matcher Management
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
// Add matcher conditionally
|
|
240
|
+
if (process.env.NODE_ENV === 'development') {
|
|
241
|
+
const debugMatcher = state.matcher.createMatcher(['*'], (data) => {
|
|
242
|
+
console.log('DEBUG: State changed:', data);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Add/remove based on user settings
|
|
247
|
+
function toggleAuditLog(enabled) {
|
|
248
|
+
if (enabled) {
|
|
249
|
+
const auditMatcher = createMatcher(['data'], (data) => {
|
|
250
|
+
logToServer('data-change', data);
|
|
251
|
+
});
|
|
252
|
+
state.matcher.addMatcher(auditMatcher);
|
|
253
|
+
return () => state.matcher.removeMatcher(auditMatcher);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Nested Path Watching
|
|
259
|
+
|
|
260
|
+
```javascript
|
|
261
|
+
// Watch different levels of nesting
|
|
262
|
+
const userMatcher = createMatcher(['user'], (userData) => {
|
|
263
|
+
console.log('Entire user object:', userData);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const profileMatcher = createMatcher(['user', 'profile'], (profileData) => {
|
|
267
|
+
console.log('Just profile:', profileData);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const nameMatcher = createMatcher(['user', 'profile', 'name'], (name) => {
|
|
271
|
+
console.log('Just name:', name);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
state.set(['user', 'profile', 'name'], 'Alice');
|
|
275
|
+
// All three matchers fire:
|
|
276
|
+
// - userMatcher gets the entire user object
|
|
277
|
+
// - profileMatcher gets just the profile object
|
|
278
|
+
// - nameMatcher gets just 'Alice'
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Batching and Consolidation
|
|
282
|
+
|
|
283
|
+
```javascript
|
|
284
|
+
const matcher = createMatcher(['items'], (itemsData) => {
|
|
285
|
+
console.log('Items:', itemsData);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Multiple rapid changes are batched
|
|
289
|
+
state.set(['items', 'item1'], { value: 1 });
|
|
290
|
+
state.set(['items', 'item2'], { value: 2 });
|
|
291
|
+
state.set(['items', 'item3'], { value: 3 });
|
|
292
|
+
|
|
293
|
+
// Single callback with current state:
|
|
294
|
+
// { item1: { value: 1 }, item2: { value: 2 }, item3: { value: 3 } }
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Common Patterns
|
|
298
|
+
|
|
299
|
+
### Form Field Validation
|
|
300
|
+
|
|
301
|
+
```javascript
|
|
302
|
+
const emailMatcher = createMatcher(['form', 'email'], (email) => {
|
|
303
|
+
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
304
|
+
state.set(['form', 'errors', 'email'], isValid ? null : 'Invalid email');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
state.matcher.addMatcher(emailMatcher);
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Persistence
|
|
311
|
+
|
|
312
|
+
```javascript
|
|
313
|
+
const persistMatcher = createMatcher(['user', 'preferences'], (preferences) => {
|
|
314
|
+
localStorage.setItem('preferences', JSON.stringify(preferences));
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
state.matcher.addMatcher(persistMatcher);
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Analytics Tracking
|
|
321
|
+
|
|
322
|
+
```javascript
|
|
323
|
+
const analyticsMatcher = createMatcher(['analytics', 'events'], (events) => {
|
|
324
|
+
Object.entries(events).forEach(([key, event]) => {
|
|
325
|
+
trackEvent(event.name, event.properties);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
state.matcher.addMatcher(analyticsMatcher);
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Derived State Updates
|
|
333
|
+
|
|
334
|
+
```javascript
|
|
335
|
+
// Update derived state when source changes
|
|
336
|
+
const cartMatcher = createMatcher(['cart', 'items'], (items) => {
|
|
337
|
+
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
|
338
|
+
state.set(['cart', 'total'], total);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
state.matcher.addMatcher(cartMatcher);
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### API Synchronization
|
|
345
|
+
|
|
346
|
+
```javascript
|
|
347
|
+
const syncMatcher = createMatcher(['user'], async (userData) => {
|
|
348
|
+
try {
|
|
349
|
+
await fetch('/api/user', {
|
|
350
|
+
method: 'PUT',
|
|
351
|
+
body: JSON.stringify(userData)
|
|
352
|
+
});
|
|
353
|
+
console.log('User synced to server');
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.error('Failed to sync user:', error);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
state.matcher.addMatcher(syncMatcher);
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Performance Considerations
|
|
363
|
+
|
|
364
|
+
1. **Automatic Batching**: Matchers automatically batch changes using `setTimeout(fn, 0)`, so multiple synchronous changes trigger the handler only once
|
|
365
|
+
|
|
366
|
+
2. **Smart Consolidation**: Multiple changes to the same path are consolidated into a single update
|
|
367
|
+
|
|
368
|
+
3. **Efficient Matching**: Uses marker comparison for fast path matching
|
|
369
|
+
|
|
370
|
+
4. **Cleanup**: Always unsubscribe matchers when they're no longer needed to prevent memory leaks
|
|
371
|
+
|
|
372
|
+
```javascript
|
|
373
|
+
// Good: Clean up when done
|
|
374
|
+
const unsubscribe = state.matcher.createMatcher(['temp'], handler);
|
|
375
|
+
// ... later
|
|
376
|
+
unsubscribe();
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Comparison with OnChange Plugin
|
|
380
|
+
|
|
381
|
+
| Feature | Matcher | OnChange |
|
|
382
|
+
|---------|---------|----------|
|
|
383
|
+
| What handler receives | Current data at path | Change objects with metadata |
|
|
384
|
+
| Scope | Specific paths | Global changes |
|
|
385
|
+
| Batching | Automatic | Automatic |
|
|
386
|
+
| Consolidation | Smart path-based | By change address |
|
|
387
|
+
| Performance | Optimized for specific paths | Tracks all changes |
|
|
388
|
+
| Use Case | Watch specific data, get values | Track all changes, get metadata |
|
|
389
|
+
|
|
390
|
+
**Use Matcher when:** You want the current data at specific paths
|
|
391
|
+
**Use OnChange when:** You need change metadata (from/to values, method, etc.)
|
|
392
|
+
|
|
393
|
+
## License
|
|
394
|
+
|
|
395
|
+
See the root [LICENSE](../../LICENSE) file for license information.
|
|
396
|
+
|
|
397
|
+
## Related Packages
|
|
398
|
+
- [@jucio.io/react](https://github.com/adrianjonmiller/react) - React integration
|
|
399
|
+
- [@jucio.io/state](../../core) - Core state management system
|
|
400
|
+
- [@jucio.io/state/history](../history) - Undo/redo functionality
|
|
401
|
+
- [@jucio.io/state/on-change](../on-change) - Global change listeners
|
|
402
|
+
|