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