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