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
246 lines
6.0 KiB
JavaScript
246 lines
6.0 KiB
JavaScript
/**
|
|
* State Serializer for LiveComponents
|
|
*
|
|
* Provides type-safe state serialization/deserialization with versioning
|
|
* and validation.
|
|
*/
|
|
|
|
/**
|
|
* Serialize component state
|
|
*
|
|
* @param {LiveComponentState} state - Component state
|
|
* @returns {string} Serialized state JSON
|
|
*/
|
|
export function serializeState(state) {
|
|
// Validate state structure
|
|
if (!state || typeof state !== 'object') {
|
|
throw new Error('State must be an object');
|
|
}
|
|
|
|
if (!state.id || typeof state.id !== 'string') {
|
|
throw new Error('State must have a valid id');
|
|
}
|
|
|
|
if (!state.component || typeof state.component !== 'string') {
|
|
throw new Error('State must have a valid component name');
|
|
}
|
|
|
|
if (typeof state.version !== 'number' || state.version < 1) {
|
|
throw new Error('State must have a valid version number');
|
|
}
|
|
|
|
// Ensure data is an object
|
|
const data = state.data || {};
|
|
if (typeof data !== 'object' || Array.isArray(data)) {
|
|
throw new Error('State data must be an object');
|
|
}
|
|
|
|
// Create serializable state
|
|
const serializableState = {
|
|
id: state.id,
|
|
component: state.component,
|
|
data: data,
|
|
version: state.version
|
|
};
|
|
|
|
// Serialize to JSON
|
|
try {
|
|
return JSON.stringify(serializableState);
|
|
} catch (error) {
|
|
throw new Error(`Failed to serialize state: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deserialize component state
|
|
*
|
|
* @param {string} json - Serialized state JSON
|
|
* @returns {LiveComponentState} Deserialized state
|
|
*/
|
|
export function deserializeState(json) {
|
|
if (typeof json !== 'string') {
|
|
throw new Error('State JSON must be a string');
|
|
}
|
|
|
|
if (json.trim() === '') {
|
|
// Return empty state
|
|
return {
|
|
id: '',
|
|
component: '',
|
|
data: {},
|
|
version: 1
|
|
};
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(json);
|
|
|
|
// Validate structure
|
|
if (!parsed || typeof parsed !== 'object') {
|
|
throw new Error('Invalid state structure');
|
|
}
|
|
|
|
// Extract state data
|
|
const id = parsed.id || '';
|
|
const component = parsed.component || '';
|
|
const data = parsed.data || {};
|
|
const version = typeof parsed.version === 'number' ? parsed.version : 1;
|
|
|
|
// Validate data is an object
|
|
if (typeof data !== 'object' || Array.isArray(data)) {
|
|
throw new Error('State data must be an object');
|
|
}
|
|
|
|
return {
|
|
id,
|
|
component,
|
|
data,
|
|
version
|
|
};
|
|
} catch (error) {
|
|
if (error instanceof SyntaxError) {
|
|
throw new Error(`Invalid JSON format: ${error.message}`);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create state diff (changes between two states)
|
|
*
|
|
* @param {LiveComponentState} oldState - Previous state
|
|
* @param {LiveComponentState} newState - New state
|
|
* @returns {Object} State diff
|
|
*/
|
|
export function createStateDiff(oldState, newState) {
|
|
const diff = {
|
|
changed: false,
|
|
added: {},
|
|
removed: {},
|
|
modified: {},
|
|
versionChange: newState.version - oldState.version
|
|
};
|
|
|
|
const oldData = oldState.data || {};
|
|
const newData = newState.data || {};
|
|
|
|
// Find added keys
|
|
for (const key in newData) {
|
|
if (!(key in oldData)) {
|
|
diff.added[key] = newData[key];
|
|
diff.changed = true;
|
|
}
|
|
}
|
|
|
|
// Find removed keys
|
|
for (const key in oldData) {
|
|
if (!(key in newData)) {
|
|
diff.removed[key] = oldData[key];
|
|
diff.changed = true;
|
|
}
|
|
}
|
|
|
|
// Find modified keys
|
|
for (const key in newData) {
|
|
if (key in oldData) {
|
|
const oldValue = oldData[key];
|
|
const newValue = newData[key];
|
|
|
|
// Deep comparison for objects
|
|
if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
|
|
diff.modified[key] = {
|
|
old: oldValue,
|
|
new: newValue
|
|
};
|
|
diff.changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return diff;
|
|
}
|
|
|
|
/**
|
|
* Merge state changes
|
|
*
|
|
* @param {LiveComponentState} baseState - Base state
|
|
* @param {Object} changes - State changes to apply
|
|
* @returns {LiveComponentState} Merged state
|
|
*/
|
|
export function mergeStateChanges(baseState, changes) {
|
|
const baseData = baseState.data || {};
|
|
const mergedData = { ...baseData, ...changes };
|
|
|
|
return {
|
|
...baseState,
|
|
data: mergedData,
|
|
version: baseState.version + 1
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate state structure
|
|
*
|
|
* @param {LiveComponentState} state - State to validate
|
|
* @returns {boolean} True if valid
|
|
*/
|
|
export function validateState(state) {
|
|
if (!state || typeof state !== 'object') {
|
|
return false;
|
|
}
|
|
|
|
if (!state.id || typeof state.id !== 'string') {
|
|
return false;
|
|
}
|
|
|
|
if (!state.component || typeof state.component !== 'string') {
|
|
return false;
|
|
}
|
|
|
|
if (typeof state.version !== 'number' || state.version < 1) {
|
|
return false;
|
|
}
|
|
|
|
if (state.data && (typeof state.data !== 'object' || Array.isArray(state.data))) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get state from DOM element
|
|
*
|
|
* @param {HTMLElement} element - Component element
|
|
* @returns {LiveComponentState|null} State or null if not found
|
|
*/
|
|
export function getStateFromElement(element) {
|
|
const stateJson = element.dataset.state;
|
|
if (!stateJson) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return deserializeState(stateJson);
|
|
} catch (error) {
|
|
console.error('[StateSerializer] Failed to deserialize state from element:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set state on DOM element
|
|
*
|
|
* @param {HTMLElement} element - Component element
|
|
* @param {LiveComponentState} state - State to set
|
|
*/
|
|
export function setStateOnElement(element, state) {
|
|
if (!validateState(state)) {
|
|
throw new Error('Invalid state structure');
|
|
}
|
|
|
|
const stateJson = serializeState(state);
|
|
element.dataset.state = stateJson;
|
|
}
|
|
|