@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.
@@ -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
+