- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
8.9 KiB
State Manager Module
Centralized, Reactive State Management for Client-Side State
The State Manager Module provides a centralized state management system similar to Redux or Vuex, with support for state persistence, cross-tab synchronization, and integration with LiveComponents.
Features
- Reactive State Store - Subscribe to state changes and react automatically
- State Persistence - Automatically save and restore state from localStorage or sessionStorage
- Cross-Tab Synchronization - Keep state synchronized across browser tabs
- Integration with LiveComponents - Seamless integration with LiveComponent state
- Time-Travel Debugging - Debug state changes with history and time-travel
- Middleware Support - Extend functionality with custom middleware
Quick Start
Basic Usage
import { StateManager } from './modules/state-manager/index.js';
// Create a state manager
const store = StateManager.create({
initialState: {
user: { name: '', email: '' },
cart: { items: [], total: 0 },
ui: { sidebarOpen: false }
}
});
// Set state
store.set('user.name', 'John Doe');
store.set('cart.items', [{ id: 1, name: 'Product' }]);
// Get state
const userName = store.get('user.name');
const cartItems = store.get('cart.items', []);
// Subscribe to changes
const unsubscribe = store.subscribe('cart.items', (items) => {
console.log('Cart items changed:', items);
updateCartUI(items);
});
// Unsubscribe
unsubscribe();
Module System Integration
<!-- Enable global state manager -->
<script type="module">
import { init } from './modules/state-manager/index.js';
init({
initialState: {
user: {},
cart: {}
},
persistence: {
enabled: true,
storage: 'localStorage',
key: 'app-state'
}
});
</script>
<!-- Access globally -->
<script>
window.StateManager.set('user.name', 'John');
const name = window.StateManager.get('user.name');
</script>
API Reference
StateManager.create(config)
Create a new StateManager instance.
Parameters:
config.initialState- Initial state objectconfig.maxHistorySize- Maximum history size (default: 50)config.enableHistory- Enable history for time-travel (default: false)config.persistence.enabled- Enable state persistence (default: false)config.persistence.storage- Storage type: 'localStorage' or 'sessionStorage' (default: 'localStorage')config.persistence.key- Storage key (default: 'app-state')config.persistence.paths- Array of paths to persist (empty = all)config.sync.enabled- Enable cross-tab synchronization (default: false)
Example:
const store = StateManager.create({
initialState: { user: {}, cart: {} },
persistence: {
enabled: true,
storage: 'localStorage',
paths: ['user', 'cart'] // Only persist these paths
},
sync: {
enabled: true // Sync across tabs
}
});
store.getState()
Get the entire state object.
Returns: Record<string, any>
store.get(path, defaultValue)
Get state at a specific path.
Parameters:
path- Dot-separated path (e.g., 'user.name')defaultValue- Default value if path doesn't exist
Returns: any
Example:
const userName = store.get('user.name', 'Guest');
const cartTotal = store.get('cart.total', 0);
store.set(path, value)
Set state at a specific path.
Parameters:
path- Dot-separated pathvalue- Value to set
Example:
store.set('user.name', 'John Doe');
store.set('cart.items', [{ id: 1, name: 'Product' }]);
store.dispatch(action)
Dispatch an action (Redux-style).
Parameters:
action- Action object withtypeproperty, or a thunk function
Example:
// Simple action
store.dispatch({
type: 'ADD_TO_CART',
productId: 123,
quantity: 1
});
// Thunk (async action)
store.dispatch(async (dispatch, getState) => {
const response = await fetch('/api/products');
const products = await response.json();
dispatch({ type: 'SET_PRODUCTS', products });
});
store.subscribe(path, callback)
Subscribe to state changes at a specific path.
Parameters:
path- Dot-separated path, or '*' for all changescallback- Callback function:(newValue, oldValue, path) => void
Returns: Unsubscribe function
Example:
const unsubscribe = store.subscribe('cart.items', (items, oldItems, path) => {
console.log(`Cart items changed at ${path}:`, items);
updateCartUI(items);
});
// Later...
unsubscribe();
store.subscribeAll(callback)
Subscribe to all state changes.
Parameters:
callback- Callback function:(state) => void
Returns: Unsubscribe function
store.use(middleware)
Add middleware to the state manager.
Parameters:
middleware- Middleware function:(action, getState) => action | null
Example:
// Logging middleware
store.use((action, getState) => {
console.log('Action:', action);
console.log('Current state:', getState());
return action; // Return action to continue, or null to block
});
// Validation middleware
store.use((action, getState) => {
if (action.type === 'SET' && action.path === 'user.email') {
if (!isValidEmail(action.value)) {
console.error('Invalid email');
return null; // Block the action
}
}
return action;
});
store.getHistory()
Get action history for time-travel debugging.
Returns: Array<HistoryPoint>
store.timeTravel(index)
Time-travel to a specific history point.
Parameters:
index- History index
store.reset()
Reset state to initial state.
store.destroy()
Destroy the state manager and clean up resources.
Integration with LiveComponents
import { StateManager } from './modules/state-manager/index.js';
import { LiveComponentManager } from './modules/livecomponent/index.js';
const store = StateManager.create();
const lcManager = LiveComponentManager.getInstance();
// Sync LiveComponent state with StateManager
lcManager.on('component:state-updated', (componentId, state) => {
store.set(`livecomponents.${componentId}`, state);
});
// Update LiveComponent from StateManager
store.subscribe('livecomponents', (state) => {
Object.entries(state).forEach(([componentId, componentState]) => {
lcManager.updateComponentState(componentId, componentState);
});
});
Use Cases
User Preferences
const store = StateManager.create({
initialState: {
preferences: {
theme: 'light',
language: 'en',
notifications: true
}
},
persistence: {
enabled: true,
storage: 'localStorage',
paths: ['preferences']
}
});
// Save preference
store.set('preferences.theme', 'dark');
// Load preference
const theme = store.get('preferences.theme', 'light');
Shopping Cart
const store = StateManager.create({
initialState: {
cart: {
items: [],
total: 0
}
},
persistence: {
enabled: true,
storage: 'sessionStorage',
paths: ['cart']
},
sync: {
enabled: true // Sync cart across tabs
}
});
// Add item
store.set('cart.items', [
...store.get('cart.items', []),
{ id: 1, name: 'Product', price: 99.99 }
]);
// Calculate total
store.subscribe('cart.items', (items) => {
const total = items.reduce((sum, item) => sum + item.price, 0);
store.set('cart.total', total);
});
UI State
const store = StateManager.create({
initialState: {
ui: {
sidebarOpen: false,
modalOpen: false,
activeTab: 'home'
}
}
});
// Toggle sidebar
store.set('ui.sidebarOpen', !store.get('ui.sidebarOpen'));
// Subscribe to UI changes
store.subscribe('ui', (uiState) => {
updateUI(uiState);
});
Best Practices
- Use Scoped State Managers - Create separate state managers for different features
- Persist Only Necessary Data - Use
pathsto limit what gets persisted - Use Middleware for Cross-Cutting Concerns - Logging, validation, etc.
- Subscribe Selectively - Only subscribe to paths you need
- Clean Up Subscriptions - Always call unsubscribe when done
Performance Considerations
- State updates are synchronous and immediate
- Subscriptions are called synchronously (be careful with expensive operations)
- Persistence is debounced internally
- Cross-tab sync uses BroadcastChannel (efficient)
Browser Support
- Chrome/Edge: 38+
- Firefox: 38+
- Safari: 15.4+
- Mobile: iOS 15.4+, Android Chrome 38+
Required Features:
- ES2020 JavaScript
- BroadcastChannel (for cross-tab sync)
- localStorage/sessionStorage (for persistence)
Next: Validation Module →