@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 ADDED
@@ -0,0 +1,61 @@
1
+ MIT License with Commons Clause
2
+
3
+ Copyright (c) 2025 Jucio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ "Commons Clause" License Condition v1.0
26
+
27
+ The Software is provided to you by the Licensor under the License, as defined
28
+ below, subject to the following condition.
29
+
30
+ Without limiting other conditions in the License, the grant of rights under the
31
+ License will not include, and the License does not grant to you, the right to
32
+ Sell the Software.
33
+
34
+ For purposes of the foregoing, "Sell" means practicing any or all of the rights
35
+ granted to you under the License to provide to third parties, for a fee or other
36
+ consideration (including without limitation fees for hosting or consulting/
37
+ support services related to the Software), a product or service whose value
38
+ derives, entirely or substantially, from the functionality of the Software.
39
+
40
+ Any license notice or attribution required by the License must also include
41
+ this Commons Clause License Condition notice.
42
+
43
+ Software: @jucie-state/state
44
+ License: MIT License
45
+ Licensor: Jucio
46
+
47
+ ---
48
+
49
+ ADDITIONAL TERMS
50
+
51
+ This software is provided "as-is" without any warranty, support, or guarantee.
52
+ The author(s) are not obligated to:
53
+ - Provide support or answer questions
54
+ - Accept or implement feature requests
55
+ - Review or merge pull requests
56
+ - Fix bugs or security issues
57
+ - Maintain or update the software
58
+
59
+ You may submit issues and pull requests, but there is no expectation that they
60
+ will be addressed. Use this software at your own risk.
61
+
package/core/README.md ADDED
@@ -0,0 +1,635 @@
1
+ # @jucie-state/core
2
+
3
+ A powerful state management system for JavaScript applications featuring path-based access, history management, and serialization capabilities.
4
+
5
+ ## Features
6
+
7
+ - ๐ŸŽฏ **Path-Based Access**: Intuitive nested object and array manipulation
8
+ - โšก **High Performance**: Optimized for frequent updates with minimal overhead
9
+ - ๐Ÿ“ **History Management**: Built-in undo/redo via HistoryManager plugin
10
+ - ๐Ÿ’พ **Serialization**: Import/export state with CBOR encoding for persistence
11
+ - ๐Ÿ” **Powerful Queries**: Built-in querying with filters and transformations
12
+ - ๐Ÿ”Œ **Plugin Architecture**: Extensible with HistoryManager, Matcher, OnChange, and custom plugins
13
+ - ๐Ÿงช **Well Tested**: Comprehensive test suite with performance benchmarks
14
+ - ๐ŸŒŠ **Batch Operations**: Efficient batch updates with change consolidation
15
+
16
+ ## License and Usage
17
+
18
+ This software is provided under the **MIT License with Commons Clause**.
19
+
20
+ ### โœ… What You Can Do
21
+ - Use this library freely in personal or commercial projects
22
+ - Include it in your paid products and applications
23
+ - Modify and fork for your own use
24
+ - View and learn from the source code
25
+
26
+ ### โŒ What You Cannot Do
27
+ - **Sell this library as a standalone product** or competing state management solution
28
+ - Offer it as a paid service (SaaS) where the primary value is this library
29
+ - Create a commercial fork that competes with this project
30
+
31
+ ### โš ๏ธ No Warranty or Support
32
+ This software is provided **"as-is"** without any warranty, support, or guarantees:
33
+ - No obligation to provide support or answer questions
34
+ - No obligation to accept or implement feature requests
35
+ - No obligation to review or merge pull requests
36
+ - No obligation to fix bugs or security issues
37
+ - No obligation to maintain or update the software
38
+
39
+ **You are welcome to submit issues and pull requests**, but there is no expectation they will be addressed. Use this software at your own risk.
40
+
41
+ See the [LICENSE](./LICENSE) file for complete terms.
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ npm install @jucio.io/state
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ ```javascript
52
+ import { createState } from '@jucio.io/state';
53
+
54
+ // Create a state instance
55
+ const state = createState();
56
+
57
+ // Set some initial data
58
+ state.set(['user'], { name: 'Alice', age: 30 });
59
+ state.set(['counter'], 0);
60
+
61
+ // Get values
62
+ console.log(state.get(['user'])); // { name: 'Alice', age: 30 }
63
+ console.log(state.get(['counter'])); // 0
64
+
65
+ // Update state
66
+ state.set(['user', 'age'], 31);
67
+ console.log(state.get(['user', 'age'])); // 31
68
+
69
+ // Update using a function
70
+ state.update(['counter'], count => count + 1);
71
+ console.log(state.get(['counter'])); // 1
72
+ ```
73
+
74
+ ## Core Concepts
75
+
76
+ ### State Management
77
+
78
+ The state system uses path-based access for nested data structures:
79
+
80
+ ```javascript
81
+ import { createState } from '@jucio.io/state';
82
+
83
+ const state = createState({
84
+ user: { name: 'Alice', profile: { age: 30 } },
85
+ items: ['apple', 'banana']
86
+ });
87
+
88
+ // Get values
89
+ const user = state.get(['user']); // { name: 'Alice', profile: { age: 30 } }
90
+ const name = state.get(['user', 'name']); // 'Alice'
91
+ const age = state.get(['user', 'profile', 'age']); // 30
92
+
93
+ // Set values
94
+ state.set(['user', 'name'], 'Bob');
95
+ state.set(['user', 'profile', 'age'], 25);
96
+ state.set(['items', 2], 'cherry'); // ['apple', 'banana', 'cherry']
97
+
98
+ // Multiple gets
99
+ const [userName, userAge] = state.get(['user', 'name'], ['user', 'profile', 'age']);
100
+ ```
101
+
102
+ ### Batch Operations
103
+
104
+ ## API Reference
105
+
106
+ ### State Creation
107
+
108
+ #### `createState(initialState?)`
109
+
110
+ Create a new state instance.
111
+
112
+ ```javascript
113
+ const state = createState({
114
+ user: { name: 'Alice' },
115
+ counter: 0
116
+ });
117
+ ```
118
+
119
+ ### State Operations
120
+
121
+ #### `get(...paths)`
122
+
123
+ Get values from state using path arrays.
124
+
125
+ ```javascript
126
+ // Single path
127
+ const user = state.get(['user']);
128
+
129
+ // Multiple paths
130
+ const [name, age] = state.get(['user', 'name'], ['user', 'age']);
131
+
132
+ // Works with arrays
133
+ const firstItem = state.get(['items', 0]);
134
+ ```
135
+
136
+ #### `set(path, value)`
137
+
138
+ Set a value at the specified path.
139
+
140
+ ```javascript
141
+ state.set(['user', 'name'], 'Bob');
142
+ state.set(['items', 0], 'apple');
143
+ state.set(['deeply', 'nested', 'value'], 42);
144
+ ```
145
+
146
+ #### `update(path, updater)`
147
+
148
+ Update a value using a function.
149
+
150
+ ```javascript
151
+ // Increment counter
152
+ state.update(['counter'], count => count + 1);
153
+
154
+ // Update object properties
155
+ state.update(['user'], user => ({ ...user, lastSeen: Date.now() }));
156
+
157
+ // Update array
158
+ state.update(['items'], items => [...items, 'new item']);
159
+ ```
160
+
161
+ #### `remove(path)`
162
+
163
+ Remove a value from state.
164
+
165
+ ```javascript
166
+ state.remove(['user', 'age']); // Remove specific property
167
+ state.remove(['items', 1]); // Remove array element
168
+ ```
169
+
170
+ #### `has(...paths)`
171
+
172
+ Check if paths exist in state.
173
+
174
+ ```javascript
175
+ const hasUser = state.has(['user']); // true/false
176
+ const [hasName, hasAge] = state.has(['user', 'name'], ['user', 'age']);
177
+ ```
178
+
179
+ #### `keys(...paths)`
180
+
181
+ Get object keys at specified paths.
182
+
183
+ ```javascript
184
+ const userKeys = state.keys(['user']); // ['name', 'profile']
185
+ const [userKeys, profileKeys] = state.keys(['user'], ['user', 'profile']);
186
+ ```
187
+
188
+ #### `typeof(...paths)`
189
+
190
+ Get the type of values at specified paths.
191
+
192
+ ```javascript
193
+ const userType = state.typeof(['user']); // 'object'
194
+ const nameType = state.typeof(['user', 'name']); // 'string'
195
+ const itemsType = state.typeof(['items']); // 'array'
196
+ ```
197
+
198
+ ### Batch Operations
199
+
200
+ #### `batch(fn?)`
201
+
202
+ Batch multiple state changes to minimize re-computations.
203
+
204
+ ```javascript
205
+ // Option 1: With callback (automatic)
206
+ state.batch(() => {
207
+ state.set(['user', 'name'], 'Charlie');
208
+ state.set(['user', 'age'], 35);
209
+ state.set(['counter'], 10);
210
+ // Batch automatically ends when callback completes
211
+ });
212
+
213
+ // Option 2: Manual control
214
+ const endBatch = state.batch();
215
+ state.set(['user', 'name'], 'Charlie');
216
+ state.set(['user', 'age'], 35);
217
+ state.set(['counter'], 10);
218
+ endBatch(); // Manually end the batch
219
+ ```
220
+
221
+ ### Queries
222
+
223
+ The state system provides tree-searching capabilities using `findWhere` and `findAllWhere`:
224
+
225
+ #### `findWhere(key, matcher, value)`
226
+
227
+ Find the first path where a key matches a condition.
228
+
229
+ ```javascript
230
+ // Find first user with role 'admin'
231
+ const adminPath = state.findWhere('role', 'is', 'admin');
232
+ // Returns: ['users', 0] (path to the matching node)
233
+
234
+ // Then get the value
235
+ if (adminPath) {
236
+ const admin = state.get(adminPath);
237
+ }
238
+
239
+ // Other matchers
240
+ state.findWhere('age', '>', 18); // Greater than
241
+ state.findWhere('age', '>=', 18); // Greater than or equal
242
+ state.findWhere('status', '!==', 'inactive'); // Not equal
243
+ ```
244
+
245
+ **Supported matchers:**
246
+ - `'is'`, `'==='`, `'=='` - Equality
247
+ - `'not'`, `'!=='`, `'!='` - Inequality
248
+ - `'>'`, `'gt'` - Greater than
249
+ - `'<'`, `'lt'` - Less than
250
+ - `'>='`, `'gte'` - Greater than or equal
251
+ - `'<='`, `'lte'` - Less than or equal
252
+ - `'includes'` - Array includes value
253
+ - `'has'` - Object has value
254
+ - `'in'` - Key exists in object
255
+
256
+ #### `findAllWhere(key, matcher, value)`
257
+
258
+ Find all paths where a key matches a condition.
259
+
260
+ ```javascript
261
+ // Find all users with active status
262
+ const activePaths = state.findAllWhere('active', 'is', true);
263
+ // Returns: [['users', 0], ['users', 2], ['users', 5]]
264
+
265
+ // Get all matching values
266
+ const activeUsers = activePaths.map(path => state.get(path));
267
+ ```
268
+
269
+ **Note:** These methods search the entire state tree recursively and return **paths**, not values. Use `state.get(path)` to retrieve the actual data.
270
+
271
+ ## Plugins
272
+
273
+ The state system has a powerful plugin architecture that enables features like undo/redo and change tracking.
274
+
275
+ ### Installing Plugins
276
+
277
+ Plugins are installed using the `install()` method:
278
+
279
+ ```javascript
280
+ import { createState } from '@jucio.io/state';
281
+ import { HistoryManager } from '@jucio.io/state/history';
282
+ import { Matcher } from '@jucio.io/state/matcher';
283
+
284
+ const state = createState();
285
+
286
+ // Install a single plugin
287
+ state.install(HistoryManager);
288
+
289
+ // Install multiple plugins
290
+ state.install(HistoryManager, Matcher);
291
+ ```
292
+
293
+ ### HistoryManager Plugin
294
+
295
+ Provides undo/redo functionality with change tracking.
296
+
297
+ ```javascript
298
+ import { HistoryManager } from '@jucio.io/state/history';
299
+
300
+ const state = createState();
301
+ state.install(HistoryManager);
302
+
303
+ state.set(['counter'], 1);
304
+ state.set(['counter'], 2);
305
+ state.set(['counter'], 3);
306
+
307
+ // Undo operations
308
+ state.history.undo(); // counter back to 2
309
+ state.history.undo(); // counter back to 1
310
+
311
+ // Redo operations
312
+ state.history.redo(); // counter back to 2
313
+
314
+ // Check history status
315
+ console.log(state.history.canUndo()); // true/false
316
+ console.log(state.history.canRedo()); // true/false
317
+ console.log(state.history.size()); // number of history entries
318
+
319
+ // Batch history changes
320
+ const unbatch = state.history.batch();
321
+ state.set(['user', 'name'], 'Alice');
322
+ state.set(['user', 'age'], 30);
323
+ unbatch(); // Commits all changes as single history entry
324
+
325
+ // Add custom markers for better history navigation
326
+ state.set(['step'], 1);
327
+ state.history.addMarker('Step 1 completed');
328
+ state.set(['step'], 2);
329
+ state.history.addMarker('Step 2 completed');
330
+
331
+ // Listen to history commits
332
+ const unsubscribe = state.history.onCommit((changes) => {
333
+ console.log('History committed:', changes);
334
+ });
335
+ ```
336
+
337
+ **Configuration Options:**
338
+ ```javascript
339
+ import { HistoryManager } from '@jucio.io/state/history';
340
+
341
+ // Configure with custom options
342
+ state.install(HistoryManager.configure({
343
+ maxSize: 200 // Maximum history entries (default: 100)
344
+ }));
345
+ ```
346
+
347
+ ### Creating Custom Plugins
348
+
349
+ You can create custom plugins by extending the Plugin base class:
350
+
351
+ ```javascript
352
+ import { Plugin } from '@jucio.io/state/Plugin';
353
+
354
+ class CustomPlugin extends Plugin {
355
+ static name = 'custom';
356
+ static options = {
357
+ customOption: 'default'
358
+ };
359
+
360
+ initialize(state, options) {
361
+ // Called once when plugin is installed
362
+ state.addChangeListener((marker, change) => {
363
+ // React to changes
364
+ });
365
+ }
366
+
367
+ actions(state) {
368
+ // Return methods available on state.custom.*
369
+ return {
370
+ myAction: () => {
371
+ // Custom functionality
372
+ }
373
+ };
374
+ }
375
+
376
+ reset() {
377
+ // Called when state.reset() is invoked
378
+ }
379
+ }
380
+
381
+ // Use the plugin
382
+ state.install(CustomPlugin);
383
+ state.custom.myAction();
384
+ ```
385
+
386
+ ### Serialization
387
+
388
+ #### Export and Import
389
+
390
+ Both `export()` and `import()` are async methods that use CBOR encoding.
391
+
392
+ ```javascript
393
+ // Export state to CBOR format (async)
394
+ const exported = await state.export();
395
+
396
+ // Import into new state
397
+ const newState = createState();
398
+ await newState.import(exported);
399
+
400
+ // Export specific path
401
+ const userExport = await state.export(['user']);
402
+ ```
403
+
404
+ ### Change Tracking
405
+
406
+ Change tracking is available through the `@jucio.io/state/on-change` plugin:
407
+
408
+ ```javascript
409
+ import { createState } from '@jucio.io/state';
410
+ import { OnChange } from '@jucio.io/state/on-change';
411
+
412
+ const state = createState();
413
+ state.install(OnChange);
414
+
415
+ const unsubscribe = state.onChange.addListener((changes) => {
416
+ changes.forEach(change => {
417
+ console.log(`${change.method} at ${change.path.join('.')}`);
418
+ });
419
+ });
420
+
421
+ // Later, unsubscribe
422
+ unsubscribe();
423
+ ```
424
+
425
+ ## Advanced Usage
426
+
427
+ ### Change Tracking
428
+
429
+ Listen to all state changes using the OnChange plugin:
430
+
431
+ ```javascript
432
+ import { OnChange } from '@jucio.io/state/on-change';
433
+
434
+ state.install(OnChange);
435
+
436
+ const unsubscribe = state.onChange.addListener((changes) => {
437
+ changes.forEach(change => {
438
+ console.log(`${change.method} at ${change.path.join('.')}`);
439
+ });
440
+ });
441
+
442
+ // Later, unsubscribe
443
+ unsubscribe();
444
+ ```
445
+
446
+ ### Custom Effects
447
+
448
+ Track specific state changes using the Matcher plugin:
449
+
450
+ ```javascript
451
+ import { Matcher, createMatcher } from '@jucio.io/state/matcher';
452
+
453
+ state.install(Matcher);
454
+
455
+ const unsubscribe = state.matcher.createMatcher(['user'], (user) => {
456
+ console.log('User data changed:', user);
457
+ });
458
+
459
+ // Clean up when done
460
+ unsubscribe();
461
+ ```
462
+
463
+ ### Complex State Structures
464
+
465
+ ```javascript
466
+ const state = createState({
467
+ app: {
468
+ theme: 'dark',
469
+ language: 'en'
470
+ },
471
+ users: [
472
+ { id: 1, name: 'Alice', role: 'admin', active: true },
473
+ { id: 2, name: 'Bob', role: 'user', active: false },
474
+ { id: 3, name: 'Charlie', role: 'user', active: true }
475
+ ],
476
+ posts: [
477
+ { id: 1, authorId: 1, title: 'Hello World', likes: 5 },
478
+ { id: 2, authorId: 2, title: 'JavaScript Tips', likes: 12 }
479
+ ]
480
+ });
481
+
482
+ // Find active users
483
+ const activePaths = state.findAllWhere('active', 'is', true);
484
+ const activeUsers = activePaths.map(path => state.get(path));
485
+
486
+ // Calculate total likes manually
487
+ const posts = state.get(['posts']);
488
+ const totalLikes = posts.reduce((sum, p) => sum + p.likes, 0);
489
+
490
+ // Get dashboard stats
491
+ const users = state.get(['users']);
492
+ const dashboardStats = {
493
+ activeUserCount: users.filter(u => u.active).length,
494
+ totalPosts: posts.length,
495
+ totalLikes: totalLikes
496
+ };
497
+ ```
498
+
499
+ ### Performance Optimization
500
+
501
+ #### Batch Operations for Performance
502
+
503
+ ```javascript
504
+ // Inefficient - triggers multiple change events
505
+ state.set(['users', 0, 'name'], 'Alice Updated');
506
+ state.set(['users', 0, 'email'], 'alice@example.com');
507
+ state.set(['users', 0, 'lastLogin'], Date.now());
508
+
509
+ // Efficient - single batched operation
510
+ state.batch(() => {
511
+ state.set(['users', 0, 'name'], 'Alice Updated');
512
+ state.set(['users', 0, 'email'], 'alice@example.com');
513
+ state.set(['users', 0, 'lastLogin'], Date.now());
514
+ });
515
+
516
+ // Or use update for object modifications
517
+ state.update(['users', 0], user => ({
518
+ ...user,
519
+ name: 'Alice Updated',
520
+ email: 'alice@example.com',
521
+ lastLogin: Date.now()
522
+ }));
523
+ ```
524
+
525
+ ## Best Practices
526
+
527
+ ### 1. Use Path Arrays Consistently
528
+ ```javascript
529
+ // โœ… Good - consistent path format
530
+ state.get(['user', 'profile', 'name']);
531
+ state.set(['user', 'profile', 'name'], 'Alice');
532
+
533
+ // โŒ Avoid - mixing path formats
534
+ state.get('user.profile.name'); // This won't work
535
+ ```
536
+
537
+ ### 2. Batch Related Updates
538
+ ```javascript
539
+ // โœ… Good - batched updates
540
+ state.batch(() => {
541
+ state.set(['user', 'name'], 'Alice');
542
+ state.set(['user', 'email'], 'alice@example.com');
543
+ state.set(['user', 'updatedAt'], Date.now());
544
+ });
545
+
546
+ // โŒ Avoid - separate updates
547
+ state.set(['user', 'name'], 'Alice');
548
+ state.set(['user', 'email'], 'alice@example.com');
549
+ state.set(['user', 'updatedAt'], Date.now());
550
+ ```
551
+
552
+ ### 3. Use Standard JavaScript for Derived Data
553
+
554
+ ```javascript
555
+ // โœ… Good - use get() and standard JS
556
+ const users = state.get(['users']);
557
+ const fullNames = users.map(u => `${u.firstName} ${u.lastName}`);
558
+
559
+ // โœ… Also good - compute on demand
560
+ function getFullName(userId) {
561
+ const users = state.get(['users']);
562
+ const user = users.find(u => u.id === userId);
563
+ return user ? `${user.firstName} ${user.lastName}` : null;
564
+ }
565
+ ```
566
+
567
+ ### 4. Clean Up Effects and Listeners
568
+ ```javascript
569
+ // โœ… Good - cleanup
570
+ class Component {
571
+ constructor() {
572
+ this.unsubscribe = state.onChange.addListener(this.handleChange.bind(this));
573
+ }
574
+
575
+ destroy() {
576
+ this.unsubscribe();
577
+ }
578
+ }
579
+ ```
580
+
581
+ ## Performance
582
+
583
+ The library is highly optimized for real-world performance:
584
+
585
+ ### Benchmarks
586
+
587
+ ```
588
+ State Operations:
589
+ get (simple): 7.6M ops/sec
590
+ set (simple): 4.8M ops/sec
591
+ get (nested): 5.0M ops/sec
592
+ set (nested): 3.6M ops/sec
593
+
594
+ Plugins:
595
+ HistoryManager: ~12% overhead on writes, 0% on reads
596
+ ```
597
+
598
+ ### Performance Considerations
599
+
600
+ - **Path-based access**: More efficient than deep object watches
601
+ - **Fine-grained tracking**: Only track changes where needed
602
+ - **Batching**: Use batch operations to consolidate multiple changes
603
+ - **Memory**: Clean up listeners when components are destroyed
604
+ - **Plugins**: Minimal overhead - gets remain fast, writes have reasonable tracking cost
605
+
606
+ ## Testing
607
+
608
+ ```bash
609
+ # Run tests
610
+ npm test
611
+
612
+ # Run tests in watch mode
613
+ npm run test:watch
614
+
615
+ # Run benchmarks
616
+ npm run bench
617
+ ```
618
+
619
+ ## License
620
+
621
+ This project is licensed under the **MIT License with Commons Clause**.
622
+
623
+ This means you can freely use this library in your projects (including commercial ones), but you cannot sell the library itself as a standalone product or competing service.
624
+
625
+ See the [LICENSE](./LICENSE) file for complete details.
626
+
627
+ ## Contributing
628
+
629
+ You are welcome to submit issues and pull requests, however:
630
+
631
+ - There is **no guarantee** that issues will be addressed
632
+ - There is **no guarantee** that pull requests will be reviewed or merged
633
+ - This project is maintained on an **as-available basis** with no commitments
634
+
635
+ By contributing, you agree that your contributions will be licensed under the same MIT + Commons Clause license.