@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,444 @@
1
+ # @jucio.io/state/on-change
2
+
3
+ Simple global change listener plugin for @jucio.io/state that notifies you of all state changes with automatic batching and consolidation.
4
+
5
+ ## Features
6
+
7
+ - 🔔 **Global Change Tracking**: Listen to all state changes in one place
8
+ - 📦 **Automatic Batching**: Changes are batched and delivered asynchronously
9
+ - 🔄 **Smart Consolidation**: Multiple changes to the same path are consolidated
10
+ - 🎯 **Simple API**: Just add listeners and remove them when done
11
+ - âš¡ **Performance Optimized**: Only active when listeners are registered
12
+ - 🔌 **Zero Configuration**: Works out of the box
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @jucio.io/state
18
+ ```
19
+
20
+ **Note:** OnChange plugin is included in the main package.
21
+
22
+ ## Quick Start
23
+
24
+ ```javascript
25
+ import { createState } from '@jucio.io/state';
26
+ import { OnChange } from '@jucio.io/state/on-change';
27
+
28
+ // Create state and install OnChange plugin
29
+ const state = createState({
30
+ user: { name: 'Alice', age: 30 },
31
+ settings: { theme: 'dark' }
32
+ });
33
+ state.install(OnChange);
34
+
35
+ // Add a change listener
36
+ const unsubscribe = state.onChange.addListener((changes) => {
37
+ console.log('State changed:', changes);
38
+ });
39
+
40
+ // Make some changes
41
+ state.set(['user', 'name'], 'Bob');
42
+ state.set(['settings', 'theme'], 'light');
43
+
44
+ // Console: "State changed: [{ path: ['user', 'name'], from: 'Alice', to: 'Bob', ... }, ...]"
45
+
46
+ // Remove listener when done
47
+ unsubscribe();
48
+ ```
49
+
50
+ ## Configuration
51
+
52
+ The OnChange plugin can be configured with custom options:
53
+
54
+ ```javascript
55
+ import { createState } from '@jucio.io/state';
56
+ import { OnChange } from '@jucio.io/state/on-change';
57
+
58
+ const state = createState({
59
+ user: { name: 'Alice', age: 30 }
60
+ });
61
+
62
+ // Install with custom configuration
63
+ state.install(OnChange.configure({
64
+ debounce: 100 // Debounce change notifications by 100ms (default: 0)
65
+ }));
66
+ ```
67
+
68
+ ### Options
69
+
70
+ - **`debounce`** (number): Delay in milliseconds before notifying listeners. Default: `0` (no debounce)
71
+
72
+ ## API Reference
73
+
74
+ ### Actions
75
+
76
+ #### `state.onChange.addListener(callback)`
77
+
78
+ Add a listener that will be called whenever state changes occur.
79
+
80
+ ```javascript
81
+ const unsubscribe = state.onChange.addListener((changes) => {
82
+ changes.forEach(change => {
83
+ console.log('Changed:', change.path);
84
+ console.log('From:', change.from);
85
+ console.log('To:', change.to);
86
+ });
87
+ });
88
+ ```
89
+
90
+ **Parameters:**
91
+ - `callback` (Function): Function that receives an array of change objects
92
+
93
+ **Returns:** Unsubscribe function to remove the listener
94
+
95
+ **Change Object Structure:**
96
+ ```javascript
97
+ {
98
+ path: ['user', 'name'], // Path that changed
99
+ from: 'Alice', // Previous value
100
+ to: 'Bob', // New value
101
+ method: 'set', // Method used (set, delete, push, etc.)
102
+ timestamp: 1234567890 // Timestamp of change (if available)
103
+ }
104
+ ```
105
+
106
+ #### `state.onChange.removeListener(callback)`
107
+
108
+ Manually remove a listener.
109
+
110
+ ```javascript
111
+ function myListener(changes) {
112
+ console.log('Changes:', changes);
113
+ }
114
+
115
+ state.onChange.addListener(myListener);
116
+
117
+ // Later...
118
+ state.onChange.removeListener(myListener);
119
+ ```
120
+
121
+ **Parameters:**
122
+ - `callback` (Function): The listener function to remove
123
+
124
+ ## How It Works
125
+
126
+ ### Automatic Batching
127
+
128
+ Multiple synchronous changes are automatically batched and delivered together:
129
+
130
+ ```javascript
131
+ state.onChange.addListener((changes) => {
132
+ console.log('Received', changes.length, 'changes');
133
+ });
134
+
135
+ // These three changes are batched together
136
+ state.set(['a'], 1);
137
+ state.set(['b'], 2);
138
+ state.set(['c'], 3);
139
+
140
+ // Console: "Received 3 changes"
141
+ ```
142
+
143
+ ### Smart Consolidation
144
+
145
+ Multiple changes to the same path are consolidated, keeping only the most recent:
146
+
147
+ ```javascript
148
+ state.onChange.addListener((changes) => {
149
+ console.log('Changes:', changes);
150
+ });
151
+
152
+ // Rapid changes to the same path
153
+ state.set(['counter'], 1);
154
+ state.set(['counter'], 2);
155
+ state.set(['counter'], 3);
156
+
157
+ // Console: "Changes: [{ path: ['counter'], from: 0, to: 3, ... }]"
158
+ // Only the final change is reported
159
+ ```
160
+
161
+ ### Performance Optimization
162
+
163
+ The plugin only tracks changes when at least one listener is registered:
164
+
165
+ ```javascript
166
+ // No listeners = no overhead
167
+ state.set(['value'], 1); // Fast, no tracking
168
+
169
+ // Add listener
170
+ const unsubscribe = state.onChange.addListener(handler);
171
+
172
+ // Now tracking is active
173
+ state.set(['value'], 2); // Tracked and reported
174
+
175
+ // Remove listener
176
+ unsubscribe();
177
+
178
+ // Back to no overhead
179
+ state.set(['value'], 3); // Fast, no tracking
180
+ ```
181
+
182
+ ## Common Use Cases
183
+
184
+ ### Logging
185
+
186
+ ```javascript
187
+ state.onChange.addListener((changes) => {
188
+ if (process.env.NODE_ENV === 'development') {
189
+ console.group('State Changes');
190
+ changes.forEach(change => {
191
+ console.log(`${change.path.join('.')}: ${change.from} → ${change.to}`);
192
+ });
193
+ console.groupEnd();
194
+ }
195
+ });
196
+ ```
197
+
198
+ ### Persistence
199
+
200
+ ```javascript
201
+ state.onChange.addListener((changes) => {
202
+ // Save entire state to localStorage
203
+ localStorage.setItem('appState', JSON.stringify(state.get([])));
204
+ });
205
+ ```
206
+
207
+ ### Analytics
208
+
209
+ ```javascript
210
+ state.onChange.addListener((changes) => {
211
+ changes.forEach(change => {
212
+ analytics.track('state_changed', {
213
+ path: change.path.join('.'),
214
+ method: change.method,
215
+ timestamp: Date.now()
216
+ });
217
+ });
218
+ });
219
+ ```
220
+
221
+ ### DevTools Integration
222
+
223
+ ```javascript
224
+ if (window.__REDUX_DEVTOOLS_EXTENSION__) {
225
+ const devTools = window.__REDUX_DEVTOOLS_EXTENSION__.connect();
226
+
227
+ state.onChange.addListener((changes) => {
228
+ changes.forEach(change => {
229
+ devTools.send({
230
+ type: `SET ${change.path.join('.')}`,
231
+ payload: change.to
232
+ }, state.get([]));
233
+ });
234
+ });
235
+ }
236
+ ```
237
+
238
+ ### Sync to Server
239
+
240
+ ```javascript
241
+ let syncTimeout;
242
+
243
+ state.onChange.addListener((changes) => {
244
+ // Debounce server sync
245
+ clearTimeout(syncTimeout);
246
+ syncTimeout = setTimeout(async () => {
247
+ await fetch('/api/sync', {
248
+ method: 'POST',
249
+ body: JSON.stringify({
250
+ changes,
251
+ state: state.get([])
252
+ })
253
+ });
254
+ }, 1000);
255
+ });
256
+ ```
257
+
258
+ ### Change History
259
+
260
+ ```javascript
261
+ const changeHistory = [];
262
+
263
+ state.onChange.addListener((changes) => {
264
+ changeHistory.push({
265
+ timestamp: Date.now(),
266
+ changes: changes.map(c => ({ ...c }))
267
+ });
268
+
269
+ // Keep last 100 change batches
270
+ if (changeHistory.length > 100) {
271
+ changeHistory.shift();
272
+ }
273
+ });
274
+ ```
275
+
276
+ ### Validation
277
+
278
+ ```javascript
279
+ state.onChange.addListener((changes) => {
280
+ changes.forEach(change => {
281
+ if (change.path[0] === 'user' && change.path[1] === 'email') {
282
+ const email = change.to;
283
+ if (!email.includes('@')) {
284
+ console.warn('Invalid email address');
285
+ // Optionally revert the change
286
+ state.set(change.path, change.from);
287
+ }
288
+ }
289
+ });
290
+ });
291
+ ```
292
+
293
+ ## Advanced Patterns
294
+
295
+ ### Conditional Listeners
296
+
297
+ ```javascript
298
+ let unsubscribe = null;
299
+
300
+ function enableChangeTracking() {
301
+ if (!unsubscribe) {
302
+ unsubscribe = state.onChange.addListener((changes) => {
303
+ console.log('Tracking changes:', changes);
304
+ });
305
+ }
306
+ }
307
+
308
+ function disableChangeTracking() {
309
+ if (unsubscribe) {
310
+ unsubscribe();
311
+ unsubscribe = null;
312
+ }
313
+ }
314
+
315
+ // Enable tracking based on some condition
316
+ if (userPreferences.debugMode) {
317
+ enableChangeTracking();
318
+ }
319
+ ```
320
+
321
+ ### Multiple Listeners
322
+
323
+ ```javascript
324
+ // Logger
325
+ state.onChange.addListener((changes) => {
326
+ console.log('LOG:', changes.length, 'changes');
327
+ });
328
+
329
+ // Validator
330
+ state.onChange.addListener((changes) => {
331
+ changes.forEach(validateChange);
332
+ });
333
+
334
+ // Sync
335
+ state.onChange.addListener((changes) => {
336
+ syncToServer(changes);
337
+ });
338
+
339
+ // All listeners receive the same changes
340
+ ```
341
+
342
+ ### Filtered Listeners
343
+
344
+ ```javascript
345
+ // Create a filtered listener helper
346
+ function createFilteredListener(pathPrefix, callback) {
347
+ return state.onChange.addListener((changes) => {
348
+ const filtered = changes.filter(change => {
349
+ return change.path.length >= pathPrefix.length &&
350
+ pathPrefix.every((segment, i) => change.path[i] === segment);
351
+ });
352
+
353
+ if (filtered.length > 0) {
354
+ callback(filtered);
355
+ }
356
+ });
357
+ }
358
+
359
+ // Only listen to user changes
360
+ const unsubscribe = createFilteredListener(['user'], (changes) => {
361
+ console.log('User changed:', changes);
362
+ });
363
+ ```
364
+
365
+ ### Change Replay
366
+
367
+ ```javascript
368
+ const recordedChanges = [];
369
+ let isRecording = false;
370
+
371
+ const unsubscribe = state.onChange.addListener((changes) => {
372
+ if (isRecording) {
373
+ recordedChanges.push(...changes);
374
+ }
375
+ });
376
+
377
+ function startRecording() {
378
+ isRecording = true;
379
+ recordedChanges.length = 0;
380
+ }
381
+
382
+ function stopRecording() {
383
+ isRecording = false;
384
+ return [...recordedChanges];
385
+ }
386
+
387
+ function replay(changes) {
388
+ changes.forEach(change => {
389
+ state.set(change.path, change.to);
390
+ });
391
+ }
392
+ ```
393
+
394
+ ## Comparison with Matcher Plugin
395
+
396
+ | Feature | OnChange | Matcher |
397
+ |---------|----------|---------|
398
+ | Scope | All changes | Specific paths |
399
+ | Batching | Automatic | Automatic |
400
+ | Consolidation | By address | By path |
401
+ | Filtering | Manual | Built-in |
402
+ | Use Case | Global tracking | Specific watchers |
403
+ | Performance | Tracks everything | Optimized for paths |
404
+
405
+ **When to use OnChange:**
406
+ - Debug logging
407
+ - Global persistence
408
+ - Analytics tracking
409
+ - DevTools integration
410
+ - You need to see all changes
411
+
412
+ **When to use Matcher:**
413
+ - Watch specific data
414
+ - React to specific paths
415
+ - Need hierarchical matching
416
+ - Better performance for selective watching
417
+
418
+ ## Performance Tips
419
+
420
+ 1. **Remove listeners when done** - Always unsubscribe to prevent memory leaks
421
+ 2. **Avoid heavy computations** - Listeners are called frequently, keep them fast
422
+ 3. **Use debouncing** for expensive operations like server sync
423
+ 4. **Filter early** if you only care about specific changes
424
+ 5. **Batch operations** when making multiple changes to reduce listener calls
425
+
426
+ ```javascript
427
+ // Good: Remove when done
428
+ const unsub = state.onChange.addListener(handler);
429
+ cleanup(() => unsub());
430
+
431
+ // Bad: Memory leak
432
+ state.onChange.addListener(handler);
433
+ ```
434
+
435
+ ## License
436
+
437
+ See the root [LICENSE](../../LICENSE) file for license information.
438
+
439
+ ## Related Packages
440
+
441
+ - [@jucio.io/state](../../core) - Core state management system
442
+ - [@jucio.io/state/history](../history) - Undo/redo functionality
443
+ - [@jucio.io/state/matcher](../matcher) - Path-based change tracking
444
+