@coherent.js/core 1.1.2 → 2.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -473,13 +473,25 @@ function createPerformanceMonitor(options = {}) {
473
473
  if (startTime !== void 0) {
474
474
  const duration = performance.now() - startTime;
475
475
  _renderTimers.delete(id);
476
- recordMetric("render", duration);
476
+ recordMetric("renderTime", duration);
477
477
  return duration;
478
478
  }
479
479
  return 0;
480
480
  }
481
+ function recordRender(operation, duration, fromCache = false, metadata = {}) {
482
+ if (operation === "render") {
483
+ recordMetric("renderTime", duration, { fromCache, ...metadata });
484
+ } else {
485
+ recordMetric("componentCount", 1, { operation, fromCache });
486
+ }
487
+ }
488
+ function recordError(operation, error, metadata = {}) {
489
+ recordMetric("errorCount", 1, { operation, error: error?.message, ...metadata });
490
+ }
481
491
  return {
482
492
  recordMetric,
493
+ recordRender,
494
+ recordError,
483
495
  measure,
484
496
  measureAsync,
485
497
  addMetric,
@@ -506,95 +518,98 @@ function createPerformanceMonitor(options = {}) {
506
518
  var performanceMonitor = createPerformanceMonitor();
507
519
 
508
520
  // src/core/html-utils.js
521
+ var HTML_ESCAPE_TEST = /[&<>"']/;
522
+ var HTML_ESCAPE = /[&<>"']/g;
523
+ var HTML_ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
509
524
  function escapeHtml(text) {
510
525
  if (typeof text !== "string") return text;
511
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
526
+ if (!HTML_ESCAPE_TEST.test(text)) return text;
527
+ return text.replace(HTML_ESCAPE, (ch) => HTML_ESCAPES[ch]);
528
+ }
529
+ var TRUSTED_CONTENT = /* @__PURE__ */ Symbol.for("coherent.js.trustedContent");
530
+ function createTrustedContent(content) {
531
+ const marker = { __html: String(content), __trusted: true };
532
+ Object.defineProperty(marker, TRUSTED_CONTENT, { value: true });
533
+ return Object.freeze(marker);
512
534
  }
513
535
  function isTrustedContent(value) {
514
- return Boolean(value) && typeof value === "object" && value.__trusted === true && typeof value.__html === "string";
536
+ return Boolean(value) && typeof value === "object" && value[TRUSTED_CONTENT] === true && typeof value.__html === "string";
537
+ }
538
+ var INVALID_ATTRIBUTE_NAME = /[\s"'<>/=\u0000-\u001F\u007F-\u009F]/;
539
+ function isValidAttributeName(name) {
540
+ return typeof name === "string" && name.length > 0 && !INVALID_ATTRIBUTE_NAME.test(name);
515
541
  }
516
542
  function isVoidElement(tagName) {
517
543
  if (typeof tagName !== "string") {
518
544
  return false;
519
545
  }
520
- const voidElements = /* @__PURE__ */ new Set([
521
- "area",
522
- "base",
523
- "br",
524
- "col",
525
- "embed",
526
- "hr",
527
- "img",
528
- "input",
529
- "link",
530
- "meta",
531
- "param",
532
- "source",
533
- "track",
534
- "wbr"
535
- ]);
536
- return voidElements.has(tagName.toLowerCase());
546
+ return voidElements.has(tagName) || voidElements.has(tagName.toLowerCase());
547
+ }
548
+ var ENUMERATED_BOOLEAN_ATTRIBUTES = /* @__PURE__ */ new Set(["spellcheck", "draggable", "contenteditable"]);
549
+ function normalizeClassValue(value) {
550
+ if (Array.isArray(value)) {
551
+ return value.map(normalizeClassValue).filter(Boolean).join(" ");
552
+ }
553
+ if (value && typeof value === "object") {
554
+ return Object.keys(value).filter((name) => value[name]).join(" ");
555
+ }
556
+ if (value === null || value === void 0 || value === false) return "";
557
+ return String(value);
537
558
  }
538
- function formatAttributes(props) {
559
+ function callAttribute(key, value) {
560
+ try {
561
+ return value();
562
+ } catch (_error) {
563
+ console.warn(`Error executing function for attribute '${key}':`, {
564
+ error: _error.message,
565
+ stack: _error.stack,
566
+ attributeKey: key
567
+ });
568
+ return "";
569
+ }
570
+ }
571
+ var ATTRIBUTE_NAMES = {
572
+ className: "class",
573
+ // Written as is, browsers read `htmlFor` as an unknown `htmlfor`
574
+ // attribute: the label was not associated with its control.
575
+ htmlFor: "for"
576
+ };
577
+ function styleToCss(style) {
578
+ return Object.entries(style).filter(([, val]) => val !== null && val !== void 0 && val !== false).map(([prop, val]) => {
579
+ const name = prop.startsWith("--") ? prop : prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
580
+ return `${name}: ${val}`;
581
+ }).join("; ");
582
+ }
583
+ function formatAttributes(props, skip) {
584
+ if (props && props.class !== void 0 && props.className !== void 0) {
585
+ const { className, ...rest } = props;
586
+ const resolve = (key, value) => normalizeClassValue(typeof value === "function" ? callAttribute(key, value) : value);
587
+ props = { ...rest, class: [resolve("class", props.class), resolve("className", className)].filter(Boolean).join(" ") };
588
+ }
539
589
  let formatted = "";
540
590
  for (const key in props) {
541
- if (props.hasOwnProperty(key)) {
591
+ if (Object.prototype.hasOwnProperty.call(props, key) && !(skip && skip.has(key))) {
542
592
  let value = props[key];
543
- const attributeName = key === "className" ? "class" : key;
593
+ const attributeName = ATTRIBUTE_NAMES[key] ?? key;
594
+ if (!isValidAttributeName(attributeName)) {
595
+ throw new Error(`Invalid attribute name ${JSON.stringify(key)}: attribute names cannot contain whitespace, quotes, '<', '>', '/', '=' or control characters`);
596
+ }
544
597
  if (typeof value === "function") {
545
598
  if (attributeName.startsWith("on")) {
546
- const actionId = `__coherent_action_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
547
- const DEBUG = typeof process !== "undefined" && process && process.env && (process.env.COHERENT_DEBUG === "1" || true) || typeof window !== "undefined" && window && window.COHERENT_DEBUG === true;
548
- if (typeof global !== "undefined") {
549
- if (!global.__coherentActionRegistry) {
550
- global.__coherentActionRegistry = {};
551
- if (DEBUG) console.log("Initialized global action registry");
552
- }
553
- global.__coherentActionRegistry[actionId] = value;
554
- if (DEBUG) console.log(`Added action ${actionId} to global registry, total: ${Object.keys(global.__coherentActionRegistry).length}`);
555
- if (DEBUG) console.log(`Global registry keys: ${Object.keys(global.__coherentActionRegistry).join(", ")}`);
556
- if (DEBUG) {
557
- if (typeof global.__coherentActionRegistryLog === "undefined") {
558
- global.__coherentActionRegistryLog = [];
559
- }
560
- global.__coherentActionRegistryLog.push({
561
- action: "add",
562
- actionId,
563
- timestamp: Date.now(),
564
- registrySize: Object.keys(global.__coherentActionRegistry).length
565
- });
566
- }
567
- } else if (typeof window !== "undefined") {
568
- if (!window.__coherentActionRegistry) {
569
- window.__coherentActionRegistry = {};
570
- if (DEBUG) console.log("Initialized window action registry");
571
- }
572
- window.__coherentActionRegistry[actionId] = value;
573
- if (DEBUG) console.log(`Added action ${actionId} to window registry, total: ${Object.keys(window.__coherentActionRegistry).length}`);
574
- if (DEBUG) console.log(`Window registry keys: ${Object.keys(window.__coherentActionRegistry).join(", ")}`);
575
- }
576
- const eventType = attributeName.substring(2);
577
- formatted += ` data-action="${actionId}" data-event="${eventType}"`;
578
599
  continue;
579
600
  } else {
580
- try {
581
- value = value();
582
- } catch (_error) {
583
- console.warn(`Error executing function for attribute '${key}':`, {
584
- _error: _error.message,
585
- stack: _error.stack,
586
- attributeKey: key
587
- });
588
- value = "";
589
- }
601
+ value = callAttribute(key, value);
590
602
  }
591
603
  }
604
+ if (attributeName === "class" && typeof value === "object" && value !== null) {
605
+ value = normalizeClassValue(value);
606
+ }
607
+ if (typeof value === "boolean" && (attributeName.startsWith("aria-") || ENUMERATED_BOOLEAN_ATTRIBUTES.has(attributeName.toLowerCase()))) {
608
+ value = String(value);
609
+ }
592
610
  if (attributeName === "style" && typeof value === "object" && value !== null) {
593
- const cssString = Object.entries(value).map(([prop, val]) => {
594
- const kebabProp = prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
595
- return `${kebabProp}: ${val}`;
596
- }).join("; ");
597
- formatted += ` ${attributeName}="${escapeHtml(cssString)}"`;
611
+ const cssString = styleToCss(value);
612
+ if (cssString) formatted += ` ${attributeName}="${escapeHtml(cssString)}"`;
598
613
  } else if (value === true) {
599
614
  formatted += ` ${attributeName}`;
600
615
  } else if (value !== false && value !== null && value !== void 0) {
@@ -616,10 +631,69 @@ function stripComments(html) {
616
631
  cursor = end + 3;
617
632
  }
618
633
  }
634
+ function endsInComment(html) {
635
+ let cursor = 0;
636
+ for (; ; ) {
637
+ const start = html.indexOf("<!--", cursor);
638
+ if (start === -1) return false;
639
+ const end = html.indexOf("-->", start + 4);
640
+ if (end === -1 || end + 3 === html.length) return true;
641
+ cursor = end + 3;
642
+ }
643
+ }
644
+ function createStreamMinifier() {
645
+ let pending = "";
646
+ let started = false;
647
+ const minifyPiece = (text, last) => {
648
+ let out = stripComments(started ? `>${text}` : text).replace(/\s+/g, " ").replace(/>\s+</g, "><");
649
+ if (started) {
650
+ out = out.slice(1);
651
+ } else {
652
+ out = out.trimStart();
653
+ }
654
+ if (last) out = out.trimEnd();
655
+ started = true;
656
+ return out;
657
+ };
658
+ return {
659
+ push(chunk) {
660
+ pending += chunk;
661
+ let cut = pending.lastIndexOf(">");
662
+ while (cut !== -1 && endsInComment(pending.slice(0, cut + 1))) {
663
+ cut = cut === 0 ? -1 : pending.lastIndexOf(">", cut - 1);
664
+ }
665
+ if (cut === -1) return "";
666
+ const head = pending.slice(0, cut + 1);
667
+ pending = pending.slice(cut + 1);
668
+ return minifyPiece(head, false);
669
+ },
670
+ end() {
671
+ const rest = pending;
672
+ pending = "";
673
+ return rest || !started ? minifyPiece(rest, true) : "";
674
+ }
675
+ };
676
+ }
619
677
  function minifyHtml(html, options = {}) {
620
678
  if (!options.minify) return html;
621
679
  return stripComments(html).replace(/\s+/g, " ").replace(/>\s+</g, "><").trim();
622
680
  }
681
+ var voidElements = /* @__PURE__ */ new Set([
682
+ "area",
683
+ "base",
684
+ "br",
685
+ "col",
686
+ "embed",
687
+ "hr",
688
+ "img",
689
+ "input",
690
+ "link",
691
+ "meta",
692
+ "param",
693
+ "source",
694
+ "track",
695
+ "wbr"
696
+ ]);
623
697
 
624
698
  // src/core/object-utils.js
625
699
  function deepClone(obj, seen = /* @__PURE__ */ new WeakMap()) {
@@ -706,6 +780,9 @@ function validateComponent(component, path = "root") {
706
780
  if (typeof component === "function") {
707
781
  return true;
708
782
  }
783
+ if (isTrustedContent(component)) {
784
+ return true;
785
+ }
709
786
  if (Array.isArray(component)) {
710
787
  component.forEach((child, index) => {
711
788
  validateComponent(child, `${path}[${index}]`);
@@ -747,22 +824,6 @@ function isCoherentObject(obj) {
747
824
  return /^[a-zA-Z][a-zA-Z0-9-]*$/.test(key);
748
825
  });
749
826
  }
750
- function extractProps(coherentObj) {
751
- if (!isCoherentObject(coherentObj)) {
752
- return {};
753
- }
754
- const props = {};
755
- const keys = Object.keys(coherentObj);
756
- keys.forEach((tag) => {
757
- const value = coherentObj[tag];
758
- if (value && typeof value === "object" && !Array.isArray(value)) {
759
- props[tag] = { ...value };
760
- } else {
761
- props[tag] = { text: value };
762
- }
763
- });
764
- return props;
765
- }
766
827
  function hasChildren(component) {
767
828
  if (Array.isArray(component)) {
768
829
  return component.length > 0;
@@ -784,6 +845,15 @@ function normalizeChildren(children) {
784
845
  return [];
785
846
  }
786
847
  if (Array.isArray(children)) {
848
+ let clean = true;
849
+ for (let i = 0; i < children.length; i++) {
850
+ const child = children[i];
851
+ if (child === null || child === void 0 || Array.isArray(child)) {
852
+ clean = false;
853
+ break;
854
+ }
855
+ }
856
+ if (clean) return children;
787
857
  return children.flat().filter((child) => child !== null && child !== void 0);
788
858
  }
789
859
  return [children];
@@ -797,7 +867,9 @@ var DEFAULT_RENDERER_CONFIG = {
797
867
  enableMonitoring: false,
798
868
  validateInput: true,
799
869
  // HTML Renderer specific options
800
- enableCache: true,
870
+ // Off by default: keying a render on its content costs a full walk of
871
+ // the tree, which only pays off when identical trees are re-rendered.
872
+ enableCache: false,
801
873
  minify: false,
802
874
  cacheSize: 1e3,
803
875
  cacheTTL: 3e5,
@@ -883,7 +955,7 @@ var BaseRenderer = class {
883
955
  return {
884
956
  ...baseConfig,
885
957
  // HTML-specific defaults
886
- enableCache: baseConfig.enableCache !== false,
958
+ enableCache: baseConfig.enableCache === true,
887
959
  enableMonitoring: baseConfig.enableMonitoring !== false
888
960
  };
889
961
  case "streaming":
@@ -916,11 +988,14 @@ var BaseRenderer = class {
916
988
  /**
917
989
  * Check if component is valid for rendering
918
990
  */
919
- isValidComponent(component) {
991
+ isValidComponent(component, depth = 0) {
920
992
  if (component === null || component === void 0) return true;
921
993
  if (typeof component === "string" || typeof component === "number") return true;
922
994
  if (typeof component === "function") return true;
923
- if (Array.isArray(component)) return component.every((child) => this.isValidComponent(child));
995
+ if (Array.isArray(component)) {
996
+ if (depth >= this.config.maxDepth) return true;
997
+ return component.every((child) => this.isValidComponent(child, depth + 1));
998
+ }
924
999
  if (isCoherentObject(component)) return true;
925
1000
  return false;
926
1001
  }
@@ -942,7 +1017,10 @@ var BaseRenderer = class {
942
1017
  if (typeof component === "string") {
943
1018
  return { type: "text", value: component };
944
1019
  }
945
- if (typeof component === "number" || typeof component === "boolean") {
1020
+ if (typeof component === "boolean") {
1021
+ return { type: "empty", value: "" };
1022
+ }
1023
+ if (typeof component === "number") {
946
1024
  return { type: "text", value: String(component) };
947
1025
  }
948
1026
  if (typeof component === "function") {
@@ -957,31 +1035,23 @@ var BaseRenderer = class {
957
1035
  return { type: "unknown", value: component };
958
1036
  }
959
1037
  /**
960
- * Execute function components with _error handling
1038
+ * Execute function components. Errors propagate: they used to be
1039
+ * swallowed here (the component rendered as nothing, logged only when
1040
+ * NODE_ENV=development), so a broken component produced a partial page
1041
+ * with a 200 and error boundaries never saw nested failures.
961
1042
  */
962
1043
  executeFunctionComponent(func, depth = 0) {
963
1044
  try {
964
- const isContextProvider = func.length > 0 || func.isContextProvider;
965
- let result;
966
- if (isContextProvider) {
967
- result = func((children) => {
968
- return this.renderComponent(children, this.config, depth + 1);
969
- });
970
- } else {
971
- result = func();
972
- }
1045
+ const result = func();
973
1046
  if (typeof result === "function") {
974
1047
  return this.executeFunctionComponent(result, depth);
975
1048
  }
976
1049
  return result;
977
- } catch (_error) {
1050
+ } catch (error) {
978
1051
  if (this.config.enableMonitoring) {
979
- performanceMonitor.recordError("functionComponent", _error);
980
- }
981
- if (typeof process !== "undefined" && process.env && true) {
982
- console.warn("Coherent.js Function Component Error:", _error.message);
1052
+ performanceMonitor.recordError("functionComponent", error);
983
1053
  }
984
- return null;
1054
+ throw error;
985
1055
  }
986
1056
  }
987
1057
  /**
@@ -1082,88 +1152,43 @@ var BaseRenderer = class {
1082
1152
  throw new Error("render must be implemented by subclass");
1083
1153
  }
1084
1154
  };
1085
- var RendererUtils = {
1086
- /**
1087
- * Check if element is static (no functions or circular references)
1088
- */
1089
- isStaticElement(element, visited = /* @__PURE__ */ new WeakSet()) {
1090
- if (!element || typeof element !== "object") {
1091
- return typeof element === "string" || typeof element === "number";
1092
- }
1093
- if (visited.has(element)) {
1094
- return false;
1095
- }
1096
- visited.add(element);
1097
- for (const [_key, value] of Object.entries(element)) {
1098
- if (typeof value === "function") return false;
1099
- if (Array.isArray(value)) {
1100
- const allStatic = value.every((child) => RendererUtils.isStaticElement(child, visited));
1101
- if (!allStatic) return false;
1102
- } else if (typeof value === "object" && value !== null) {
1103
- if (!RendererUtils.isStaticElement(value, visited)) return false;
1104
- }
1105
- }
1106
- return true;
1107
- },
1108
- /**
1109
- * Check if object has functions (for caching decisions)
1110
- */
1111
- hasFunctions(obj, visited = /* @__PURE__ */ new WeakSet()) {
1112
- if (visited.has(obj)) return false;
1113
- visited.add(obj);
1114
- for (const value of Object.values(obj)) {
1115
- if (typeof value === "function") return true;
1116
- if (typeof value === "object" && value !== null && RendererUtils.hasFunctions(value, visited)) {
1117
- return true;
1118
- }
1119
- }
1120
- return false;
1121
- },
1122
- /**
1123
- * Get element complexity score
1124
- */
1125
- getElementComplexity(element) {
1126
- if (!element || typeof element !== "object") return 1;
1127
- let complexity = Object.keys(element).length;
1128
- if (element.children && Array.isArray(element.children)) {
1129
- complexity += element.children.reduce(
1130
- (sum, child) => sum + RendererUtils.getElementComplexity(child),
1131
- 0
1132
- );
1133
- }
1134
- return complexity;
1135
- },
1136
- /**
1137
- * Generate cache key for element
1138
- */
1139
- generateCacheKey(tagName, element) {
1140
- try {
1141
- const keyData = {
1142
- tag: tagName,
1143
- props: extractProps(element),
1144
- hasChildren: hasChildren(element),
1145
- childrenType: Array.isArray(element.children) ? "array" : typeof element.children
1146
- };
1147
- return `element:${JSON.stringify(keyData)}`;
1148
- } catch (_error) {
1149
- if (typeof process !== "undefined" && process.env && true) {
1150
- console.warn("Failed to generate cache key:", _error);
1151
- }
1152
- return null;
1155
+ var UNCACHEABLE = /* @__PURE__ */ Symbol("uncacheable");
1156
+ function serializeForCache(value) {
1157
+ try {
1158
+ return serialize(value, /* @__PURE__ */ new Set());
1159
+ } catch (error) {
1160
+ if (error === UNCACHEABLE) return null;
1161
+ throw error;
1162
+ }
1163
+ }
1164
+ function serialize(value, ancestors) {
1165
+ switch (typeof value) {
1166
+ case "string":
1167
+ return JSON.stringify(value);
1168
+ case "number":
1169
+ return `n${value}`;
1170
+ case "boolean":
1171
+ return value ? "t" : "f";
1172
+ case "undefined":
1173
+ return "u";
1174
+ case "object": {
1175
+ if (value === null) return "z";
1176
+ if (isTrustedContent(value)) return `T${JSON.stringify(value.__html)}`;
1177
+ if (ancestors.has(value)) throw UNCACHEABLE;
1178
+ const isArray = Array.isArray(value);
1179
+ if (!isArray) {
1180
+ const proto = Object.getPrototypeOf(value);
1181
+ if (proto !== Object.prototype && proto !== null) throw UNCACHEABLE;
1182
+ }
1183
+ ancestors.add(value);
1184
+ const body = isArray ? value.map((item) => serialize(item, ancestors)).join(",") : Object.keys(value).map((key) => `${JSON.stringify(key)}:${serialize(value[key], ancestors)}`).join(",");
1185
+ ancestors.delete(value);
1186
+ return isArray ? `[${body}]` : `{${body}}`;
1153
1187
  }
1154
- },
1155
- /**
1156
- * Check if element is cacheable
1157
- */
1158
- isCacheable(element, options) {
1159
- if (!options.enableCache) return false;
1160
- if (RendererUtils.hasFunctions(element)) return false;
1161
- if (RendererUtils.getElementComplexity(element) > 1e3) return false;
1162
- const cacheKey = RendererUtils.generateCacheKey(element.tagName || "unknown", element);
1163
- if (!cacheKey) return false;
1164
- return true;
1188
+ default:
1189
+ throw UNCACHEABLE;
1165
1190
  }
1166
- };
1191
+ }
1167
1192
 
1168
1193
  // src/core/html-nesting-rules.js
1169
1194
  var FORBIDDEN_CHILDREN = {
@@ -1249,12 +1274,13 @@ var HTMLNestingError = class extends Error {
1249
1274
  // src/performance/cache-manager.js
1250
1275
  function createCacheManager(options = {}) {
1251
1276
  const {
1252
- maxCacheSize = 1e3,
1253
1277
  maxMemoryMB = 100,
1254
1278
  ttlMs = 1e3 * 60 * 5,
1255
1279
  // 5 minutes
1256
1280
  enableStatistics = true
1257
1281
  } = options;
1282
+ const maxCacheSize = options.maxCacheSize ?? options.maxSize ?? 1e3;
1283
+ const maxMemoryBytes = maxMemoryMB * 1024 * 1024;
1258
1284
  const caches = {
1259
1285
  static: /* @__PURE__ */ new Map(),
1260
1286
  // Never-changing components
@@ -1297,51 +1323,57 @@ function createCacheManager(options = {}) {
1297
1323
  return `${extractComponentName(component)}_${hash}`;
1298
1324
  }
1299
1325
  function get(key, type = "component") {
1300
- const cache = caches[type] || caches.component;
1326
+ const cacheType = caches[type] ? type : "component";
1327
+ const cache = caches[cacheType];
1301
1328
  const entry = cache.get(key);
1302
1329
  if (!entry) {
1303
1330
  stats.misses++;
1304
- if (enableStatistics) stats.accessCount[type]++;
1331
+ if (enableStatistics) stats.accessCount[cacheType]++;
1305
1332
  return null;
1306
1333
  }
1307
- if (Date.now() - entry.timestamp > ttlMs) {
1334
+ if (Date.now() - entry.timestamp > entry.ttl) {
1308
1335
  cache.delete(key);
1309
1336
  updateMemoryUsage(-entry.size);
1310
1337
  stats.misses++;
1311
- if (enableStatistics) stats.accessCount[type]++;
1338
+ if (enableStatistics) stats.accessCount[cacheType]++;
1312
1339
  return null;
1313
1340
  }
1341
+ cache.delete(key);
1342
+ cache.set(key, entry);
1314
1343
  entry.lastAccess = Date.now();
1315
1344
  entry.accessCount++;
1316
1345
  stats.hits++;
1317
1346
  if (enableStatistics) {
1318
- stats.accessCount[type]++;
1319
- stats.hitRate[type] = stats.hits / (stats.hits + stats.misses) * 100;
1347
+ stats.accessCount[cacheType]++;
1348
+ stats.hitRate[cacheType] = stats.hits / (stats.hits + stats.misses) * 100;
1320
1349
  }
1321
1350
  return entry.value;
1322
1351
  }
1323
1352
  function set(key, value, type = "component", metadata = {}) {
1324
- const cache = caches[type] || caches.component;
1325
- const size = calculateSize(value);
1326
- if (memoryUsage + size > maxMemoryMB * 1024 * 1024) {
1327
- optimize(type, size);
1353
+ const cacheType = caches[type] ? type : "component";
1354
+ const cache = caches[cacheType];
1355
+ const size = calculateSize(value) + calculateSize(key);
1356
+ if (size > maxMemoryBytes) return;
1357
+ const existing = cache.get(key);
1358
+ if (existing) {
1359
+ cache.delete(key);
1360
+ updateMemoryUsage(-existing.size);
1328
1361
  }
1329
- const entry = {
1362
+ const now = Date.now();
1363
+ cache.set(key, {
1330
1364
  value,
1331
- timestamp: Date.now(),
1332
- lastAccess: Date.now(),
1365
+ timestamp: now,
1366
+ lastAccess: now,
1333
1367
  size,
1334
1368
  metadata,
1369
+ ttl: typeof metadata.ttlMs === "number" ? metadata.ttlMs : ttlMs,
1335
1370
  accessCount: 0
1336
- };
1337
- const existing = cache.get(key);
1338
- if (existing) {
1339
- updateMemoryUsage(-existing.size);
1340
- }
1341
- cache.set(key, entry);
1371
+ });
1342
1372
  updateMemoryUsage(size);
1343
- if (cache.size > maxCacheSize) {
1344
- optimize(type);
1373
+ evict(cache, () => cache.size > maxCacheSize);
1374
+ for (const other of Object.values(caches)) {
1375
+ if (memoryUsage <= maxMemoryBytes) break;
1376
+ evict(other, () => memoryUsage > maxMemoryBytes);
1345
1377
  }
1346
1378
  }
1347
1379
  function remove(key, type) {
@@ -1368,11 +1400,14 @@ function createCacheManager(options = {}) {
1368
1400
  if (type) {
1369
1401
  const cache = caches[type];
1370
1402
  if (cache) {
1403
+ for (const entry of cache.values()) {
1404
+ updateMemoryUsage(-entry.size);
1405
+ }
1371
1406
  cache.clear();
1372
1407
  }
1373
- } else {
1374
- Object.values(caches).forEach((cache) => cache.clear());
1408
+ return;
1375
1409
  }
1410
+ Object.values(caches).forEach((cache) => cache.clear());
1376
1411
  memoryUsage = 0;
1377
1412
  }
1378
1413
  function getStats() {
@@ -1391,7 +1426,7 @@ function createCacheManager(options = {}) {
1391
1426
  let freed = 0;
1392
1427
  for (const [, cache] of Object.entries(caches)) {
1393
1428
  for (const [key, entry] of cache.entries()) {
1394
- if (now - entry.timestamp > ttlMs) {
1429
+ if (now - entry.timestamp > entry.ttl) {
1395
1430
  cache.delete(key);
1396
1431
  updateMemoryUsage(-entry.size);
1397
1432
  freed++;
@@ -1416,17 +1451,15 @@ function createCacheManager(options = {}) {
1416
1451
  function updateMemoryUsage(delta) {
1417
1452
  memoryUsage = Math.max(0, memoryUsage + delta);
1418
1453
  }
1419
- function optimize(type, requiredSpace = 0) {
1420
- const cache = caches[type] || caches.component;
1421
- const entries = Array.from(cache.entries()).sort(([, a], [, b]) => a.lastAccess - b.lastAccess);
1454
+ function evict(cache, shouldEvict) {
1422
1455
  let freed = 0;
1423
- for (const [key, entry] of entries) {
1424
- if (freed >= requiredSpace) break;
1456
+ for (const [key, entry] of cache) {
1457
+ if (!shouldEvict()) break;
1425
1458
  cache.delete(key);
1426
1459
  updateMemoryUsage(-entry.size);
1427
1460
  freed += entry.size;
1428
1461
  }
1429
- return { freed };
1462
+ return freed;
1430
1463
  }
1431
1464
  function simpleHash(str) {
1432
1465
  let hash = 0;
@@ -1581,7 +1614,7 @@ var ErrorHandler = class {
1581
1614
  enableStackTrace: options.enableStackTrace !== false,
1582
1615
  enableSuggestions: options.enableSuggestions !== false,
1583
1616
  enableLogging: options.enableLogging ?? defaultEnableLogging,
1584
- logLevel: options.logLevel || "_error",
1617
+ logLevel: options.logLevel || "error",
1585
1618
  maxErrorHistory: options.maxErrorHistory || 100,
1586
1619
  ...options
1587
1620
  };
@@ -1732,7 +1765,7 @@ var ErrorHandler = class {
1732
1765
  if (suggestions.length === 0) {
1733
1766
  suggestions.push(
1734
1767
  "Enable development tools for more detailed debugging",
1735
- "Check browser console for additional _error details",
1768
+ "Check browser console for additional error details",
1736
1769
  "Use component validation tools to identify issues"
1737
1770
  );
1738
1771
  }
@@ -1874,15 +1907,34 @@ var ErrorHandler = class {
1874
1907
  var globalErrorHandler = new ErrorHandler();
1875
1908
 
1876
1909
  // src/rendering/html-renderer.js
1910
+ var RESERVED_PROPS = /* @__PURE__ */ new Set(["children", "text", "key", "html"]);
1877
1911
  var rendererCache = createCacheManager({
1878
- maxSize: 1e3,
1912
+ maxCacheSize: 1e3,
1879
1913
  ttlMs: 3e5
1880
1914
  // 5 minutes
1881
1915
  });
1916
+ function assertNotThenable(value, path) {
1917
+ if (value && (typeof value === "object" || typeof value === "function") && typeof value.then === "function") {
1918
+ throw new RenderingError(
1919
+ `Cannot render a Promise at ${path === "root" ? "root" : formatRenderPath(path)}: render() is synchronous. Await async components (and their data) before rendering.`,
1920
+ void 0,
1921
+ { path: path === "root" ? "root" : formatRenderPath(path), renderer: "html" }
1922
+ );
1923
+ }
1924
+ }
1925
+ function childPath(parent, segment) {
1926
+ return { parent, segment };
1927
+ }
1882
1928
  function formatRenderPath(path) {
1883
- if (!path || path.length === 0) return "root";
1929
+ const segments = [];
1930
+ if (Array.isArray(path)) {
1931
+ segments.push(...path);
1932
+ } else {
1933
+ for (let node = path; node; node = node.parent) segments.push(node.segment);
1934
+ segments.reverse();
1935
+ }
1884
1936
  let rendered = "root";
1885
- for (const segment of path) {
1937
+ for (const segment of segments) {
1886
1938
  if (typeof segment !== "string" || segment.length === 0) continue;
1887
1939
  if (segment.startsWith("[")) {
1888
1940
  rendered += segment;
@@ -1895,15 +1947,15 @@ function formatRenderPath(path) {
1895
1947
  var HTMLRenderer = class extends BaseRenderer {
1896
1948
  constructor(options = {}) {
1897
1949
  super({
1898
- enableCache: options.enableCache !== false,
1899
1950
  enableMonitoring: options.enableMonitoring !== false,
1900
1951
  minify: options.minify || false,
1901
1952
  streaming: options.streaming || false,
1902
1953
  maxDepth: options.maxDepth || 100,
1903
- ...options
1954
+ ...options,
1955
+ enableCache: options.enableCache === true
1904
1956
  });
1905
- if (this.config.enableCache && !this.cache) {
1906
- this.cache = rendererCache;
1957
+ if (this.config.enableCache) {
1958
+ this.cache = options.cache || rendererCache;
1907
1959
  }
1908
1960
  }
1909
1961
  /**
@@ -1931,15 +1983,30 @@ var HTMLRenderer = class extends BaseRenderer {
1931
1983
  const config = { ...this.config, ...options };
1932
1984
  this.startTiming();
1933
1985
  try {
1986
+ assertNotThenable(component, "root");
1934
1987
  if (config.validateInput && !this.isValidComponent(component)) {
1935
1988
  throw new Error("Invalid component structure");
1936
1989
  }
1990
+ const cacheKey = this.cache && config.enableCache ? serializeForCache(component) : null;
1991
+ const fullKey = cacheKey === null ? null : `render:${config.minify ? "min" : "raw"}:${cacheKey}`;
1992
+ if (fullKey !== null) {
1993
+ const cached = this.cache.get(fullKey);
1994
+ if (cached !== null) {
1995
+ this.endTiming();
1996
+ return cached;
1997
+ }
1998
+ }
1937
1999
  const renderOptions = {
1938
2000
  ...config,
1939
2001
  seenObjects: /* @__PURE__ */ new WeakSet()
1940
2002
  };
1941
- const html = this.renderComponent(component, renderOptions, 0, []);
2003
+ const html = this.renderComponent(component, renderOptions, 0, null);
1942
2004
  const finalHtml = config.minify ? minifyHtml(html, config) : html;
2005
+ if (fullKey !== null) {
2006
+ this.cache.set(fullKey, finalHtml, "component", {
2007
+ ttlMs: typeof config.cacheTTL === "number" ? config.cacheTTL : void 0
2008
+ });
2009
+ }
1943
2010
  this.endTiming();
1944
2011
  this.recordPerformance("render", this.metrics.startTime, false, {
1945
2012
  cacheEnabled: config.enableCache
@@ -1959,7 +2026,7 @@ var HTMLRenderer = class extends BaseRenderer {
1959
2026
  /**
1960
2027
  * Render a single component with full optimization pipeline
1961
2028
  */
1962
- renderComponent(component, options, depth = 0, path = []) {
2029
+ renderComponent(component, options, depth = 0, path = null) {
1963
2030
  if (component === null || component === void 0) {
1964
2031
  return "";
1965
2032
  }
@@ -1969,8 +2036,13 @@ var HTMLRenderer = class extends BaseRenderer {
1969
2036
  if (isTrustedContent(component)) {
1970
2037
  return component.__html;
1971
2038
  }
1972
- if (typeof component === "object" && component !== null && !Array.isArray(component)) {
1973
- if (options.seenObjects && options.seenObjects.has(component)) {
2039
+ if (typeof component === "object" && component.__isLazy === true && typeof component.evaluate === "function") {
2040
+ return this.renderComponent(component.evaluate(), options, depth + 1, childPath(path, "()"));
2041
+ }
2042
+ assertNotThenable(component, path);
2043
+ const tracked = options.seenObjects && typeof component === "object" && component !== null ? component : null;
2044
+ if (tracked) {
2045
+ if (options.seenObjects.has(tracked)) {
1974
2046
  throw new RenderingError(
1975
2047
  "Circular reference detected in component tree",
1976
2048
  component,
@@ -1978,9 +2050,7 @@ var HTMLRenderer = class extends BaseRenderer {
1978
2050
  ["Remove the circular reference", "Use lazy loading to break the cycle"]
1979
2051
  );
1980
2052
  }
1981
- if (options.seenObjects) {
1982
- options.seenObjects.add(component);
1983
- }
2053
+ options.seenObjects.add(tracked);
1984
2054
  }
1985
2055
  this.validateDepth(depth);
1986
2056
  try {
@@ -1991,8 +2061,8 @@ var HTMLRenderer = class extends BaseRenderer {
1991
2061
  case "text":
1992
2062
  return escapeHtml(value);
1993
2063
  case "function": {
1994
- const result = this.executeFunctionComponent(value, depth);
1995
- return this.renderComponent(result, options, depth + 1, [...path, "()"]);
2064
+ const result = this.runFunctionComponent(value, options, depth, path);
2065
+ return this.renderComponent(result, options, depth + 1, childPath(path, "()"));
1996
2066
  }
1997
2067
  case "array":
1998
2068
  if (typeof process !== "undefined" && process.env && true && value.length > 1) {
@@ -2010,11 +2080,23 @@ var HTMLRenderer = class extends BaseRenderer {
2010
2080
  );
2011
2081
  }
2012
2082
  }
2013
- return value.map((child, index) => this.renderComponent(child, options, depth + 1, [...path, `[${index}]`])).join("");
2083
+ {
2084
+ let html = "";
2085
+ for (let index = 0; index < value.length; index++) {
2086
+ html += this.renderComponent(value[index], options, depth + 1, childPath(path, `[${index}]`));
2087
+ }
2088
+ return html;
2089
+ }
2014
2090
  case "element": {
2015
- const tagName = Object.keys(value)[0];
2016
- const elementContent = value[tagName];
2017
- return this.renderElement(tagName, elementContent, options, depth, [...path, tagName]);
2091
+ const tagNames = Object.keys(value);
2092
+ if (tagNames.length === 1) {
2093
+ return this.renderElement(tagNames[0], value[tagNames[0]], options, depth, childPath(path, tagNames[0]));
2094
+ }
2095
+ let html = "";
2096
+ for (const tagName of tagNames) {
2097
+ html += this.renderElement(tagName, value[tagName], options, depth, childPath(path, tagName));
2098
+ }
2099
+ return html;
2018
2100
  }
2019
2101
  default:
2020
2102
  this.recordError("renderComponent", new Error(`Unknown component type: ${type}`));
@@ -2030,16 +2112,34 @@ var HTMLRenderer = class extends BaseRenderer {
2030
2112
  }
2031
2113
  throw _error;
2032
2114
  }
2033
- throw new RenderingError(_error.message, void 0, { path: renderPath, renderer: "html" });
2115
+ const wrapped = new RenderingError(_error.message, void 0, { path: renderPath, renderer: "html" });
2116
+ wrapped.cause = _error;
2117
+ throw wrapped;
2118
+ } finally {
2119
+ if (tracked) options.seenObjects.delete(tracked);
2120
+ }
2121
+ }
2122
+ /**
2123
+ * Run a function component, giving `options.onError` the chance to
2124
+ * replace a component that throws.
2125
+ */
2126
+ runFunctionComponent(func, options, depth, path) {
2127
+ try {
2128
+ return this.executeFunctionComponent(func, depth);
2129
+ } catch (error) {
2130
+ if (typeof options.onError === "function") {
2131
+ return options.onError(error, { path: formatRenderPath(path) });
2132
+ }
2133
+ throw error;
2034
2134
  }
2035
2135
  }
2036
2136
  /**
2037
2137
  * Render an HTML element with advanced caching and optimization
2038
2138
  */
2039
- renderElement(tagName, element, options, depth = 0, path = []) {
2040
- const startTime = performance.now();
2041
- if (element && typeof element === "object" && !Array.isArray(element)) {
2042
- if (options.seenObjects && options.seenObjects.has(element)) {
2139
+ renderElement(tagName, element, options, depth = 0, path = null) {
2140
+ const tracked = options.seenObjects && element && typeof element === "object" && !Array.isArray(element) ? element : null;
2141
+ if (tracked) {
2142
+ if (options.seenObjects.has(tracked)) {
2043
2143
  throw new RenderingError(
2044
2144
  "Circular reference detected in component tree",
2045
2145
  element,
@@ -2047,32 +2147,31 @@ var HTMLRenderer = class extends BaseRenderer {
2047
2147
  ["Remove the circular reference", "Use lazy loading to break the cycle"]
2048
2148
  );
2049
2149
  }
2050
- if (options.seenObjects) {
2051
- options.seenObjects.add(element);
2052
- }
2053
- }
2054
- if (options.enableMonitoring && this.cache) {
2150
+ options.seenObjects.add(tracked);
2055
2151
  }
2056
- if (options.enableCache && this.cache && RendererUtils.isStaticElement(element)) {
2057
- try {
2058
- const cacheKey = `static:${tagName}:${JSON.stringify(element)}`;
2059
- const cached = this.cache.get("static", cacheKey);
2060
- if (cached) {
2061
- this.recordPerformance(tagName, startTime, true);
2062
- return cached.value;
2063
- }
2064
- } catch {
2065
- }
2152
+ try {
2153
+ return this.renderElementContent(tagName, element, options, depth, path);
2154
+ } finally {
2155
+ if (tracked) options.seenObjects.delete(tracked);
2066
2156
  }
2157
+ }
2158
+ renderElementContent(tagName, element, options, depth = 0, path = null) {
2159
+ const startTime = options.enableMonitoring ? performance.now() : 0;
2067
2160
  if (typeof element === "string" || typeof element === "number" || typeof element === "boolean") {
2068
2161
  const html2 = isVoidElement(tagName) ? `<${tagName}>` : `<${tagName}>${escapeHtml(String(element))}</${tagName}>`;
2069
- this.cacheIfStatic(tagName, element, html2, options);
2070
2162
  this.recordPerformance(tagName, startTime, false);
2071
2163
  return html2;
2072
2164
  }
2073
2165
  if (typeof element === "function") {
2074
- const result = this.executeFunctionComponent(element, depth);
2075
- return this.renderElement(tagName, result, options, depth, [...path, "()"]);
2166
+ let result;
2167
+ try {
2168
+ result = this.executeFunctionComponent(element, depth);
2169
+ } catch (error) {
2170
+ if (typeof options.onError !== "function") throw error;
2171
+ const replacement = options.onError(error, { path: formatRenderPath(path) });
2172
+ return this.renderComponent(replacement, options, depth + 1, childPath(path, "()"));
2173
+ }
2174
+ return this.renderElement(tagName, result, options, depth, childPath(path, "()"));
2076
2175
  }
2077
2176
  if (element && typeof element === "object") {
2078
2177
  return this.renderObjectElement(tagName, element, options, depth, path);
@@ -2087,105 +2186,255 @@ var HTMLRenderer = class extends BaseRenderer {
2087
2186
  return html;
2088
2187
  }
2089
2188
  /**
2090
- * Cache element if it's static
2189
+ * Render complex object elements with attributes and children
2091
2190
  */
2092
- cacheIfStatic(tagName, element, html) {
2093
- if (this.config.enableCache && this.cache && RendererUtils.isStaticElement(element)) {
2094
- try {
2095
- const cacheKey = `static:${tagName}:${JSON.stringify(element)}`;
2096
- this.cache.set("static", cacheKey, html, {
2097
- ttlMs: this.config.cacheTTL || 5 * 60 * 1e3,
2098
- // 5 minutes default
2099
- size: html.length
2100
- // Approximate size
2101
- });
2102
- } catch {
2191
+ renderObjectElement(tagName, element, options, depth = 0, path = null) {
2192
+ const startTime = options.enableMonitoring ? performance.now() : 0;
2193
+ const parts = elementParts(tagName, element);
2194
+ let html = parts.open + parts.content;
2195
+ if (parts.children) {
2196
+ const forbidden = FORBIDDEN_CHILDREN[tagName.toLowerCase()];
2197
+ for (let index = 0; index < parts.children.length; index++) {
2198
+ const child = parts.children[index];
2199
+ const segment = childPath(path, `children[${index}]`);
2200
+ checkNesting(tagName, forbidden, child, segment);
2201
+ html += this.renderComponent(child, options, depth + 1, segment);
2103
2202
  }
2104
2203
  }
2204
+ html += parts.close;
2205
+ this.recordPerformance(tagName, startTime, false);
2206
+ return html;
2105
2207
  }
2106
2208
  /**
2107
- * Render complex object elements with attributes and children
2209
+ * Streaming counterpart of renderComponent: yields HTML pieces. Elements
2210
+ * with many children are streamed child by child; everything else goes
2211
+ * through the synchronous renderer, which is exact and faster, so the
2212
+ * streamed output is the same as render()'s by construction.
2108
2213
  */
2109
- renderObjectElement(tagName, element, options, depth = 0, path = []) {
2110
- const startTime = performance.now();
2111
- if (options.enableCache && this.cache) {
2112
- const cacheKey = RendererUtils.generateCacheKey(tagName, element);
2113
- if (cacheKey) {
2114
- const cached = this.cache.get(cacheKey);
2115
- if (cached) {
2116
- this.recordPerformance(tagName, startTime, true);
2117
- return cached;
2214
+ async *streamComponent(component, options, depth = 0, path = null) {
2215
+ if (component === null || component === void 0) return;
2216
+ this.validateDepth(depth);
2217
+ if (typeof component === "function") {
2218
+ const result = this.runFunctionComponent(component, options, depth, path);
2219
+ yield* this.streamComponent(result, options, depth + 1, childPath(path, "()"));
2220
+ return;
2221
+ }
2222
+ if (Array.isArray(component)) {
2223
+ yield* this.streamTracked(component, options, path, async function* (renderer) {
2224
+ for (let index = 0; index < component.length; index++) {
2225
+ yield* renderer.streamComponent(component[index], options, depth + 1, childPath(path, `[${index}]`));
2118
2226
  }
2227
+ });
2228
+ return;
2229
+ }
2230
+ if (typeof component === "object" && !isTrustedContent(component) && component.__isLazy !== true) {
2231
+ const { type, value } = this.processComponentType(component);
2232
+ if (type === "element") {
2233
+ yield* this.streamTracked(component, options, path, async function* (renderer) {
2234
+ for (const tagName of Object.keys(value)) {
2235
+ yield* renderer.streamElement(tagName, value[tagName], options, depth, childPath(path, tagName));
2236
+ }
2237
+ });
2238
+ return;
2119
2239
  }
2120
2240
  }
2121
- const { children, text, key: _key, html: _rawHtml, ...attributes } = element || {};
2122
- const attributeString = formatAttributes(attributes);
2123
- const openingTag = attributeString ? `<${tagName} ${attributeString}>` : `<${tagName}>`;
2124
- if (isVoidElement(tagName)) {
2125
- if (options.enableCache && this.cache && RendererUtils.isCacheable(element, options)) {
2126
- const cacheKey = RendererUtils.generateCacheKey(tagName, element);
2127
- if (cacheKey) {
2128
- this.cache.set(cacheKey, openingTag);
2241
+ yield this.renderComponent(component, options, depth, path);
2242
+ }
2243
+ async *streamElement(tagName, element, options, depth, path) {
2244
+ if (!element || typeof element !== "object" || Array.isArray(element) || !shouldStream(element.children)) {
2245
+ yield this.renderElement(tagName, element, options, depth, path);
2246
+ return;
2247
+ }
2248
+ yield* this.streamTracked(element, options, path, async function* (renderer) {
2249
+ const parts = elementParts(tagName, element);
2250
+ yield parts.open + parts.content;
2251
+ if (parts.children) {
2252
+ const forbidden = FORBIDDEN_CHILDREN[tagName.toLowerCase()];
2253
+ for (let index = 0; index < parts.children.length; index++) {
2254
+ const child = parts.children[index];
2255
+ const segment = childPath(path, `children[${index}]`);
2256
+ checkNesting(tagName, forbidden, child, segment);
2257
+ yield* renderer.streamComponent(child, options, depth + 1, segment);
2129
2258
  }
2130
2259
  }
2131
- this.recordPerformance(tagName, startTime, false);
2132
- return openingTag;
2260
+ yield parts.close;
2261
+ });
2262
+ }
2263
+ /**
2264
+ * Run `body` with `value` on the ancestor path used for cycle detection.
2265
+ */
2266
+ async *streamTracked(value, options, path, body) {
2267
+ if (options.seenObjects.has(value)) {
2268
+ throw new RenderingError(
2269
+ "Circular reference detected in component tree",
2270
+ value,
2271
+ { path: formatRenderPath(path) },
2272
+ ["Remove the circular reference", "Use lazy loading to break the cycle"]
2273
+ );
2133
2274
  }
2134
- if (_rawHtml !== void 0) {
2135
- const resolvedHtml = typeof _rawHtml === "function" ? _rawHtml() : _rawHtml;
2136
- const rawContent = isTrustedContent(resolvedHtml) ? resolvedHtml.__html : String(resolvedHtml);
2137
- const result = `${openingTag}${rawContent}</${tagName}>`;
2138
- return result;
2275
+ options.seenObjects.add(value);
2276
+ try {
2277
+ yield* body(this);
2278
+ } finally {
2279
+ options.seenObjects.delete(value);
2139
2280
  }
2140
- if (isTrustedContent(text)) {
2141
- return `${openingTag}${text.__html}</${tagName}>`;
2142
- }
2143
- let textContent = "";
2144
- if (text !== void 0) {
2145
- const isScript = tagName === "script";
2146
- const isStyle = tagName === "style";
2147
- const isRawTag = isScript || isStyle;
2148
- const raw = typeof text === "function" ? String(text()) : String(text);
2149
- if (isRawTag) {
2150
- const safe = raw.replace(/<\/(script)/gi, "<\\/$1").replace(/<\/(style)/gi, "<\\/$1").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
2151
- textContent = safe;
2152
- } else {
2153
- textContent = escapeHtml(raw);
2281
+ }
2282
+ };
2283
+ var STREAM_MIN_CHILDREN = 8;
2284
+ function shouldStream(children) {
2285
+ if (children === void 0 || children === null) return false;
2286
+ const list = Array.isArray(children) ? children : [children];
2287
+ if (list.length >= STREAM_MIN_CHILDREN) return true;
2288
+ for (const child of list) {
2289
+ if (typeof child === "function" || Array.isArray(child)) return true;
2290
+ if (child && typeof child === "object") {
2291
+ for (const key in child) {
2292
+ const content = child[key];
2293
+ if (typeof content === "function") return true;
2294
+ if (content && typeof content === "object" && content.children !== void 0 && content.children !== null) return true;
2154
2295
  }
2155
2296
  }
2156
- let childrenHtml = "";
2157
- if (hasChildren(element)) {
2158
- const normalizedChildren = normalizeChildren(children);
2159
- childrenHtml = normalizedChildren.map((child, index) => {
2160
- if (child && typeof child === "object" && !Array.isArray(child)) {
2161
- const childTagName = Object.keys(child)[0];
2162
- if (childTagName) {
2163
- validateNesting(tagName, childTagName, formatRenderPath([...path, `children[${index}]`]));
2164
- }
2165
- }
2166
- return this.renderComponent(child, options, depth + 1, [...path, `children[${index}]`]);
2167
- }).join("");
2297
+ }
2298
+ return false;
2299
+ }
2300
+ function elementParts(tagName, element) {
2301
+ const { children, text: rawText, html: rawHtml } = element || {};
2302
+ const attributeString = formatAttributes(element, RESERVED_PROPS);
2303
+ const open = attributeString ? `<${tagName} ${attributeString}>` : `<${tagName}>`;
2304
+ if (isVoidElement(tagName)) {
2305
+ return { open, content: "", children: null, close: "" };
2306
+ }
2307
+ const close = `</${tagName}>`;
2308
+ const html = typeof rawHtml === "function" ? rawHtml() : rawHtml;
2309
+ if (html !== void 0 && html !== null) {
2310
+ return { open, content: isTrustedContent(html) ? html.__html : String(html), children: null, close };
2311
+ }
2312
+ const text = typeof rawText === "function" && !isTrustedContent(rawText) ? rawText() : rawText;
2313
+ if (isTrustedContent(text)) {
2314
+ return { open, content: text.__html, children: null, close };
2315
+ }
2316
+ let content = "";
2317
+ if (text !== void 0 && text !== null) {
2318
+ const raw = String(text);
2319
+ if (tagName === "script" || tagName === "style") {
2320
+ content = raw.replace(/<\/(script)/gi, "<\\/$1").replace(/<\/(style)/gi, "<\\/$1").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
2321
+ } else {
2322
+ content = escapeHtml(raw);
2168
2323
  }
2169
- const html = `${openingTag}${textContent}${childrenHtml}</${tagName}>`;
2170
- if (options.enableCache && this.cache && RendererUtils.isCacheable(element, options)) {
2171
- const cacheKey = RendererUtils.generateCacheKey(tagName, element);
2172
- if (cacheKey) {
2173
- this.cache.set(cacheKey, html);
2174
- }
2324
+ }
2325
+ const normalized = children !== void 0 && children !== null ? normalizeChildren(children) : null;
2326
+ return { open, content, children: normalized && normalized.length > 0 ? normalized : null, close };
2327
+ }
2328
+ function checkNesting(tagName, forbidden, child, path) {
2329
+ if (forbidden && child && typeof child === "object" && !Array.isArray(child)) {
2330
+ const childTagName = Object.keys(child)[0];
2331
+ if (childTagName && forbidden.has(childTagName.toLowerCase())) {
2332
+ validateNesting(tagName, childTagName, formatRenderPath(path));
2175
2333
  }
2176
- this.recordPerformance(tagName, startTime, false);
2177
- return html;
2178
2334
  }
2179
- };
2335
+ }
2180
2336
  function render(component, options = {}) {
2181
2337
  const mergedOptions = {
2182
- enableCache: true,
2183
2338
  enableMonitoring: false,
2184
2339
  ...options
2185
2340
  };
2186
2341
  const renderer = new HTMLRenderer(mergedOptions);
2187
2342
  return renderer.render(component, mergedOptions);
2188
2343
  }
2344
+ async function* renderToStream(component, options = {}) {
2345
+ const { chunkSize = 8192, ...renderOptions } = options;
2346
+ const renderer = new HTMLRenderer({ enableMonitoring: false, ...renderOptions, enableCache: false });
2347
+ const config = { ...renderer.config, seenObjects: /* @__PURE__ */ new WeakSet() };
2348
+ assertNotThenable(component, "root");
2349
+ if (config.validateInput && !renderer.isValidComponent(component)) {
2350
+ throw new Error("Invalid component structure");
2351
+ }
2352
+ const minifier = config.minify ? createStreamMinifier() : null;
2353
+ let buffer = "";
2354
+ for await (const piece of renderer.streamComponent(component, config, 0, null)) {
2355
+ buffer += minifier ? minifier.push(piece) : piece;
2356
+ if (buffer.length >= chunkSize) {
2357
+ yield buffer;
2358
+ buffer = "";
2359
+ await yieldToEventLoop();
2360
+ }
2361
+ }
2362
+ if (minifier) buffer += minifier.end();
2363
+ if (buffer) yield buffer;
2364
+ }
2365
+ var yieldToEventLoop = typeof setImmediate === "function" ? () => new Promise((resolve) => setImmediate(resolve)) : () => new Promise((resolve) => setTimeout(resolve, 0));
2366
+ function waitForDrain(response) {
2367
+ if (response.destroyed) return Promise.resolve(false);
2368
+ return new Promise((resolve) => {
2369
+ const settle = (drained) => () => {
2370
+ response.off("drain", onDrain);
2371
+ response.off("close", onClose);
2372
+ response.off("error", onClose);
2373
+ resolve(drained);
2374
+ };
2375
+ const onDrain = settle(true);
2376
+ const onClose = settle(false);
2377
+ response.on("drain", onDrain);
2378
+ response.on("close", onClose);
2379
+ response.on("error", onClose);
2380
+ });
2381
+ }
2382
+ var streamingUtils = {
2383
+ /**
2384
+ * Collect all chunks into a single string
2385
+ */
2386
+ async collectChunks(chunkGenerator) {
2387
+ let html = "";
2388
+ for await (const chunk of chunkGenerator) {
2389
+ html += chunk;
2390
+ }
2391
+ return html;
2392
+ },
2393
+ /**
2394
+ * Stream directly to a Node.js response, with backpressure.
2395
+ *
2396
+ * Resolves with the number of bytes written once the response has ended.
2397
+ * If the client disconnects first, rendering stops (the generator is
2398
+ * closed) and it resolves with the bytes written so far without ending
2399
+ * the response. If rendering fails, the response is destroyed and it
2400
+ * rejects with the rendering error.
2401
+ */
2402
+ async streamToResponse(chunkGenerator, response) {
2403
+ let totalBytes = 0;
2404
+ if (!response.headersSent && !response.getHeader?.("Content-Type")) {
2405
+ response.setHeader("Content-Type", "text/html; charset=utf-8");
2406
+ }
2407
+ try {
2408
+ for await (const chunk of chunkGenerator) {
2409
+ if (response.destroyed) return totalBytes;
2410
+ totalBytes += Buffer.byteLength(chunk);
2411
+ if (!response.write(chunk) && !await waitForDrain(response)) {
2412
+ return totalBytes;
2413
+ }
2414
+ }
2415
+ } catch (error) {
2416
+ response.destroy(error);
2417
+ throw error;
2418
+ }
2419
+ response.end();
2420
+ return totalBytes;
2421
+ },
2422
+ /**
2423
+ * Stream with progress callback
2424
+ */
2425
+ async *streamWithProgress(chunkGenerator, onProgress) {
2426
+ let totalBytes = 0;
2427
+ let chunkCount = 0;
2428
+ for await (const chunk of chunkGenerator) {
2429
+ totalBytes += Buffer.byteLength(chunk);
2430
+ chunkCount++;
2431
+ if (onProgress) {
2432
+ onProgress({ chunkCount, totalBytes, chunk });
2433
+ }
2434
+ yield chunk;
2435
+ }
2436
+ }
2437
+ };
2189
2438
 
2190
2439
  // src/components/component-system.js
2191
2440
  var COMPONENT_REGISTRY = /* @__PURE__ */ new Map();
@@ -2232,7 +2481,7 @@ var ComponentState = class {
2232
2481
  try {
2233
2482
  listener(newState, oldState);
2234
2483
  } catch (_error) {
2235
- console.error("State listener _error:", _error);
2484
+ console.error("State listener error:", _error);
2236
2485
  }
2237
2486
  });
2238
2487
  this.isUpdating = false;
@@ -2401,7 +2650,7 @@ var Component = class _Component {
2401
2650
  return this.rendered;
2402
2651
  } catch (_error) {
2403
2652
  this.handleError(_error);
2404
- return { div: { className: "component-_error", text: `Error in ${this.name}` } };
2653
+ return { div: { className: "component-error", text: `Error in ${this.name}` } };
2405
2654
  }
2406
2655
  }
2407
2656
  /**
@@ -2647,7 +2896,7 @@ function lazy(factory, options = {}) {
2647
2896
  if (onError) {
2648
2897
  onError(_error);
2649
2898
  } else {
2650
- console.error("Lazy evaluation _error:", _error);
2899
+ console.error("Lazy evaluation error:", _error);
2651
2900
  }
2652
2901
  return fallback;
2653
2902
  } finally {
@@ -2750,27 +2999,32 @@ function evaluateWithTimeout(factory, timeout, args, fallback) {
2750
2999
  }
2751
3000
  }).catch(() => fallback);
2752
3001
  }
3002
+ var functionIds = /* @__PURE__ */ new WeakMap();
3003
+ var nextFunctionId = 0;
3004
+ function functionId(fn) {
3005
+ let id = functionIds.get(fn);
3006
+ if (id === void 0) {
3007
+ id = ++nextFunctionId;
3008
+ functionIds.set(fn, id);
3009
+ }
3010
+ return id;
3011
+ }
3012
+ function serializeMemoKey(value) {
3013
+ return JSON.stringify(value, (_key, v) => typeof v === "function" ? `\0fn:${functionId(v)}` : v);
3014
+ }
2753
3015
  function memo(fn, options = {}) {
2754
3016
  const {
2755
3017
  // Caching strategy
2756
3018
  strategy = "lru",
2757
3019
  // 'lru', 'ttl', 'weak', 'simple'
2758
3020
  maxSize = 100,
2759
- // Maximum cache entries
3021
+ // Maximum cache entries ('lru' and 'ttl')
2760
3022
  ttl = null,
2761
3023
  // Time to live in milliseconds
2762
3024
  // Key generation
2763
3025
  keyFn = null,
2764
3026
  // Custom key function
2765
- keySerializer = JSON.stringify,
2766
- // Default serialization
2767
- // Comparison
2768
- // eslint-disable-next-line no-unused-vars
2769
- compareFn = null,
2770
- // Custom equality comparison
2771
- // eslint-disable-next-line no-unused-vars
2772
- shallow = false,
2773
- // Shallow comparison for objects
3027
+ keySerializer = serializeMemoKey,
2774
3028
  // Lifecycle hooks
2775
3029
  onHit = null,
2776
3030
  // Called on cache hit
@@ -2785,63 +3039,74 @@ function memo(fn, options = {}) {
2785
3039
  debug = false
2786
3040
  // Debug logging
2787
3041
  } = options;
2788
- let cache;
2789
- const stats_data = stats ? { hits: 0, misses: 0, evictions: 0 } : null;
2790
- switch (strategy) {
2791
- case "lru":
2792
- cache = new LRUCache(maxSize, { onEvict });
2793
- break;
2794
- case "ttl":
2795
- cache = new TTLCache(ttl, { onEvict });
2796
- break;
2797
- case "weak":
2798
- cache = /* @__PURE__ */ new WeakMap();
2799
- break;
2800
- default:
2801
- cache = /* @__PURE__ */ new Map();
2802
- }
3042
+ const statsData = { hits: 0, misses: 0, evictions: 0 };
3043
+ const expiresIn = strategy === "ttl" && ttl === null ? 5e3 : ttl;
3044
+ const store = strategy === "simple" ? /* @__PURE__ */ new Map() : new LRUCache(maxSize, {
3045
+ onEvict: (key, entry) => {
3046
+ statsData.evictions++;
3047
+ if (onEvict) onEvict(key, entry.value);
3048
+ }
3049
+ });
3050
+ const weakStore = strategy === "weak" ? /* @__PURE__ */ new WeakMap() : null;
2803
3051
  const generateKey = keyFn || ((...args) => {
2804
3052
  if (args.length === 0) return "__empty__";
2805
3053
  if (args.length === 1) return keySerializer(args[0]);
2806
3054
  return keySerializer(args);
2807
3055
  });
3056
+ const isFresh = (entry) => entry.expires === null || Date.now() < entry.expires;
3057
+ const lookup = (args) => {
3058
+ if (weakStore) {
3059
+ const [first] = args;
3060
+ const cacheable = first !== null && (typeof first === "object" || typeof first === "function");
3061
+ return { cacheable, key: first, entries: weakStore };
3062
+ }
3063
+ try {
3064
+ return { cacheable: true, key: generateKey(...args), entries: store };
3065
+ } catch {
3066
+ return { cacheable: false };
3067
+ }
3068
+ };
2808
3069
  const memoizedFn = (...args) => {
2809
- const key = generateKey(...args);
2810
- if (cache.has(key)) {
2811
- const cached = cache.get(key);
2812
- if (cached && (!cached.expires || Date.now() < cached.expires)) {
2813
- if (debug) console.log(`Memo cache hit for key: ${key}`);
3070
+ const { cacheable, key, entries } = lookup(args);
3071
+ if (!cacheable) return fn(...args);
3072
+ const cached = entries.get(key);
3073
+ if (cached !== void 0) {
3074
+ if (isFresh(cached)) {
3075
+ if (debug) console.log(`Memo cache hit for key: ${String(key)}`);
2814
3076
  if (onHit) onHit(key, cached.value, args);
2815
- if (stats_data) stats_data.hits++;
2816
- return cached.value || cached;
2817
- } else {
2818
- cache.delete(key);
3077
+ statsData.hits++;
3078
+ return cached.value;
2819
3079
  }
3080
+ entries.delete(key);
2820
3081
  }
2821
- if (debug) console.log(`Memo cache miss for key: ${key}`);
3082
+ if (debug) console.log(`Memo cache miss for key: ${String(key)}`);
2822
3083
  if (onMiss) onMiss(key, args);
2823
- if (stats_data) stats_data.misses++;
2824
- const result = fn(...args);
2825
- const cacheEntry = ttl ? { value: result, expires: Date.now() + ttl } : result;
2826
- cache.set(key, cacheEntry);
2827
- return result;
3084
+ statsData.misses++;
3085
+ const value = fn(...args);
3086
+ entries.set(key, { value, expires: expiresIn ? Date.now() + expiresIn : null });
3087
+ return value;
3088
+ };
3089
+ memoizedFn.cache = weakStore || (store instanceof LRUCache ? store.cache : store);
3090
+ memoizedFn.clear = () => {
3091
+ if (!weakStore) store.clear();
2828
3092
  };
2829
- memoizedFn.cache = cache;
2830
- memoizedFn.clear = () => cache.clear();
2831
- memoizedFn.delete = (key) => cache.delete(key);
2832
- memoizedFn.has = (key) => cache.has(key);
2833
- memoizedFn.size = () => cache.size;
2834
- if (stats_data) {
2835
- memoizedFn.stats = () => ({ ...stats_data });
3093
+ memoizedFn.delete = (key) => (weakStore || store).delete(key);
3094
+ memoizedFn.has = (key) => {
3095
+ const entry = (weakStore || store).get(key);
3096
+ return entry !== void 0 && isFresh(entry);
3097
+ };
3098
+ memoizedFn.size = () => weakStore ? void 0 : store.size;
3099
+ if (stats) {
3100
+ memoizedFn.stats = () => ({ ...statsData });
2836
3101
  memoizedFn.resetStats = () => {
2837
- stats_data.hits = 0;
2838
- stats_data.misses = 0;
2839
- stats_data.evictions = 0;
3102
+ statsData.hits = 0;
3103
+ statsData.misses = 0;
3104
+ statsData.evictions = 0;
2840
3105
  };
2841
3106
  }
2842
3107
  memoizedFn.refresh = (...args) => {
2843
- const key = generateKey(...args);
2844
- cache.delete(key);
3108
+ const { cacheable, key, entries } = lookup(args);
3109
+ if (cacheable) entries.delete(key);
2845
3110
  return memoizedFn(...args);
2846
3111
  };
2847
3112
  return memoizedFn;
@@ -2905,66 +3170,6 @@ var LRUCache = class {
2905
3170
  return this.cache.size;
2906
3171
  }
2907
3172
  };
2908
- var TTLCache = class {
2909
- constructor(ttl, options = {}) {
2910
- this.ttl = ttl;
2911
- this.cache = /* @__PURE__ */ new Map();
2912
- this.timers = /* @__PURE__ */ new Map();
2913
- this.onEvict = options.onEvict;
2914
- }
2915
- get(key) {
2916
- if (this.cache.has(key)) {
2917
- const entry = this.cache.get(key);
2918
- if (Date.now() < entry.expires) {
2919
- return entry.value;
2920
- } else {
2921
- this.delete(key);
2922
- }
2923
- }
2924
- return void 0;
2925
- }
2926
- set(key, value) {
2927
- if (this.timers.has(key)) {
2928
- clearTimeout(this.timers.get(key));
2929
- }
2930
- const expires = Date.now() + this.ttl;
2931
- this.cache.set(key, { value, expires });
2932
- const timer = setTimeout(() => {
2933
- this.delete(key);
2934
- }, this.ttl);
2935
- this.timers.set(key, timer);
2936
- }
2937
- has(key) {
2938
- if (this.cache.has(key)) {
2939
- const entry = this.cache.get(key);
2940
- return Date.now() < entry.expires;
2941
- }
2942
- return false;
2943
- }
2944
- delete(key) {
2945
- const had = this.cache.has(key);
2946
- if (had) {
2947
- const entry = this.cache.get(key);
2948
- this.cache.delete(key);
2949
- if (this.timers.has(key)) {
2950
- clearTimeout(this.timers.get(key));
2951
- this.timers.delete(key);
2952
- }
2953
- if (this.onEvict) {
2954
- this.onEvict(key, entry.value);
2955
- }
2956
- }
2957
- return had;
2958
- }
2959
- clear() {
2960
- this.timers.forEach((timer) => clearTimeout(timer));
2961
- this.timers.clear();
2962
- this.cache.clear();
2963
- }
2964
- get size() {
2965
- return this.cache.size;
2966
- }
2967
- };
2968
3173
  function shallowEqual(a, b) {
2969
3174
  if (a === b) return true;
2970
3175
  if (!a || !b) return false;
@@ -3158,7 +3363,9 @@ function createStateContainer(initialState, options) {
3158
3363
  storageKey,
3159
3364
  storage,
3160
3365
  reducer,
3161
- middleware,
3366
+ // Defaulted here: withStateUtils.shared() passes {}, and spreading
3367
+ // an undefined middleware list threw "middleware is not iterable".
3368
+ middleware = [],
3162
3369
  validator,
3163
3370
  onStateChange,
3164
3371
  debug
@@ -3213,7 +3420,7 @@ function createStateContainer(initialState, options) {
3213
3420
  try {
3214
3421
  listener(state, prevState);
3215
3422
  } catch (_error) {
3216
- if (debug) console.error("State listener _error:", _error);
3423
+ if (debug) console.error("State listener error:", _error);
3217
3424
  }
3218
3425
  });
3219
3426
  if (onStateChange) {
@@ -3357,8 +3564,10 @@ var withStateUtils = {
3357
3564
  }
3358
3565
  }),
3359
3566
  /**
3360
- * State with loading/_error handling
3567
+ * State with loading/error handling
3361
3568
  */
3569
+ // `_loading` / `_error` are the documented state keys (docs/components/state.md).
3570
+ /* eslint-disable no-restricted-syntax */
3362
3571
  withLoading: async (initialState) => withState({
3363
3572
  ...initialState,
3364
3573
  _loading: false,
@@ -3388,6 +3597,7 @@ var withStateUtils = {
3388
3597
  }
3389
3598
  }
3390
3599
  }),
3600
+ /* eslint-enable no-restricted-syntax */
3391
3601
  /**
3392
3602
  * State with undo/redo functionality
3393
3603
  */
@@ -3488,7 +3698,7 @@ var LIFECYCLE_PHASES = {
3488
3698
  UPDATED: "updated",
3489
3699
  BEFORE_UNMOUNT: "beforeUnmount",
3490
3700
  UNMOUNTED: "unmounted",
3491
- ERROR: "_error"
3701
+ ERROR: "error"
3492
3702
  };
3493
3703
  var componentInstances = /* @__PURE__ */ new WeakMap();
3494
3704
  var componentRegistry = /* @__PURE__ */ new Map();
@@ -3808,7 +4018,7 @@ var ComponentEventSystem = class {
3808
4018
  handler(event);
3809
4019
  } catch (_error) {
3810
4020
  globalErrorHandler.handle(_error, {
3811
- type: "event-handler-_error",
4021
+ type: "event-handler-error",
3812
4022
  context: { event, handler: handler.toString() }
3813
4023
  });
3814
4024
  }
@@ -3827,7 +4037,7 @@ var ComponentEventSystem = class {
3827
4037
  handler(event);
3828
4038
  } catch (_error) {
3829
4039
  globalErrorHandler.handle(_error, {
3830
- type: "global-event-handler-_error",
4040
+ type: "global-event-handler-error",
3831
4041
  context: { event, handler: handler.toString() }
3832
4042
  });
3833
4043
  }
@@ -4150,6 +4360,7 @@ var ComponentCache = class {
4150
4360
  this.cleanupTimer = setInterval(() => {
4151
4361
  this.cleanup();
4152
4362
  }, this.options.cleanupInterval);
4363
+ this.cleanupTimer.unref?.();
4153
4364
  }
4154
4365
  }
4155
4366
  /**
@@ -4439,6 +4650,37 @@ function memoize(component, keyGenerator, options = {}) {
4439
4650
  }
4440
4651
 
4441
4652
  // src/components/error-boundary.js
4653
+ function isBrowser() {
4654
+ return typeof window !== "undefined" && typeof document !== "undefined";
4655
+ }
4656
+ function resolveNestedComponents(node, depth = 0) {
4657
+ if (depth > 1e3) return node;
4658
+ if (typeof node === "function") {
4659
+ return resolveNestedComponents(node(), depth + 1);
4660
+ }
4661
+ if (Array.isArray(node)) {
4662
+ let changed = false;
4663
+ const resolved = node.map((child) => {
4664
+ const result = resolveNestedComponents(child, depth + 1);
4665
+ if (result !== child) changed = true;
4666
+ return result;
4667
+ });
4668
+ return changed ? resolved : node;
4669
+ }
4670
+ if (node && typeof node === "object" && !isTrustedContent(node)) {
4671
+ const keys = Object.keys(node);
4672
+ if (keys.length !== 1) return node;
4673
+ const tag = keys[0];
4674
+ const props = node[tag];
4675
+ if (props && typeof props === "object" && !Array.isArray(props) && props.children !== void 0) {
4676
+ const children = resolveNestedComponents(props.children, depth + 1);
4677
+ if (children !== props.children) {
4678
+ return { [tag]: { ...props, children } };
4679
+ }
4680
+ }
4681
+ }
4682
+ return node;
4683
+ }
4442
4684
  var ErrorBoundaryState = class {
4443
4685
  constructor() {
4444
4686
  this.hasError = false;
@@ -4470,19 +4712,23 @@ function createErrorBoundary(options = {}) {
4470
4712
  maxErrors = Infinity,
4471
4713
  resetTimeout = null
4472
4714
  } = options;
4473
- const state = new ErrorBoundaryState();
4715
+ const sharedState = new ErrorBoundaryState();
4474
4716
  let previousProps = {};
4475
4717
  let resetTimer = null;
4476
4718
  return function errorBoundaryWrapper(component) {
4477
4719
  return function wrappedComponent(props = {}) {
4720
+ const persistent = isBrowser();
4721
+ const state = persistent ? sharedState : new ErrorBoundaryState();
4478
4722
  try {
4479
- if (resetOnPropsChange && shouldReset(props, previousProps, resetKeys)) {
4723
+ if (persistent && resetOnPropsChange && shouldReset(props, previousProps, resetKeys)) {
4480
4724
  state.reset();
4481
4725
  if (onReset) {
4482
4726
  onReset();
4483
4727
  }
4484
4728
  }
4485
- previousProps = { ...props };
4729
+ if (persistent) {
4730
+ previousProps = { ...props };
4731
+ }
4486
4732
  if (state.hasError) {
4487
4733
  if (state.errorCount >= maxErrors) {
4488
4734
  return typeof fallback === "function" ? fallback(state.error, state.errorInfo, { permanent: true }) : fallback;
@@ -4499,7 +4745,7 @@ function createErrorBoundary(options = {}) {
4499
4745
  return fallbackComponent;
4500
4746
  }
4501
4747
  const result = typeof component === "function" ? component(props) : component;
4502
- return result;
4748
+ return resolveNestedComponents(result);
4503
4749
  } catch (error) {
4504
4750
  const errorInfo = {
4505
4751
  componentStack: error.stack,
@@ -4514,7 +4760,7 @@ function createErrorBoundary(options = {}) {
4514
4760
  console.error("Error in onError callback:", callbackError);
4515
4761
  }
4516
4762
  }
4517
- if (resetTimeout && !resetTimer) {
4763
+ if (persistent && resetTimeout && !resetTimer) {
4518
4764
  resetTimer = setTimeout(() => {
4519
4765
  state.reset();
4520
4766
  resetTimer = null;
@@ -4630,9 +4876,10 @@ function createAsyncErrorBoundary(options = {}) {
4630
4876
  } = options;
4631
4877
  return function asyncBoundaryWrapper(asyncComponent) {
4632
4878
  return async function wrappedAsyncComponent(props = {}) {
4879
+ let timer;
4633
4880
  try {
4634
4881
  const timeoutPromise = new Promise((_, reject) => {
4635
- setTimeout(() => reject(new Error("Component load timeout")), timeout);
4882
+ timer = setTimeout(() => reject(new Error("Component load timeout")), timeout);
4636
4883
  });
4637
4884
  const result = await Promise.race([
4638
4885
  typeof asyncComponent === "function" ? asyncComponent(props) : asyncComponent,
@@ -4644,6 +4891,8 @@ function createAsyncErrorBoundary(options = {}) {
4644
4891
  onError(error, { props, async: true });
4645
4892
  }
4646
4893
  return typeof errorFallback === "function" ? errorFallback(error, { props }) : errorFallback;
4894
+ } finally {
4895
+ clearTimeout(timer);
4647
4896
  }
4648
4897
  };
4649
4898
  };
@@ -4738,7 +4987,7 @@ function renderWithTemplate(component, options = {}) {
4738
4987
  template = "<!DOCTYPE html>\n{{content}}"
4739
4988
  } = options;
4740
4989
  const html = renderWithMonitoring(component, options);
4741
- return template.replace("{{content}}", html);
4990
+ return template.replace("{{content}}", () => html);
4742
4991
  }
4743
4992
  async function renderComponentFactory(componentFactory, factoryArgs, options = {}) {
4744
4993
  const component = await Promise.resolve(
@@ -5276,6 +5525,29 @@ var EventBus = class {
5276
5525
  }
5277
5526
  return false;
5278
5527
  }
5528
+ /**
5529
+ * A view of this bus whose event and action names are prefixed with
5530
+ * `${scope}:`. It shares listeners with the bus, so `scope:event` can
5531
+ * still be observed from the unscoped bus. withEventBus({ scope }) and
5532
+ * emitEvent(name, { scope }) called this, but it didn't exist, so any
5533
+ * scoped usage threw "createScope is not a function".
5534
+ *
5535
+ * @param {string} scope - Scope name
5536
+ * @returns {Object} Scoped event bus
5537
+ */
5538
+ createScope(scope) {
5539
+ const prefix = `${scope}:`;
5540
+ return {
5541
+ emit: (event, data) => this.emit(prefix + event, data),
5542
+ emitSync: (event, data) => this.emitSync(prefix + event, data),
5543
+ on: (event, listener, options) => this.on(prefix + event, listener, options),
5544
+ once: (event, listener, options) => this.once(prefix + event, listener, options),
5545
+ off: (event, listenerId) => this.off(prefix + event, listenerId),
5546
+ registerAction: (action, handler) => this.registerAction(prefix + action, handler),
5547
+ handleAction: (action, element, event, data) => this.handleAction(prefix + action, element, event, data),
5548
+ createScope: (child) => this.createScope(prefix + child)
5549
+ };
5550
+ }
5279
5551
  /**
5280
5552
  * Remove all listeners for an event
5281
5553
  */
@@ -5889,7 +6161,8 @@ function withEventBus(options = {}) {
5889
6161
  function EventBusComponent(props = {}, state = {}, context = {}) {
5890
6162
  const bus = scope ? eventBus.createScope(scope) : eventBus;
5891
6163
  const listenerIds = /* @__PURE__ */ new Map();
5892
- Object.entries(events).forEach(([event, handler]) => {
6164
+ const interactive = typeof window !== "undefined" && typeof document !== "undefined";
6165
+ if (interactive) Object.entries(events).forEach(([event, handler]) => {
5893
6166
  const listenerId = bus.on(event, (data, eventName) => {
5894
6167
  if (typeof handler === "function") {
5895
6168
  handler.call(this, data, eventName, { props, state, context });
@@ -5897,7 +6170,7 @@ function withEventBus(options = {}) {
5897
6170
  });
5898
6171
  listenerIds.set(event, listenerId);
5899
6172
  });
5900
- Object.entries(actions).forEach(([action, handler]) => {
6173
+ if (interactive) Object.entries(actions).forEach(([action, handler]) => {
5901
6174
  bus.registerAction(action, (actionContext) => {
5902
6175
  if (typeof handler === "function") {
5903
6176
  handler.call(this, actionContext, { props, state, context });
@@ -5941,7 +6214,10 @@ function withEventBus(options = {}) {
5941
6214
  originalUnmount.call(this);
5942
6215
  };
5943
6216
  } else {
5944
- result.__eventBusCleanup = eventUtils.cleanup;
6217
+ Object.defineProperty(result, "__eventBusCleanup", {
6218
+ value: eventUtils.cleanup,
6219
+ configurable: true
6220
+ });
5945
6221
  }
5946
6222
  }
5947
6223
  return result;
@@ -6269,6 +6545,7 @@ var eventSystem2 = {
6269
6545
  registerAction: globalEventBus.registerAction.bind(globalEventBus),
6270
6546
  registerActions: globalEventBus.registerActions.bind(globalEventBus),
6271
6547
  handleAction: globalEventBus.handleAction.bind(globalEventBus),
6548
+ createScope: globalEventBus.createScope.bind(globalEventBus),
6272
6549
  // Statistics and debugging
6273
6550
  getStats: globalEventBus.getStats.bind(globalEventBus),
6274
6551
  resetStats: globalEventBus.resetStats.bind(globalEventBus),
@@ -6372,23 +6649,74 @@ var compose = {
6372
6649
  };
6373
6650
 
6374
6651
  // src/index.js
6375
- var scopeCounter = { value: 0 };
6376
- function generateScopeId() {
6377
- return `coh-${scopeCounter.value++}`;
6652
+ function generateScopeId(cssText) {
6653
+ let hash = 2166136261;
6654
+ for (let i = 0; i < cssText.length; i++) {
6655
+ hash ^= cssText.charCodeAt(i);
6656
+ hash = Math.imul(hash, 16777619);
6657
+ }
6658
+ return `coh-${(hash >>> 0).toString(36)}`;
6659
+ }
6660
+ var GROUPING_AT_RULES = /* @__PURE__ */ new Set(["media", "supports", "container", "layer", "document", "scope"]);
6661
+ function splitSelectorList(prelude) {
6662
+ const parts = [];
6663
+ let depth = 0;
6664
+ let start = 0;
6665
+ for (let i = 0; i < prelude.length; i++) {
6666
+ const ch = prelude[i];
6667
+ if (ch === "(" || ch === "[") depth++;
6668
+ else if (ch === ")" || ch === "]") depth--;
6669
+ else if (ch === "," && depth === 0) {
6670
+ parts.push(prelude.slice(start, i));
6671
+ start = i + 1;
6672
+ }
6673
+ }
6674
+ parts.push(prelude.slice(start));
6675
+ return parts;
6676
+ }
6677
+ function scopeSelector(selector, scopeId) {
6678
+ const trimmed = selector.trim();
6679
+ if (!trimmed) return selector;
6680
+ if (trimmed.includes(":")) {
6681
+ return trimmed.replace(/([^:]+)(:.*)?/, `$1[${scopeId}]$2`);
6682
+ }
6683
+ return `${trimmed}[${scopeId}]`;
6378
6684
  }
6379
6685
  function scopeCSS(css, scopeId) {
6380
6686
  if (!css || typeof css !== "string") return css;
6381
- return css.replace(/([^{}]*)\s*{/g, (match, selector) => {
6382
- const selectors = selector.split(",").map((s) => {
6383
- const trimmed = s.trim();
6384
- if (!trimmed) return s;
6385
- if (trimmed.includes(":")) {
6386
- return trimmed.replace(/([^:]+)(:.*)?/, `$1[${scopeId}]$2`);
6387
- }
6388
- return `${trimmed}[${scopeId}]`;
6389
- });
6390
- return `${selectors.join(", ")} {`;
6391
- });
6687
+ let result = "";
6688
+ let i = 0;
6689
+ while (i < css.length) {
6690
+ const open = css.indexOf("{", i);
6691
+ if (open === -1) {
6692
+ result += css.slice(i);
6693
+ break;
6694
+ }
6695
+ let depth = 1;
6696
+ let j = open + 1;
6697
+ while (j < css.length && depth > 0) {
6698
+ if (css[j] === "{") depth++;
6699
+ else if (css[j] === "}") depth--;
6700
+ j++;
6701
+ }
6702
+ const body = css.slice(open + 1, depth === 0 ? j - 1 : j);
6703
+ const rawPrelude = css.slice(i, open);
6704
+ const semicolon = rawPrelude.lastIndexOf(";");
6705
+ const lead = semicolon === -1 ? "" : rawPrelude.slice(0, semicolon + 1);
6706
+ const prelude = semicolon === -1 ? rawPrelude : rawPrelude.slice(semicolon + 1);
6707
+ const trimmed = prelude.trim();
6708
+ if (trimmed.startsWith("@")) {
6709
+ const name = trimmed.slice(1).split(/[\s({]/)[0].toLowerCase();
6710
+ const inner = GROUPING_AT_RULES.has(name) ? scopeCSS(body, scopeId) : body;
6711
+ result += `${lead}${prelude}{${inner}}`;
6712
+ } else {
6713
+ const leading = prelude.match(/^\s*/)[0];
6714
+ const scoped = splitSelectorList(prelude).map((part) => scopeSelector(part, scopeId)).join(", ");
6715
+ result += `${lead}${leading}${scoped} {${body}}`;
6716
+ }
6717
+ i = j;
6718
+ }
6719
+ return result;
6392
6720
  }
6393
6721
  function applyScopeToElement(element, scopeId) {
6394
6722
  if (typeof element === "string" || typeof element === "number" || !element) {
@@ -6416,10 +6744,7 @@ function applyScopeToElement(element, scopeId) {
6416
6744
  return element;
6417
6745
  }
6418
6746
  function dangerouslySetInnerContent(content) {
6419
- return {
6420
- __html: content,
6421
- __trusted: true
6422
- };
6747
+ return createTrustedContent(content);
6423
6748
  }
6424
6749
  function injectHydrationAttributes(component, options) {
6425
6750
  if (!component || typeof component !== "object" || Array.isArray(component)) {
@@ -6451,20 +6776,42 @@ function Island(componentFn) {
6451
6776
  });
6452
6777
  };
6453
6778
  }
6454
- function render2(obj, options = {}) {
6779
+ function prepareRender(obj, options) {
6455
6780
  const scoped = options.scoped ?? options.encapsulate ?? false;
6456
6781
  const { scoped: _scoped, encapsulate: _encapsulate, hydratable: _hydratable, island: _island, ...rendererOptions } = options;
6457
- let component = scoped ? renderScopedComponent(obj) : obj;
6458
- if (typeof component === "function") {
6459
- component = component(options);
6782
+ let component = typeof obj === "function" ? obj(options) : obj;
6783
+ if (scoped) {
6784
+ component = renderScopedComponent(component);
6460
6785
  }
6461
6786
  if (_hydratable || _island) {
6462
6787
  component = injectHydrationAttributes(component, { hydratable: _hydratable, island: _island });
6463
6788
  }
6789
+ return { component, rendererOptions };
6790
+ }
6791
+ function render2(obj, options = {}) {
6792
+ const { component, rendererOptions } = prepareRender(obj, options);
6464
6793
  return render(component, rendererOptions);
6465
6794
  }
6795
+ function renderToStream2(obj, options = {}) {
6796
+ const { component, rendererOptions } = prepareRender(obj, options);
6797
+ return renderToStream(component, rendererOptions);
6798
+ }
6799
+ function collectStyleText(element, out = []) {
6800
+ if (Array.isArray(element)) {
6801
+ element.forEach((item) => collectStyleText(item, out));
6802
+ } else if (element && typeof element === "object") {
6803
+ for (const [tagName, props] of Object.entries(element)) {
6804
+ if (tagName === "style" && props && typeof props === "object" && typeof props.text === "string") {
6805
+ out.push(props.text);
6806
+ } else if (props && typeof props === "object" && props.children) {
6807
+ collectStyleText(props.children, out);
6808
+ }
6809
+ }
6810
+ }
6811
+ return out;
6812
+ }
6466
6813
  function renderScopedComponent(component) {
6467
- const scopeId = generateScopeId();
6814
+ const scopeId = generateScopeId(collectStyleText(component).join("\n"));
6468
6815
  function processScopedElement(element) {
6469
6816
  if (!element || typeof element !== "object") {
6470
6817
  return element;
@@ -6495,21 +6842,13 @@ function renderScopedComponent(component) {
6495
6842
  const scopedComponent = applyScopeToElement(processedComponent, scopeId);
6496
6843
  return scopedComponent;
6497
6844
  }
6498
- var memoCache = /* @__PURE__ */ new Map();
6499
- function memo2(component, keyGenerator) {
6500
- return function MemoizedComponent(props = {}) {
6501
- const key = keyGenerator ? keyGenerator(props) : JSON.stringify(props);
6502
- if (memoCache.has(key)) {
6503
- return memoCache.get(key);
6504
- }
6505
- const result = component(props);
6506
- memoCache.set(key, result);
6507
- if (memoCache.size > 100) {
6508
- const firstKey = memoCache.keys().next().value;
6509
- memoCache.delete(firstKey);
6510
- }
6511
- return result;
6512
- };
6845
+ function memo2(component, options = {}) {
6846
+ const withDefaultProps = (props = {}, ...rest) => component(props, ...rest);
6847
+ if (typeof options === "function") {
6848
+ const keyGenerator = options;
6849
+ return memo(withDefaultProps, { keyFn: (props = {}, ...rest) => keyGenerator(props, ...rest) });
6850
+ }
6851
+ return memo(withDefaultProps, options);
6513
6852
  }
6514
6853
  function validateComponent2(obj) {
6515
6854
  if (!obj || typeof obj !== "object") {
@@ -6530,7 +6869,7 @@ function deepClone2(obj) {
6530
6869
  }
6531
6870
  return cloned;
6532
6871
  }
6533
- var VERSION = true ? "1.1.2" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
6872
+ var VERSION = true ? "2.0.0-rc.0" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
6534
6873
  var fp = {
6535
6874
  /**
6536
6875
  * Curried map: fp.map(fn)(array)
@@ -6542,6 +6881,7 @@ var fp = {
6542
6881
  var coherent = {
6543
6882
  // Core rendering
6544
6883
  render: render2,
6884
+ renderToStream: renderToStream2,
6545
6885
  // Shadow DOM (client-side only)
6546
6886
  shadowDOM: shadow_dom_exports,
6547
6887
  // Component system
@@ -6659,6 +6999,7 @@ export {
6659
6999
  isLazy,
6660
7000
  isPeerDependencyAvailable,
6661
7001
  isTrustedContent,
7002
+ isValidAttributeName,
6662
7003
  isVoidElement,
6663
7004
  lazy,
6664
7005
  componentUtils as lifecycleUtils,
@@ -6674,9 +7015,11 @@ export {
6674
7015
  registerComponent,
6675
7016
  render2 as render,
6676
7017
  renderComponentFactory,
7018
+ renderToStream2 as renderToStream,
6677
7019
  renderWithMonitoring,
6678
7020
  renderWithTemplate,
6679
7021
  shadow_dom_exports as shadowDOM,
7022
+ streamingUtils,
6680
7023
  useHooks,
6681
7024
  validateComponent2 as validateComponent,
6682
7025
  validateNesting,