Core state management system with serialization and path-based access
npm install @jucie-state/coreA powerful state management system for JavaScript applications featuring path-based access, history management, and serialization capabilities.
- ๐ฏ Path-Based Access: Intuitive nested object and array manipulation
- โก High Performance: Optimized for frequent updates with minimal overhead
- ๐ History Management: Built-in undo/redo via HistoryManager plugin
- ๐พ Serialization: Import/export state with CBOR encoding for persistence
- ๐ Powerful Queries: Built-in querying with filters and transformations
- ๐ Plugin Architecture: Extensible with HistoryManager, Matcher, OnChange, and custom plugins
- ๐งช Well Tested: Comprehensive test suite with performance benchmarks
- ๐ Batch Operations: Efficient batch updates with change consolidation
This software is provided under the MIT License with Commons Clause.
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.
See the LICENSE file for complete terms.
``bash`
npm install @jucie-state/core
`javascript
import { createState } from '@jucie-state/core';
// Create a state instance
const state = createState();
// Set some initial data
state.set(['user'], { name: 'Alice', age: 30 });
state.set(['counter'], 0);
// Get values
console.log(state.get(['user'])); // { name: 'Alice', age: 30 }
console.log(state.get(['counter'])); // 0
// Update state
state.set(['user', 'age'], 31);
console.log(state.get(['user', 'age'])); // 31
// Update using a function
state.update(['counter'], count => count + 1);
console.log(state.get(['counter'])); // 1
`
The state system uses path-based access for nested data structures:
`javascript
import { createState } from '@jucie-state/core';
const state = createState({
user: { name: 'Alice', profile: { age: 30 } },
items: ['apple', 'banana']
});
// Get values
const user = state.get(['user']); // { name: 'Alice', profile: { age: 30 } }
const name = state.get(['user', 'name']); // 'Alice'
const age = state.get(['user', 'profile', 'age']); // 30
// Set values
state.set(['user', 'name'], 'Bob');
state.set(['user', 'profile', 'age'], 25);
state.set(['items', 2], 'cherry'); // ['apple', 'banana', 'cherry']
// Multiple gets
const [userName, userAge] = state.get(['user', 'name'], ['user', 'profile', 'age']);
`
#### createState(initialState?, config?)
Create a new state instance.
`javascript`
const state = createState({
user: { name: 'Alice' },
counter: 0
});
#### get(...paths)
Get values from state using path arrays.
`javascript
// Single path
const user = state.get(['user']);
// Multiple paths
const [name, age] = state.get(['user', 'name'], ['user', 'age']);
// Works with arrays
const firstItem = state.get(['items', 0]);
`
#### set(path, value)
Set a value at the specified path.
`javascript`
state.set(['user', 'name'], 'Bob');
state.set(['items', 0], 'apple');
state.set(['deeply', 'nested', 'value'], 42);
#### update(path, updater)
Update a value using a function.
`javascript
// Increment counter
state.update(['counter'], count => count + 1);
// Update object properties
state.update(['user'], user => ({ ...user, lastSeen: Date.now() }));
// Update array
state.update(['items'], items => [...items, 'new item']);
`
#### remove(path)
Remove a value from state.
`javascript`
state.remove(['user', 'age']); // Remove specific property
state.remove(['items', 1]); // Remove array element
#### has(...paths)
Check if paths exist in state.
`javascript`
const hasUser = state.has(['user']); // true/false
const [hasName, hasAge] = state.has(['user', 'name'], ['user', 'age']);
#### keys(...paths)
Get object keys at specified paths.
`javascript`
const userKeys = state.keys(['user']); // ['name', 'profile']
const [userKeys, profileKeys] = state.keys(['user'], ['user', 'profile']);
#### typeof(...paths)
Get the type of values at specified paths.
`javascript`
const userType = state.typeof(['user']); // 'object'
const nameType = state.typeof(['user', 'name']); // 'string'
const itemsType = state.typeof(['items']); // 'array'
#### batch(fn)
Batch multiple state changes to minimize re-computations.
`javascript`
state.batch(() => {
state.set(['user', 'name'], 'Charlie');
state.set(['user', 'age'], 35);
state.set(['counter'], 10);
// All changes batched together
});
#### query(queryFn, ...paths)
Query state with filtering and transformation.
`javascript
// Find all users over 18
const adults = state.query(
(users) => users.filter(user => user.age >= 18),
['users']
);
// Get specific fields from multiple paths
const summary = state.query(
([users, settings]) => ({
totalUsers: users.length,
theme: settings.theme
}),
['users'],
['settings']
);
`
#### find(predicate, ...paths)
Find the first matching item.
`javascript`
const adminUser = state.find(
user => user.role === 'admin',
['users']
);
#### filter(predicate, ...paths)
Filter items based on a predicate.
`javascript`
const activeUsers = state.filter(
user => user.status === 'active',
['users']
);
The state system has a powerful plugin architecture that enables features like undo/redo and change tracking.
Plugins are installed using the install() method:
`javascript
import { createState } from '@jucie-state/core';
import { HistoryManager } from '@jucie-state/history';
import { Matcher } from '@jucie-state/matcher';
const state = createState();
// Install a single plugin
state.install(HistoryManager);
// Install multiple plugins
state.install(HistoryManager, Matcher);
`
Provides undo/redo functionality with change tracking.
`javascript
import { HistoryManager } from '@jucie-state/history';
const state = createState();
state.install(HistoryManager);
state.set(['counter'], 1);
state.set(['counter'], 2);
state.set(['counter'], 3);
// Undo operations
state.history.undo(); // counter back to 2
state.history.undo(); // counter back to 1
// Redo operations
state.history.redo(); // counter back to 2
// Check history status
console.log(state.history.canUndo()); // true/false
console.log(state.history.canRedo()); // true/false
console.log(state.history.size()); // number of history entries
// Batch history changes
const unbatch = state.history.batch();
state.set(['user', 'name'], 'Alice');
state.set(['user', 'age'], 30);
unbatch(); // Commits all changes as single history entry
// Add custom markers for better history navigation
state.set(['step'], 1);
state.history.addMarker('Step 1 completed');
state.set(['step'], 2);
state.history.addMarker('Step 2 completed');
// Listen to history commits
const unsubscribe = state.history.onCommit((changes) => {
console.log('History committed:', changes);
});
`
Configuration Options:
`javascript
import { HistoryManager } from '@jucie-state/history';
// Configure with custom options
state.install(HistoryManager.configure({
maxSize: 200 // Maximum history entries (default: 100)
}));
`
You can create custom plugins by extending the Plugin base class:
`javascript
import { Plugin } from '@jucie-state/core/Plugin';
class CustomPlugin extends Plugin {
static name = 'custom';
static options = {
customOption: 'default'
};
initialize(state, options) {
// Called once when plugin is installed
state.addChangeListener((marker, change) => {
// React to changes
});
}
actions(state) {
// Return methods available on state.custom.*
return {
myAction: () => {
// Custom functionality
}
};
}
reset() {
// Called when state.reset() is invoked
}
}
// Use the plugin
state.install(CustomPlugin);
state.custom.myAction();
`
#### Export and Import
`javascript
// Export state to CBOR format
const exported = state.export();
// Create new state from exported data
const newState = createState();
newState.import(exported);
// Or create new state from export
const restoredState = createState();
restoredState.import(exported);
`
Change tracking is available through the @jucie-state/on-change plugin:
`javascript
import { createState } from '@jucie-state/core';
import { OnChange } from '@jucie-state/on-change';
const state = createState();
state.install(OnChange);
const unsubscribe = state.onChange.addListener((changes) => {
changes.forEach(change => {
console.log(${change.method} at ${change.path.join('.')});
});
});
// Later, unsubscribe
unsubscribe();
`
Listen to all state changes using the OnChange plugin:
`javascript
import { OnChange } from '@jucie-state/on-change';
state.install(OnChange);
const unsubscribe = state.onChange.addListener((changes) => {
changes.forEach(change => {
console.log(${change.method} at ${change.path.join('.')});
});
});
// Later, unsubscribe
unsubscribe();
`
Track specific state changes using the Matcher plugin:
`javascript
import { Matcher, createMatcher } from '@jucie-state/matcher';
state.install(Matcher);
const unsubscribe = state.matcher.createMatcher(['user'], (user) => {
console.log('User data changed:', user);
});
// Clean up when done
unsubscribe();
`
`javascript
const state = createState({
app: {
theme: 'dark',
language: 'en'
},
users: [
{ id: 1, name: 'Alice', role: 'admin', active: true },
{ id: 2, name: 'Bob', role: 'user', active: false },
{ id: 3, name: 'Charlie', role: 'user', active: true }
],
posts: [
{ id: 1, authorId: 1, title: 'Hello World', likes: 5 },
{ id: 2, authorId: 2, title: 'JavaScript Tips', likes: 12 }
]
});
// Query users
const activeUsers = state.query(
(users) => users.filter(u => u.active),
['users']
);
const totalLikes = state.query(
(posts) => posts.reduce((sum, p) => sum + p.likes, 0),
['posts']
);
const dashboardStats = state.query(
([users, posts]) => ({
activeUserCount: users.filter(u => u.active).length,
totalPosts: posts.length,
totalLikes: posts.reduce((sum, p) => sum + p.likes, 0)
}),
['users'],
['posts']
);
`
#### Batch Operations for Performance
`javascript
// Inefficient - triggers multiple change events
state.set(['users', 0, 'name'], 'Alice Updated');
state.set(['users', 0, 'email'], 'alice@example.com');
state.set(['users', 0, 'lastLogin'], Date.now());
// Efficient - single batched operation
state.batch(() => {
state.set(['users', 0, 'name'], 'Alice Updated');
state.set(['users', 0, 'email'], 'alice@example.com');
state.set(['users', 0, 'lastLogin'], Date.now());
});
// Or use update for object modifications
state.update(['users', 0], user => ({
...user,
name: 'Alice Updated',
email: 'alice@example.com',
lastLogin: Date.now()
}));
`
The state system integrates seamlessly with the Jucie Engine:
`javascript
import { Engine, ServiceProvider } from '@jucie-state/engine';
class UserService extends ServiceProvider {
static manifest = {
name: 'User Service',
namespace: 'users',
version: '1.0.0'
};
actions({ state }) {
return {
addUser: (userData) => {
const users = state.get(['users']) || [];
const newUser = { id: Date.now(), ...userData };
state.set(['users'], [...users, newUser]);
return newUser;
},
updateUser: (id, updates) => {
const users = state.get(['users']) || [];
const index = users.findIndex(u => u.id === id);
if (index !== -1) {
state.update(['users', index], user => ({ ...user, ...updates }));
return state.get(['users', index]);
}
return null;
},
getActiveUsers: () => {
return state.filter(user => user.active, ['users']);
}
};
}
getters({ state }) {
return {
userCount: () => (state.get(['users']) || []).length,
hasUsers: () => (state.get(['users']) || []).length > 0
};
}
}
`
javascript
// โ
Good - consistent path format
state.get(['user', 'profile', 'name']);
state.set(['user', 'profile', 'name'], 'Alice');// โ Avoid - mixing path formats
state.get('user.profile.name'); // This won't work
`$3
`javascript
// โ
Good - batched updates
state.batch(() => {
state.set(['user', 'name'], 'Alice');
state.set(['user', 'email'], 'alice@example.com');
state.set(['user', 'updatedAt'], Date.now());
});// โ Avoid - separate updates
state.set(['user', 'name'], 'Alice');
state.set(['user', 'email'], 'alice@example.com');
state.set(['user', 'updatedAt'], Date.now());
`$3
`javascript
// โ
Good - query for derived data
const fullNames = state.query(
(users) => users.map(u => ${u.firstName} ${u.lastName}),
['users']
);// โ
Also good - compute on demand
function getFullName(userId) {
const user = state.find(u => u.id === userId, ['users']);
return user ?
${user.firstName} ${user.lastName} : null;
}
`$3
`javascript
// โ
Good - cleanup
class Component {
constructor() {
this.unsubscribe = state.onChange.addListener(this.handleChange.bind(this));
} destroy() {
this.unsubscribe();
}
}
`Performance
The library is highly optimized for real-world performance:
$3
`
State Operations:
get (simple): 7.6M ops/sec
set (simple): 4.8M ops/sec
get (nested): 5.0M ops/sec
set (nested): 3.6M ops/secPlugins:
HistoryManager: ~12% overhead on writes, 0% on reads
`$3
- Path-based access: More efficient than deep object watches
- Fine-grained tracking: Only track changes where needed
- Batching: Use batch operations to consolidate multiple changes
- Memory: Clean up listeners when components are destroyed
- Plugins: Minimal overhead - gets remain fast, writes have reasonable tracking cost
Testing
`bash
Run tests
npm testRun tests in watch mode
npm run test:watchRun benchmarks
npm run bench
``This project is licensed under the MIT License with Commons Clause.
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.
See the LICENSE file for complete details.
You are welcome to submit issues and pull requests, however:
- There is no guarantee that issues will be addressed
- There is no guarantee that pull requests will be reviewed or merged
- This project is maintained on an as-available basis with no commitments
By contributing, you agree that your contributions will be licensed under the same MIT + Commons Clause license.