fix: Gitea Traefik routing and connection pool optimization
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- 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
This commit is contained in:
556
resources/js/modules/state-manager/StateManager.js
Normal file
556
resources/js/modules/state-manager/StateManager.js
Normal file
@@ -0,0 +1,556 @@
|
||||
/**
|
||||
* State Management Module
|
||||
*
|
||||
* Provides centralized, reactive state management for client-side state.
|
||||
* Features:
|
||||
* - Reactive state store (similar to Redux/Vuex)
|
||||
* - State persistence (localStorage, sessionStorage)
|
||||
* - State synchronization across tabs (BroadcastChannel)
|
||||
* - Integration with LiveComponents
|
||||
* - Time-travel debugging support
|
||||
*/
|
||||
|
||||
import { Logger } from '../../core/logger.js';
|
||||
|
||||
/**
|
||||
* StateManager - Centralized state management
|
||||
*/
|
||||
export class StateManager {
|
||||
constructor(config = {}) {
|
||||
this.state = config.initialState || {};
|
||||
this.subscribers = new Map(); // Map<path, Set<callback>>
|
||||
this.middleware = [];
|
||||
this.history = []; // For time-travel debugging
|
||||
this.maxHistorySize = config.maxHistorySize || 50;
|
||||
this.enableHistory = config.enableHistory ?? false;
|
||||
|
||||
// Persistence configuration
|
||||
this.persistence = {
|
||||
enabled: config.persistence?.enabled ?? false,
|
||||
storage: config.persistence?.storage || 'localStorage', // 'localStorage' | 'sessionStorage'
|
||||
key: config.persistence?.key || 'app-state',
|
||||
paths: config.persistence?.paths || [] // Only persist these paths
|
||||
};
|
||||
|
||||
// Cross-tab synchronization
|
||||
this.sync = {
|
||||
enabled: config.sync?.enabled ?? false,
|
||||
channel: null
|
||||
};
|
||||
|
||||
// Load persisted state
|
||||
if (this.persistence.enabled) {
|
||||
this.loadPersistedState();
|
||||
}
|
||||
|
||||
// Initialize cross-tab sync
|
||||
if (this.sync.enabled && typeof BroadcastChannel !== 'undefined') {
|
||||
this.initCrossTabSync();
|
||||
}
|
||||
|
||||
Logger.info('[StateManager] Initialized', {
|
||||
persistence: this.persistence.enabled,
|
||||
sync: this.sync.enabled,
|
||||
history: this.enableHistory
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new StateManager instance
|
||||
*/
|
||||
static create(config = {}) {
|
||||
return new StateManager(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current state
|
||||
*/
|
||||
getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get state at a specific path
|
||||
*/
|
||||
get(path, defaultValue = undefined) {
|
||||
const keys = path.split('.');
|
||||
let value = this.state;
|
||||
|
||||
for (const key of keys) {
|
||||
if (value === null || value === undefined || typeof value !== 'object') {
|
||||
return defaultValue;
|
||||
}
|
||||
value = value[key];
|
||||
}
|
||||
|
||||
return value !== undefined ? value : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set state at a specific path
|
||||
*/
|
||||
set(path, value) {
|
||||
const keys = path.split('.');
|
||||
const newState = { ...this.state };
|
||||
let current = newState;
|
||||
|
||||
// Navigate/create nested structure
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i];
|
||||
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
|
||||
current[key] = {};
|
||||
} else {
|
||||
current[key] = { ...current[key] };
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
// Set the value
|
||||
const lastKey = keys[keys.length - 1];
|
||||
const oldValue = this.get(path);
|
||||
current[lastKey] = value;
|
||||
|
||||
// Apply middleware
|
||||
const action = { type: 'SET', path, value, oldValue };
|
||||
const processedAction = this.applyMiddleware(action);
|
||||
|
||||
if (processedAction === null) {
|
||||
return; // Middleware blocked the action
|
||||
}
|
||||
|
||||
// Update state
|
||||
this.state = newState;
|
||||
|
||||
// Save to history
|
||||
if (this.enableHistory) {
|
||||
this.addToHistory(action);
|
||||
}
|
||||
|
||||
// Persist if enabled
|
||||
if (this.persistence.enabled && this.shouldPersist(path)) {
|
||||
this.persistState();
|
||||
}
|
||||
|
||||
// Sync across tabs
|
||||
if (this.sync.enabled) {
|
||||
this.broadcastStateChange(action);
|
||||
}
|
||||
|
||||
// Notify subscribers
|
||||
this.notifySubscribers(path, value, oldValue);
|
||||
|
||||
Logger.debug('[StateManager] State updated', { path, value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch an action (similar to Redux)
|
||||
*/
|
||||
dispatch(action) {
|
||||
if (typeof action === 'function') {
|
||||
// Thunk support
|
||||
return action(this.dispatch.bind(this), this.getState.bind(this));
|
||||
}
|
||||
|
||||
if (!action.type) {
|
||||
throw new Error('Action must have a type property');
|
||||
}
|
||||
|
||||
// Apply middleware
|
||||
const processedAction = this.applyMiddleware(action);
|
||||
if (processedAction === null) {
|
||||
return; // Middleware blocked the action
|
||||
}
|
||||
|
||||
// Execute action (reducer pattern)
|
||||
const newState = this.reducer(this.state, processedAction);
|
||||
|
||||
if (newState !== this.state) {
|
||||
const oldState = this.state;
|
||||
this.state = newState;
|
||||
|
||||
// Save to history
|
||||
if (this.enableHistory) {
|
||||
this.addToHistory(processedAction);
|
||||
}
|
||||
|
||||
// Persist if enabled
|
||||
if (this.persistence.enabled) {
|
||||
this.persistState();
|
||||
}
|
||||
|
||||
// Sync across tabs
|
||||
if (this.sync.enabled) {
|
||||
this.broadcastStateChange(processedAction);
|
||||
}
|
||||
|
||||
// Notify all subscribers
|
||||
this.notifyAllSubscribers();
|
||||
}
|
||||
|
||||
Logger.debug('[StateManager] Action dispatched', processedAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default reducer (can be overridden)
|
||||
*/
|
||||
reducer(state, action) {
|
||||
// Default reducer handles SET actions
|
||||
if (action.type === 'SET' && action.path) {
|
||||
const keys = action.path.split('.');
|
||||
const newState = { ...state };
|
||||
let current = newState;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i];
|
||||
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
|
||||
current[key] = {};
|
||||
} else {
|
||||
current[key] = { ...current[key] };
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
current[keys[keys.length - 1]] = action.value;
|
||||
return newState;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to state changes
|
||||
*/
|
||||
subscribe(path, callback) {
|
||||
if (!this.subscribers.has(path)) {
|
||||
this.subscribers.set(path, new Set());
|
||||
}
|
||||
|
||||
this.subscribers.get(path).add(callback);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const callbacks = this.subscribers.get(path);
|
||||
if (callbacks) {
|
||||
callbacks.delete(callback);
|
||||
if (callbacks.size === 0) {
|
||||
this.subscribers.delete(path);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to all state changes
|
||||
*/
|
||||
subscribeAll(callback) {
|
||||
return this.subscribe('*', callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify subscribers of a state change
|
||||
*/
|
||||
notifySubscribers(path, newValue, oldValue) {
|
||||
// Notify path-specific subscribers
|
||||
const pathCallbacks = this.subscribers.get(path);
|
||||
if (pathCallbacks) {
|
||||
pathCallbacks.forEach(callback => {
|
||||
try {
|
||||
callback(newValue, oldValue, path);
|
||||
} catch (error) {
|
||||
Logger.error('[StateManager] Subscriber error', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Notify wildcard subscribers
|
||||
const wildcardCallbacks = this.subscribers.get('*');
|
||||
if (wildcardCallbacks) {
|
||||
wildcardCallbacks.forEach(callback => {
|
||||
try {
|
||||
callback(this.state, path);
|
||||
} catch (error) {
|
||||
Logger.error('[StateManager] Subscriber error', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Notify parent path subscribers
|
||||
const pathParts = path.split('.');
|
||||
for (let i = pathParts.length - 1; i > 0; i--) {
|
||||
const parentPath = pathParts.slice(0, i).join('.');
|
||||
const parentCallbacks = this.subscribers.get(parentPath);
|
||||
if (parentCallbacks) {
|
||||
parentCallbacks.forEach(callback => {
|
||||
try {
|
||||
callback(this.get(parentPath), parentPath);
|
||||
} catch (error) {
|
||||
Logger.error('[StateManager] Subscriber error', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all subscribers (for full state changes)
|
||||
*/
|
||||
notifyAllSubscribers() {
|
||||
this.subscribers.forEach((callbacks, path) => {
|
||||
callbacks.forEach(callback => {
|
||||
try {
|
||||
if (path === '*') {
|
||||
callback(this.state);
|
||||
} else {
|
||||
const value = this.get(path);
|
||||
callback(value, path);
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error('[StateManager] Subscriber error', error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add middleware
|
||||
*/
|
||||
use(middleware) {
|
||||
if (typeof middleware !== 'function') {
|
||||
throw new Error('Middleware must be a function');
|
||||
}
|
||||
this.middleware.push(middleware);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply middleware chain
|
||||
*/
|
||||
applyMiddleware(action) {
|
||||
let processedAction = action;
|
||||
|
||||
for (const middleware of this.middleware) {
|
||||
processedAction = middleware(processedAction, this.getState.bind(this));
|
||||
if (processedAction === null) {
|
||||
return null; // Middleware blocked the action
|
||||
}
|
||||
}
|
||||
|
||||
return processedAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load persisted state
|
||||
*/
|
||||
loadPersistedState() {
|
||||
try {
|
||||
const storage = this.persistence.storage === 'sessionStorage' ? sessionStorage : localStorage;
|
||||
const stored = storage.getItem(this.persistence.key);
|
||||
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
this.state = { ...this.state, ...parsed };
|
||||
Logger.info('[StateManager] Loaded persisted state');
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error('[StateManager] Failed to load persisted state', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist state
|
||||
*/
|
||||
persistState() {
|
||||
try {
|
||||
const storage = this.persistence.storage === 'sessionStorage' ? sessionStorage : localStorage;
|
||||
const stateToPersist = this.persistence.paths.length > 0
|
||||
? this.getPersistedPaths()
|
||||
: this.state;
|
||||
|
||||
storage.setItem(this.persistence.key, JSON.stringify(stateToPersist));
|
||||
Logger.debug('[StateManager] State persisted');
|
||||
} catch (error) {
|
||||
Logger.error('[StateManager] Failed to persist state', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get only paths that should be persisted
|
||||
*/
|
||||
getPersistedPaths() {
|
||||
const result = {};
|
||||
for (const path of this.persistence.paths) {
|
||||
const value = this.get(path);
|
||||
if (value !== undefined) {
|
||||
const keys = path.split('.');
|
||||
let current = result;
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
if (!(keys[i] in current)) {
|
||||
current[keys[i]] = {};
|
||||
}
|
||||
current = current[keys[i]];
|
||||
}
|
||||
current[keys[keys.length - 1]] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if path should be persisted
|
||||
*/
|
||||
shouldPersist(path) {
|
||||
if (this.persistence.paths.length === 0) {
|
||||
return true; // Persist all if no paths specified
|
||||
}
|
||||
return this.persistence.paths.some(p => path.startsWith(p));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize cross-tab synchronization
|
||||
*/
|
||||
initCrossTabSync() {
|
||||
try {
|
||||
this.sync.channel = new BroadcastChannel('state-manager-sync');
|
||||
|
||||
this.sync.channel.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'STATE_CHANGE') {
|
||||
this.handleRemoteStateChange(event.data.action);
|
||||
}
|
||||
});
|
||||
|
||||
Logger.info('[StateManager] Cross-tab sync initialized');
|
||||
} catch (error) {
|
||||
Logger.error('[StateManager] Failed to initialize cross-tab sync', error);
|
||||
this.sync.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast state change to other tabs
|
||||
*/
|
||||
broadcastStateChange(action) {
|
||||
if (this.sync.channel) {
|
||||
try {
|
||||
this.sync.channel.postMessage({
|
||||
type: 'STATE_CHANGE',
|
||||
action,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.error('[StateManager] Failed to broadcast state change', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle remote state change from another tab
|
||||
*/
|
||||
handleRemoteStateChange(action) {
|
||||
// Apply the action without broadcasting (to avoid loops)
|
||||
const wasSyncEnabled = this.sync.enabled;
|
||||
this.sync.enabled = false;
|
||||
|
||||
if (action.type === 'SET' && action.path) {
|
||||
this.set(action.path, action.value);
|
||||
} else {
|
||||
this.dispatch(action);
|
||||
}
|
||||
|
||||
this.sync.enabled = wasSyncEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add action to history
|
||||
*/
|
||||
addToHistory(action) {
|
||||
this.history.push({
|
||||
action,
|
||||
state: JSON.parse(JSON.stringify(this.state)),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Limit history size
|
||||
if (this.history.length > this.maxHistorySize) {
|
||||
this.history.shift();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get history
|
||||
*/
|
||||
getHistory() {
|
||||
return [...this.history];
|
||||
}
|
||||
|
||||
/**
|
||||
* Time-travel to a specific history point
|
||||
*/
|
||||
timeTravel(index) {
|
||||
if (index < 0 || index >= this.history.length) {
|
||||
throw new Error('Invalid history index');
|
||||
}
|
||||
|
||||
const historyPoint = this.history[index];
|
||||
this.state = JSON.parse(JSON.stringify(historyPoint.state));
|
||||
this.notifyAllSubscribers();
|
||||
|
||||
Logger.info('[StateManager] Time-traveled to history point', index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset state to initial state
|
||||
*/
|
||||
reset() {
|
||||
this.state = {};
|
||||
this.history = [];
|
||||
this.notifyAllSubscribers();
|
||||
|
||||
if (this.persistence.enabled) {
|
||||
const storage = this.persistence.storage === 'sessionStorage' ? sessionStorage : localStorage;
|
||||
storage.removeItem(this.persistence.key);
|
||||
}
|
||||
|
||||
Logger.info('[StateManager] State reset');
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy state manager
|
||||
*/
|
||||
destroy() {
|
||||
// Unsubscribe all
|
||||
this.subscribers.clear();
|
||||
|
||||
// Close cross-tab sync
|
||||
if (this.sync.channel) {
|
||||
this.sync.channel.close();
|
||||
this.sync.channel = null;
|
||||
}
|
||||
|
||||
// Clear history
|
||||
this.history = [];
|
||||
|
||||
Logger.info('[StateManager] Destroyed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a global state manager instance
|
||||
*/
|
||||
let globalStateManager = null;
|
||||
|
||||
/**
|
||||
* Get or create global state manager
|
||||
*/
|
||||
export function getGlobalStateManager(config = {}) {
|
||||
if (!globalStateManager) {
|
||||
globalStateManager = StateManager.create(config);
|
||||
}
|
||||
return globalStateManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a scoped state manager (for component-specific state)
|
||||
*/
|
||||
export function createScopedStateManager(config = {}) {
|
||||
return StateManager.create(config);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user