All files / client/src/hydration state-serializer.js

93.47% Statements 43/46
97.14% Branches 34/35
100% Functions 5/5
94.73% Lines 36/38

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125                            22x     18x 18x   18x 32x 27x 27x         18x   16x 16x   16x                           39x   14x 14x 14x   2x 2x                     26x 2x     24x 24x               41x 40x 39x 37x 35x     35x 4x     31x     6x     25x             4x                   2x   2x 1x             2x    
/**
 * State serialization utilities for Coherent.js hydration
 *
 * Uses base64 encoding to safely embed state in data attributes
 * without escaping issues.
 */
 
/**
 * Serialize component state to base64-encoded JSON string
 *
 * @param {Object} state - Component state object
 * @returns {string|null} - Base64 encoded state or null if empty/invalid
 */
export function serializeState(state) {
  if (!state || typeof state !== 'object') return null;
 
  // Filter out non-serializable values (functions, symbols, undefined)
  const serializable = {};
  let hasSerializable = false;
 
  for (const [key, value] of Object.entries(state)) {
    if (isSerializable(value)) {
      serializable[key] = value;
      hasSerializable = true;
    }
    // Silently omit functions, symbols, undefined - they reconstruct on hydrate
  }
 
  if (!hasSerializable) return null;
 
  try {
    const json = JSON.stringify(serializable);
    // Use encodeURIComponent to handle unicode, then btoa for base64
    return btoa(encodeURIComponent(json));
  } catch (e) {
    console.warn('[Coherent.js] Failed to serialize state:', e);
    return null;
  }
}
 
/**
 * Deserialize state from base64-encoded JSON string
 *
 * @param {string} encoded - Base64 encoded state string
 * @returns {Object|null} - Deserialized state or null if invalid
 */
export function deserializeState(encoded) {
  if (!encoded || typeof encoded !== 'string') return null;
 
  try {
    const json = decodeURIComponent(atob(encoded));
    return JSON.parse(json);
  } catch (e) {
    console.warn('[Coherent.js] Failed to deserialize state:', e);
    return null;
  }
}
 
/**
 * Extract state from a DOM element's data-state attribute
 *
 * @param {HTMLElement} element - DOM element to extract state from
 * @returns {Object|null} - Extracted state or null
 */
export function extractState(element) {
  if (!element || typeof element.getAttribute !== 'function') {
    return null;
  }
 
  const encoded = element.getAttribute('data-state');
  return deserializeState(encoded);
}
 
/**
 * Check if a value is serializable to JSON
 * @private
 */
function isSerializable(value) {
  if (value === undefined) return false;
  if (value === null) return true;
  if (typeof value === 'function') return false;
  if (typeof value === 'symbol') return false;
  Iif (typeof value === 'bigint') return false; // BigInt not JSON serializable
 
  // Arrays and objects need recursive check
  if (Array.isArray(value)) {
    return value.every(isSerializable);
  }
 
  if (typeof value === 'object') {
    // Check for circular references would be expensive here
    // JSON.stringify will catch them in serializeState
    return true;
  }
 
  return true; // primitives (string, number, boolean)
}
 
/**
 * Size warning threshold (bytes)
 * Warn if serialized state exceeds this
 */
const STATE_SIZE_WARNING_THRESHOLD = 10 * 1024; // 10KB
 
/**
 * Serialize state with size warning
 *
 * @param {Object} state - Component state
 * @param {string} componentName - Component name for warning message
 * @returns {string|null} - Serialized state
 */
export function serializeStateWithWarning(state, componentName = 'Unknown') {
  const encoded = serializeState(state);
 
  if (encoded && encoded.length > STATE_SIZE_WARNING_THRESHOLD) {
    console.warn(
      `[Coherent.js] Large state detected for component "${componentName}": ` +
      `${Math.round(encoded.length / 1024)}KB. Consider using a state management ` +
      `solution for large datasets.`
    );
  }
 
  return encoded;
}