# @jucie.io/state

A powerful state management system for JavaScript applications featuring path-based access, history management, and serialization capabilities.

## Features

- 🎯 **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

## License and Usage

This software is provided under the **MIT License with Commons Clause**. 

### ✅ What You Can Do
- Use this library freely in personal or commercial projects
- Include it in your paid products and applications
- Modify and fork for your own use
- View and learn from the source code

### ❌ What You Cannot Do
- **Sell this library as a standalone product** or competing state management solution
- Offer it as a paid service (SaaS) where the primary value is this library
- Create a commercial fork that competes with this project

### ⚠️ No Warranty or Support
This software is provided **"as-is"** without any warranty, support, or guarantees:
- No obligation to provide support or answer questions
- No obligation to accept or implement feature requests
- No obligation to review or merge pull requests
- No obligation to fix bugs or security issues
- No obligation to maintain or update the software

**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](./LICENSE) file for complete terms.

## Installation

```bash
npm install @jucio.io/state
```

## Quick Start

```javascript
import { createState } from '@jucio.io/state';

// 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
```

## Core Concepts

### State Management

The state system uses path-based access for nested data structures:

```javascript
import { createState } from '@jucio.io/state';

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']);
```

### Batch Operations

## API Reference

### State Creation

#### `createState(initialState?)`

Create a new state instance.

```javascript
const state = createState({
  user: { name: 'Alice' },
  counter: 0
});
```

### State Operations

#### `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 Operations

#### `batch(fn?)`

Batch multiple state changes to minimize re-computations.

```javascript
// Option 1: With callback (automatic)
state.batch(() => {
  state.set(['user', 'name'], 'Charlie');
  state.set(['user', 'age'], 35);
  state.set(['counter'], 10);
  // Batch automatically ends when callback completes
});

// Option 2: Manual control
const endBatch = state.batch();
state.set(['user', 'name'], 'Charlie');
state.set(['user', 'age'], 35);
state.set(['counter'], 10);
endBatch(); // Manually end the batch
```

### Queries

The state system provides tree-searching capabilities using `findWhere` and `findAllWhere`:

#### `findWhere(key, matcher, value)`

Find the first path where a key matches a condition.

```javascript
// Find first user with role 'admin'
const adminPath = state.findWhere('role', 'is', 'admin');
// Returns: ['users', 0] (path to the matching node)

// Then get the value
if (adminPath) {
  const admin = state.get(adminPath);
}

// Other matchers
state.findWhere('age', '>', 18);      // Greater than
state.findWhere('age', '>=', 18);     // Greater than or equal
state.findWhere('status', '!==', 'inactive');  // Not equal
```

**Supported matchers:**
- `'is'`, `'==='`, `'=='` - Equality
- `'not'`, `'!=='`, `'!='` - Inequality  
- `'>'`, `'gt'` - Greater than
- `'<'`, `'lt'` - Less than
- `'>='`, `'gte'` - Greater than or equal
- `'<='`, `'lte'` - Less than or equal
- `'includes'` - Array includes value
- `'has'` - Object has value
- `'in'` - Key exists in object

#### `findAllWhere(key, matcher, value)`

Find all paths where a key matches a condition.

```javascript
// Find all users with active status
const activePaths = state.findAllWhere('active', 'is', true);
// Returns: [['users', 0], ['users', 2], ['users', 5]]

// Get all matching values
const activeUsers = activePaths.map(path => state.get(path));
```

**Note:** These methods search the entire state tree recursively and return **paths**, not values. Use `state.get(path)` to retrieve the actual data.

## Plugins

The state system has a powerful plugin architecture that enables features like undo/redo and change tracking.

### Installing Plugins

Plugins are installed using the `install()` method:

```javascript
import { createState } from '@jucio.io/state';
import { HistoryManager } from '@jucio.io/state/history';
import { Matcher } from '@jucio.io/state/matcher';

const state = createState();

// Install a single plugin
state.install(HistoryManager);

// Install multiple plugins
state.install(HistoryManager, Matcher);
```

### HistoryManager Plugin

Provides undo/redo functionality with change tracking.

```javascript
import { HistoryManager } from '@jucio.io/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 '@jucio.io/state/history';

// Configure with custom options
state.install(HistoryManager.configure({
  maxSize: 200  // Maximum history entries (default: 100)
}));
```

### Creating Custom Plugins

You can create custom plugins by extending the Plugin base class:

```javascript
import { Plugin } from '@jucio.io/state/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();
```

### Serialization

#### Export and Import

Both `export()` and `import()` are async methods that use CBOR encoding.

```javascript
// Export state to CBOR format (async)
const exported = await state.export();

// Import into new state
const newState = createState();
await newState.import(exported);

// Export specific path
const userExport = await state.export(['user']);
```

### Change Tracking

Change tracking is available through the `@jucio.io/state/on-change` plugin:

```javascript
import { createState } from '@jucio.io/state';
import { OnChange } from '@jucio.io/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();
```

## Advanced Usage

### Change Tracking

Listen to all state changes using the OnChange plugin:

```javascript
import { OnChange } from '@jucio.io/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();
```

### Custom Effects

Track specific state changes using the Matcher plugin:

```javascript
import { Matcher, createMatcher } from '@jucio.io/state/matcher';

state.install(Matcher);

const unsubscribe = state.matcher.createMatcher(['user'], (user) => {
  console.log('User data changed:', user);
});

// Clean up when done
unsubscribe();
```

### Complex State Structures

```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 }
  ]
});

// Find active users
const activePaths = state.findAllWhere('active', 'is', true);
const activeUsers = activePaths.map(path => state.get(path));

// Calculate total likes manually
const posts = state.get(['posts']);
const totalLikes = posts.reduce((sum, p) => sum + p.likes, 0);

// Get dashboard stats
const users = state.get(['users']);
const dashboardStats = {
  activeUserCount: users.filter(u => u.active).length,
  totalPosts: posts.length,
  totalLikes: totalLikes
};
```

### Performance Optimization

#### 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()
}));
```

## Best Practices

### 1. Use Path Arrays Consistently
```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
```

### 2. Batch Related Updates
```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. Use Standard JavaScript for Derived Data

```javascript
// ✅ Good - use get() and standard JS
const users = state.get(['users']);
const fullNames = users.map(u => `${u.firstName} ${u.lastName}`);

// ✅ Also good - compute on demand
function getFullName(userId) {
  const users = state.get(['users']);
  const user = users.find(u => u.id === userId);
  return user ? `${user.firstName} ${user.lastName}` : null;
}
```

### 4. Clean Up Effects and Listeners
```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:

### Benchmarks

```
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/sec

Plugins:
  HistoryManager:     ~12% overhead on writes, 0% on reads
```

### Performance Considerations

- **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 test

# Run tests in watch mode
npm run test:watch

# Run benchmarks
npm run bench
```

## License

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](./LICENSE) file for complete details.

## Contributing

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.
