@coherent.js/core 1.1.0 → 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) {
@@ -604,10 +619,81 @@ function formatAttributes(props) {
604
619
  }
605
620
  return formatted.trim();
606
621
  }
622
+ function stripComments(html) {
623
+ let out = "";
624
+ let cursor = 0;
625
+ for (; ; ) {
626
+ const start = html.indexOf("<!--", cursor);
627
+ if (start === -1) return out + html.slice(cursor);
628
+ out += html.slice(cursor, start);
629
+ const end = html.indexOf("-->", start + 4);
630
+ if (end === -1) return out;
631
+ cursor = end + 3;
632
+ }
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
+ }
607
677
  function minifyHtml(html, options = {}) {
608
678
  if (!options.minify) return html;
609
- return html.replace(/<!--[\s\S]*?-->/g, "").replace(/\s+/g, " ").replace(/>\s+</g, "><").trim();
679
+ return stripComments(html).replace(/\s+/g, " ").replace(/>\s+</g, "><").trim();
610
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
+ ]);
611
697
 
612
698
  // src/core/object-utils.js
613
699
  function deepClone(obj, seen = /* @__PURE__ */ new WeakMap()) {
@@ -694,6 +780,9 @@ function validateComponent(component, path = "root") {
694
780
  if (typeof component === "function") {
695
781
  return true;
696
782
  }
783
+ if (isTrustedContent(component)) {
784
+ return true;
785
+ }
697
786
  if (Array.isArray(component)) {
698
787
  component.forEach((child, index) => {
699
788
  validateComponent(child, `${path}[${index}]`);
@@ -735,22 +824,6 @@ function isCoherentObject(obj) {
735
824
  return /^[a-zA-Z][a-zA-Z0-9-]*$/.test(key);
736
825
  });
737
826
  }
738
- function extractProps(coherentObj) {
739
- if (!isCoherentObject(coherentObj)) {
740
- return {};
741
- }
742
- const props = {};
743
- const keys = Object.keys(coherentObj);
744
- keys.forEach((tag) => {
745
- const value = coherentObj[tag];
746
- if (value && typeof value === "object" && !Array.isArray(value)) {
747
- props[tag] = { ...value };
748
- } else {
749
- props[tag] = { text: value };
750
- }
751
- });
752
- return props;
753
- }
754
827
  function hasChildren(component) {
755
828
  if (Array.isArray(component)) {
756
829
  return component.length > 0;
@@ -772,6 +845,15 @@ function normalizeChildren(children) {
772
845
  return [];
773
846
  }
774
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;
775
857
  return children.flat().filter((child) => child !== null && child !== void 0);
776
858
  }
777
859
  return [children];
@@ -785,7 +867,9 @@ var DEFAULT_RENDERER_CONFIG = {
785
867
  enableMonitoring: false,
786
868
  validateInput: true,
787
869
  // HTML Renderer specific options
788
- 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,
789
873
  minify: false,
790
874
  cacheSize: 1e3,
791
875
  cacheTTL: 3e5,
@@ -871,7 +955,7 @@ var BaseRenderer = class {
871
955
  return {
872
956
  ...baseConfig,
873
957
  // HTML-specific defaults
874
- enableCache: baseConfig.enableCache !== false,
958
+ enableCache: baseConfig.enableCache === true,
875
959
  enableMonitoring: baseConfig.enableMonitoring !== false
876
960
  };
877
961
  case "streaming":
@@ -904,11 +988,14 @@ var BaseRenderer = class {
904
988
  /**
905
989
  * Check if component is valid for rendering
906
990
  */
907
- isValidComponent(component) {
991
+ isValidComponent(component, depth = 0) {
908
992
  if (component === null || component === void 0) return true;
909
993
  if (typeof component === "string" || typeof component === "number") return true;
910
994
  if (typeof component === "function") return true;
911
- 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
+ }
912
999
  if (isCoherentObject(component)) return true;
913
1000
  return false;
914
1001
  }
@@ -930,7 +1017,10 @@ var BaseRenderer = class {
930
1017
  if (typeof component === "string") {
931
1018
  return { type: "text", value: component };
932
1019
  }
933
- if (typeof component === "number" || typeof component === "boolean") {
1020
+ if (typeof component === "boolean") {
1021
+ return { type: "empty", value: "" };
1022
+ }
1023
+ if (typeof component === "number") {
934
1024
  return { type: "text", value: String(component) };
935
1025
  }
936
1026
  if (typeof component === "function") {
@@ -945,31 +1035,23 @@ var BaseRenderer = class {
945
1035
  return { type: "unknown", value: component };
946
1036
  }
947
1037
  /**
948
- * 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.
949
1042
  */
950
1043
  executeFunctionComponent(func, depth = 0) {
951
1044
  try {
952
- const isContextProvider = func.length > 0 || func.isContextProvider;
953
- let result;
954
- if (isContextProvider) {
955
- result = func((children) => {
956
- return this.renderComponent(children, this.config, depth + 1);
957
- });
958
- } else {
959
- result = func();
960
- }
1045
+ const result = func();
961
1046
  if (typeof result === "function") {
962
1047
  return this.executeFunctionComponent(result, depth);
963
1048
  }
964
1049
  return result;
965
- } catch (_error) {
1050
+ } catch (error) {
966
1051
  if (this.config.enableMonitoring) {
967
- performanceMonitor.recordError("functionComponent", _error);
968
- }
969
- if (typeof process !== "undefined" && process.env && true) {
970
- console.warn("Coherent.js Function Component Error:", _error.message);
1052
+ performanceMonitor.recordError("functionComponent", error);
971
1053
  }
972
- return null;
1054
+ throw error;
973
1055
  }
974
1056
  }
975
1057
  /**
@@ -1070,88 +1152,43 @@ var BaseRenderer = class {
1070
1152
  throw new Error("render must be implemented by subclass");
1071
1153
  }
1072
1154
  };
1073
- var RendererUtils = {
1074
- /**
1075
- * Check if element is static (no functions or circular references)
1076
- */
1077
- isStaticElement(element, visited = /* @__PURE__ */ new WeakSet()) {
1078
- if (!element || typeof element !== "object") {
1079
- return typeof element === "string" || typeof element === "number";
1080
- }
1081
- if (visited.has(element)) {
1082
- return false;
1083
- }
1084
- visited.add(element);
1085
- for (const [_key, value] of Object.entries(element)) {
1086
- if (typeof value === "function") return false;
1087
- if (Array.isArray(value)) {
1088
- const allStatic = value.every((child) => RendererUtils.isStaticElement(child, visited));
1089
- if (!allStatic) return false;
1090
- } else if (typeof value === "object" && value !== null) {
1091
- if (!RendererUtils.isStaticElement(value, visited)) return false;
1092
- }
1093
- }
1094
- return true;
1095
- },
1096
- /**
1097
- * Check if object has functions (for caching decisions)
1098
- */
1099
- hasFunctions(obj, visited = /* @__PURE__ */ new WeakSet()) {
1100
- if (visited.has(obj)) return false;
1101
- visited.add(obj);
1102
- for (const value of Object.values(obj)) {
1103
- if (typeof value === "function") return true;
1104
- if (typeof value === "object" && value !== null && RendererUtils.hasFunctions(value, visited)) {
1105
- return true;
1106
- }
1107
- }
1108
- return false;
1109
- },
1110
- /**
1111
- * Get element complexity score
1112
- */
1113
- getElementComplexity(element) {
1114
- if (!element || typeof element !== "object") return 1;
1115
- let complexity = Object.keys(element).length;
1116
- if (element.children && Array.isArray(element.children)) {
1117
- complexity += element.children.reduce(
1118
- (sum, child) => sum + RendererUtils.getElementComplexity(child),
1119
- 0
1120
- );
1121
- }
1122
- return complexity;
1123
- },
1124
- /**
1125
- * Generate cache key for element
1126
- */
1127
- generateCacheKey(tagName, element) {
1128
- try {
1129
- const keyData = {
1130
- tag: tagName,
1131
- props: extractProps(element),
1132
- hasChildren: hasChildren(element),
1133
- childrenType: Array.isArray(element.children) ? "array" : typeof element.children
1134
- };
1135
- return `element:${JSON.stringify(keyData)}`;
1136
- } catch (_error) {
1137
- if (typeof process !== "undefined" && process.env && true) {
1138
- console.warn("Failed to generate cache key:", _error);
1139
- }
1140
- 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}}`;
1141
1187
  }
1142
- },
1143
- /**
1144
- * Check if element is cacheable
1145
- */
1146
- isCacheable(element, options) {
1147
- if (!options.enableCache) return false;
1148
- if (RendererUtils.hasFunctions(element)) return false;
1149
- if (RendererUtils.getElementComplexity(element) > 1e3) return false;
1150
- const cacheKey = RendererUtils.generateCacheKey(element.tagName || "unknown", element);
1151
- if (!cacheKey) return false;
1152
- return true;
1188
+ default:
1189
+ throw UNCACHEABLE;
1153
1190
  }
1154
- };
1191
+ }
1155
1192
 
1156
1193
  // src/core/html-nesting-rules.js
1157
1194
  var FORBIDDEN_CHILDREN = {
@@ -1237,12 +1274,13 @@ var HTMLNestingError = class extends Error {
1237
1274
  // src/performance/cache-manager.js
1238
1275
  function createCacheManager(options = {}) {
1239
1276
  const {
1240
- maxCacheSize = 1e3,
1241
1277
  maxMemoryMB = 100,
1242
1278
  ttlMs = 1e3 * 60 * 5,
1243
1279
  // 5 minutes
1244
1280
  enableStatistics = true
1245
1281
  } = options;
1282
+ const maxCacheSize = options.maxCacheSize ?? options.maxSize ?? 1e3;
1283
+ const maxMemoryBytes = maxMemoryMB * 1024 * 1024;
1246
1284
  const caches = {
1247
1285
  static: /* @__PURE__ */ new Map(),
1248
1286
  // Never-changing components
@@ -1285,51 +1323,57 @@ function createCacheManager(options = {}) {
1285
1323
  return `${extractComponentName(component)}_${hash}`;
1286
1324
  }
1287
1325
  function get(key, type = "component") {
1288
- const cache = caches[type] || caches.component;
1326
+ const cacheType = caches[type] ? type : "component";
1327
+ const cache = caches[cacheType];
1289
1328
  const entry = cache.get(key);
1290
1329
  if (!entry) {
1291
1330
  stats.misses++;
1292
- if (enableStatistics) stats.accessCount[type]++;
1331
+ if (enableStatistics) stats.accessCount[cacheType]++;
1293
1332
  return null;
1294
1333
  }
1295
- if (Date.now() - entry.timestamp > ttlMs) {
1334
+ if (Date.now() - entry.timestamp > entry.ttl) {
1296
1335
  cache.delete(key);
1297
1336
  updateMemoryUsage(-entry.size);
1298
1337
  stats.misses++;
1299
- if (enableStatistics) stats.accessCount[type]++;
1338
+ if (enableStatistics) stats.accessCount[cacheType]++;
1300
1339
  return null;
1301
1340
  }
1341
+ cache.delete(key);
1342
+ cache.set(key, entry);
1302
1343
  entry.lastAccess = Date.now();
1303
1344
  entry.accessCount++;
1304
1345
  stats.hits++;
1305
1346
  if (enableStatistics) {
1306
- stats.accessCount[type]++;
1307
- 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;
1308
1349
  }
1309
1350
  return entry.value;
1310
1351
  }
1311
1352
  function set(key, value, type = "component", metadata = {}) {
1312
- const cache = caches[type] || caches.component;
1313
- const size = calculateSize(value);
1314
- if (memoryUsage + size > maxMemoryMB * 1024 * 1024) {
1315
- 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);
1316
1361
  }
1317
- const entry = {
1362
+ const now = Date.now();
1363
+ cache.set(key, {
1318
1364
  value,
1319
- timestamp: Date.now(),
1320
- lastAccess: Date.now(),
1365
+ timestamp: now,
1366
+ lastAccess: now,
1321
1367
  size,
1322
1368
  metadata,
1369
+ ttl: typeof metadata.ttlMs === "number" ? metadata.ttlMs : ttlMs,
1323
1370
  accessCount: 0
1324
- };
1325
- const existing = cache.get(key);
1326
- if (existing) {
1327
- updateMemoryUsage(-existing.size);
1328
- }
1329
- cache.set(key, entry);
1371
+ });
1330
1372
  updateMemoryUsage(size);
1331
- if (cache.size > maxCacheSize) {
1332
- 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);
1333
1377
  }
1334
1378
  }
1335
1379
  function remove(key, type) {
@@ -1356,11 +1400,14 @@ function createCacheManager(options = {}) {
1356
1400
  if (type) {
1357
1401
  const cache = caches[type];
1358
1402
  if (cache) {
1403
+ for (const entry of cache.values()) {
1404
+ updateMemoryUsage(-entry.size);
1405
+ }
1359
1406
  cache.clear();
1360
1407
  }
1361
- } else {
1362
- Object.values(caches).forEach((cache) => cache.clear());
1408
+ return;
1363
1409
  }
1410
+ Object.values(caches).forEach((cache) => cache.clear());
1364
1411
  memoryUsage = 0;
1365
1412
  }
1366
1413
  function getStats() {
@@ -1379,7 +1426,7 @@ function createCacheManager(options = {}) {
1379
1426
  let freed = 0;
1380
1427
  for (const [, cache] of Object.entries(caches)) {
1381
1428
  for (const [key, entry] of cache.entries()) {
1382
- if (now - entry.timestamp > ttlMs) {
1429
+ if (now - entry.timestamp > entry.ttl) {
1383
1430
  cache.delete(key);
1384
1431
  updateMemoryUsage(-entry.size);
1385
1432
  freed++;
@@ -1404,17 +1451,15 @@ function createCacheManager(options = {}) {
1404
1451
  function updateMemoryUsage(delta) {
1405
1452
  memoryUsage = Math.max(0, memoryUsage + delta);
1406
1453
  }
1407
- function optimize(type, requiredSpace = 0) {
1408
- const cache = caches[type] || caches.component;
1409
- const entries = Array.from(cache.entries()).sort(([, a], [, b]) => a.lastAccess - b.lastAccess);
1454
+ function evict(cache, shouldEvict) {
1410
1455
  let freed = 0;
1411
- for (const [key, entry] of entries) {
1412
- if (freed >= requiredSpace) break;
1456
+ for (const [key, entry] of cache) {
1457
+ if (!shouldEvict()) break;
1413
1458
  cache.delete(key);
1414
1459
  updateMemoryUsage(-entry.size);
1415
1460
  freed += entry.size;
1416
1461
  }
1417
- return { freed };
1462
+ return freed;
1418
1463
  }
1419
1464
  function simpleHash(str) {
1420
1465
  let hash = 0;
@@ -1569,7 +1614,7 @@ var ErrorHandler = class {
1569
1614
  enableStackTrace: options.enableStackTrace !== false,
1570
1615
  enableSuggestions: options.enableSuggestions !== false,
1571
1616
  enableLogging: options.enableLogging ?? defaultEnableLogging,
1572
- logLevel: options.logLevel || "_error",
1617
+ logLevel: options.logLevel || "error",
1573
1618
  maxErrorHistory: options.maxErrorHistory || 100,
1574
1619
  ...options
1575
1620
  };
@@ -1720,7 +1765,7 @@ var ErrorHandler = class {
1720
1765
  if (suggestions.length === 0) {
1721
1766
  suggestions.push(
1722
1767
  "Enable development tools for more detailed debugging",
1723
- "Check browser console for additional _error details",
1768
+ "Check browser console for additional error details",
1724
1769
  "Use component validation tools to identify issues"
1725
1770
  );
1726
1771
  }
@@ -1862,15 +1907,34 @@ var ErrorHandler = class {
1862
1907
  var globalErrorHandler = new ErrorHandler();
1863
1908
 
1864
1909
  // src/rendering/html-renderer.js
1910
+ var RESERVED_PROPS = /* @__PURE__ */ new Set(["children", "text", "key", "html"]);
1865
1911
  var rendererCache = createCacheManager({
1866
- maxSize: 1e3,
1912
+ maxCacheSize: 1e3,
1867
1913
  ttlMs: 3e5
1868
1914
  // 5 minutes
1869
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
+ }
1870
1928
  function formatRenderPath(path) {
1871
- 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
+ }
1872
1936
  let rendered = "root";
1873
- for (const segment of path) {
1937
+ for (const segment of segments) {
1874
1938
  if (typeof segment !== "string" || segment.length === 0) continue;
1875
1939
  if (segment.startsWith("[")) {
1876
1940
  rendered += segment;
@@ -1883,15 +1947,15 @@ function formatRenderPath(path) {
1883
1947
  var HTMLRenderer = class extends BaseRenderer {
1884
1948
  constructor(options = {}) {
1885
1949
  super({
1886
- enableCache: options.enableCache !== false,
1887
1950
  enableMonitoring: options.enableMonitoring !== false,
1888
1951
  minify: options.minify || false,
1889
1952
  streaming: options.streaming || false,
1890
1953
  maxDepth: options.maxDepth || 100,
1891
- ...options
1954
+ ...options,
1955
+ enableCache: options.enableCache === true
1892
1956
  });
1893
- if (this.config.enableCache && !this.cache) {
1894
- this.cache = rendererCache;
1957
+ if (this.config.enableCache) {
1958
+ this.cache = options.cache || rendererCache;
1895
1959
  }
1896
1960
  }
1897
1961
  /**
@@ -1919,15 +1983,30 @@ var HTMLRenderer = class extends BaseRenderer {
1919
1983
  const config = { ...this.config, ...options };
1920
1984
  this.startTiming();
1921
1985
  try {
1986
+ assertNotThenable(component, "root");
1922
1987
  if (config.validateInput && !this.isValidComponent(component)) {
1923
1988
  throw new Error("Invalid component structure");
1924
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
+ }
1925
1999
  const renderOptions = {
1926
2000
  ...config,
1927
2001
  seenObjects: /* @__PURE__ */ new WeakSet()
1928
2002
  };
1929
- const html = this.renderComponent(component, renderOptions, 0, []);
2003
+ const html = this.renderComponent(component, renderOptions, 0, null);
1930
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
+ }
1931
2010
  this.endTiming();
1932
2011
  this.recordPerformance("render", this.metrics.startTime, false, {
1933
2012
  cacheEnabled: config.enableCache
@@ -1947,7 +2026,7 @@ var HTMLRenderer = class extends BaseRenderer {
1947
2026
  /**
1948
2027
  * Render a single component with full optimization pipeline
1949
2028
  */
1950
- renderComponent(component, options, depth = 0, path = []) {
2029
+ renderComponent(component, options, depth = 0, path = null) {
1951
2030
  if (component === null || component === void 0) {
1952
2031
  return "";
1953
2032
  }
@@ -1957,8 +2036,13 @@ var HTMLRenderer = class extends BaseRenderer {
1957
2036
  if (isTrustedContent(component)) {
1958
2037
  return component.__html;
1959
2038
  }
1960
- if (typeof component === "object" && component !== null && !Array.isArray(component)) {
1961
- 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)) {
1962
2046
  throw new RenderingError(
1963
2047
  "Circular reference detected in component tree",
1964
2048
  component,
@@ -1966,9 +2050,7 @@ var HTMLRenderer = class extends BaseRenderer {
1966
2050
  ["Remove the circular reference", "Use lazy loading to break the cycle"]
1967
2051
  );
1968
2052
  }
1969
- if (options.seenObjects) {
1970
- options.seenObjects.add(component);
1971
- }
2053
+ options.seenObjects.add(tracked);
1972
2054
  }
1973
2055
  this.validateDepth(depth);
1974
2056
  try {
@@ -1979,8 +2061,8 @@ var HTMLRenderer = class extends BaseRenderer {
1979
2061
  case "text":
1980
2062
  return escapeHtml(value);
1981
2063
  case "function": {
1982
- const result = this.executeFunctionComponent(value, depth);
1983
- 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, "()"));
1984
2066
  }
1985
2067
  case "array":
1986
2068
  if (typeof process !== "undefined" && process.env && true && value.length > 1) {
@@ -1998,11 +2080,23 @@ var HTMLRenderer = class extends BaseRenderer {
1998
2080
  );
1999
2081
  }
2000
2082
  }
2001
- 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
+ }
2002
2090
  case "element": {
2003
- const tagName = Object.keys(value)[0];
2004
- const elementContent = value[tagName];
2005
- 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;
2006
2100
  }
2007
2101
  default:
2008
2102
  this.recordError("renderComponent", new Error(`Unknown component type: ${type}`));
@@ -2018,16 +2112,34 @@ var HTMLRenderer = class extends BaseRenderer {
2018
2112
  }
2019
2113
  throw _error;
2020
2114
  }
2021
- 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;
2022
2134
  }
2023
2135
  }
2024
2136
  /**
2025
2137
  * Render an HTML element with advanced caching and optimization
2026
2138
  */
2027
- renderElement(tagName, element, options, depth = 0, path = []) {
2028
- const startTime = performance.now();
2029
- if (element && typeof element === "object" && !Array.isArray(element)) {
2030
- 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)) {
2031
2143
  throw new RenderingError(
2032
2144
  "Circular reference detected in component tree",
2033
2145
  element,
@@ -2035,32 +2147,31 @@ var HTMLRenderer = class extends BaseRenderer {
2035
2147
  ["Remove the circular reference", "Use lazy loading to break the cycle"]
2036
2148
  );
2037
2149
  }
2038
- if (options.seenObjects) {
2039
- options.seenObjects.add(element);
2040
- }
2041
- }
2042
- if (options.enableMonitoring && this.cache) {
2150
+ options.seenObjects.add(tracked);
2043
2151
  }
2044
- if (options.enableCache && this.cache && RendererUtils.isStaticElement(element)) {
2045
- try {
2046
- const cacheKey = `static:${tagName}:${JSON.stringify(element)}`;
2047
- const cached = this.cache.get("static", cacheKey);
2048
- if (cached) {
2049
- this.recordPerformance(tagName, startTime, true);
2050
- return cached.value;
2051
- }
2052
- } catch {
2053
- }
2152
+ try {
2153
+ return this.renderElementContent(tagName, element, options, depth, path);
2154
+ } finally {
2155
+ if (tracked) options.seenObjects.delete(tracked);
2054
2156
  }
2157
+ }
2158
+ renderElementContent(tagName, element, options, depth = 0, path = null) {
2159
+ const startTime = options.enableMonitoring ? performance.now() : 0;
2055
2160
  if (typeof element === "string" || typeof element === "number" || typeof element === "boolean") {
2056
2161
  const html2 = isVoidElement(tagName) ? `<${tagName}>` : `<${tagName}>${escapeHtml(String(element))}</${tagName}>`;
2057
- this.cacheIfStatic(tagName, element, html2, options);
2058
2162
  this.recordPerformance(tagName, startTime, false);
2059
2163
  return html2;
2060
2164
  }
2061
2165
  if (typeof element === "function") {
2062
- const result = this.executeFunctionComponent(element, depth);
2063
- 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, "()"));
2064
2175
  }
2065
2176
  if (element && typeof element === "object") {
2066
2177
  return this.renderObjectElement(tagName, element, options, depth, path);
@@ -2075,105 +2186,255 @@ var HTMLRenderer = class extends BaseRenderer {
2075
2186
  return html;
2076
2187
  }
2077
2188
  /**
2078
- * Cache element if it's static
2189
+ * Render complex object elements with attributes and children
2079
2190
  */
2080
- cacheIfStatic(tagName, element, html) {
2081
- if (this.config.enableCache && this.cache && RendererUtils.isStaticElement(element)) {
2082
- try {
2083
- const cacheKey = `static:${tagName}:${JSON.stringify(element)}`;
2084
- this.cache.set("static", cacheKey, html, {
2085
- ttlMs: this.config.cacheTTL || 5 * 60 * 1e3,
2086
- // 5 minutes default
2087
- size: html.length
2088
- // Approximate size
2089
- });
2090
- } 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);
2091
2202
  }
2092
2203
  }
2204
+ html += parts.close;
2205
+ this.recordPerformance(tagName, startTime, false);
2206
+ return html;
2093
2207
  }
2094
2208
  /**
2095
- * 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.
2096
2213
  */
2097
- renderObjectElement(tagName, element, options, depth = 0, path = []) {
2098
- const startTime = performance.now();
2099
- if (options.enableCache && this.cache) {
2100
- const cacheKey = RendererUtils.generateCacheKey(tagName, element);
2101
- if (cacheKey) {
2102
- const cached = this.cache.get(cacheKey);
2103
- if (cached) {
2104
- this.recordPerformance(tagName, startTime, true);
2105
- 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}]`));
2106
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;
2107
2239
  }
2108
2240
  }
2109
- const { children, text, key: _key, html: _rawHtml, ...attributes } = element || {};
2110
- const attributeString = formatAttributes(attributes);
2111
- const openingTag = attributeString ? `<${tagName} ${attributeString}>` : `<${tagName}>`;
2112
- if (isVoidElement(tagName)) {
2113
- if (options.enableCache && this.cache && RendererUtils.isCacheable(element, options)) {
2114
- const cacheKey = RendererUtils.generateCacheKey(tagName, element);
2115
- if (cacheKey) {
2116
- 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);
2117
2258
  }
2118
2259
  }
2119
- this.recordPerformance(tagName, startTime, false);
2120
- 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
+ );
2121
2274
  }
2122
- if (_rawHtml !== void 0) {
2123
- const resolvedHtml = typeof _rawHtml === "function" ? _rawHtml() : _rawHtml;
2124
- const rawContent = isTrustedContent(resolvedHtml) ? resolvedHtml.__html : String(resolvedHtml);
2125
- const result = `${openingTag}${rawContent}</${tagName}>`;
2126
- return result;
2275
+ options.seenObjects.add(value);
2276
+ try {
2277
+ yield* body(this);
2278
+ } finally {
2279
+ options.seenObjects.delete(value);
2127
2280
  }
2128
- if (isTrustedContent(text)) {
2129
- return `${openingTag}${text.__html}</${tagName}>`;
2130
- }
2131
- let textContent = "";
2132
- if (text !== void 0) {
2133
- const isScript = tagName === "script";
2134
- const isStyle = tagName === "style";
2135
- const isRawTag = isScript || isStyle;
2136
- const raw = typeof text === "function" ? String(text()) : String(text);
2137
- if (isRawTag) {
2138
- const safe = raw.replace(/<\/(script)/gi, "<\\/$1").replace(/<\/(style)/gi, "<\\/$1").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
2139
- textContent = safe;
2140
- } else {
2141
- 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;
2142
2295
  }
2143
2296
  }
2144
- let childrenHtml = "";
2145
- if (hasChildren(element)) {
2146
- const normalizedChildren = normalizeChildren(children);
2147
- childrenHtml = normalizedChildren.map((child, index) => {
2148
- if (child && typeof child === "object" && !Array.isArray(child)) {
2149
- const childTagName = Object.keys(child)[0];
2150
- if (childTagName) {
2151
- validateNesting(tagName, childTagName, formatRenderPath([...path, `children[${index}]`]));
2152
- }
2153
- }
2154
- return this.renderComponent(child, options, depth + 1, [...path, `children[${index}]`]);
2155
- }).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);
2156
2323
  }
2157
- const html = `${openingTag}${textContent}${childrenHtml}</${tagName}>`;
2158
- if (options.enableCache && this.cache && RendererUtils.isCacheable(element, options)) {
2159
- const cacheKey = RendererUtils.generateCacheKey(tagName, element);
2160
- if (cacheKey) {
2161
- this.cache.set(cacheKey, html);
2162
- }
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));
2163
2333
  }
2164
- this.recordPerformance(tagName, startTime, false);
2165
- return html;
2166
2334
  }
2167
- };
2335
+ }
2168
2336
  function render(component, options = {}) {
2169
2337
  const mergedOptions = {
2170
- enableCache: true,
2171
2338
  enableMonitoring: false,
2172
2339
  ...options
2173
2340
  };
2174
2341
  const renderer = new HTMLRenderer(mergedOptions);
2175
2342
  return renderer.render(component, mergedOptions);
2176
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
+ };
2177
2438
 
2178
2439
  // src/components/component-system.js
2179
2440
  var COMPONENT_REGISTRY = /* @__PURE__ */ new Map();
@@ -2220,7 +2481,7 @@ var ComponentState = class {
2220
2481
  try {
2221
2482
  listener(newState, oldState);
2222
2483
  } catch (_error) {
2223
- console.error("State listener _error:", _error);
2484
+ console.error("State listener error:", _error);
2224
2485
  }
2225
2486
  });
2226
2487
  this.isUpdating = false;
@@ -2389,7 +2650,7 @@ var Component = class _Component {
2389
2650
  return this.rendered;
2390
2651
  } catch (_error) {
2391
2652
  this.handleError(_error);
2392
- return { div: { className: "component-_error", text: `Error in ${this.name}` } };
2653
+ return { div: { className: "component-error", text: `Error in ${this.name}` } };
2393
2654
  }
2394
2655
  }
2395
2656
  /**
@@ -2635,7 +2896,7 @@ function lazy(factory, options = {}) {
2635
2896
  if (onError) {
2636
2897
  onError(_error);
2637
2898
  } else {
2638
- console.error("Lazy evaluation _error:", _error);
2899
+ console.error("Lazy evaluation error:", _error);
2639
2900
  }
2640
2901
  return fallback;
2641
2902
  } finally {
@@ -2738,27 +2999,32 @@ function evaluateWithTimeout(factory, timeout, args, fallback) {
2738
2999
  }
2739
3000
  }).catch(() => fallback);
2740
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
+ }
2741
3015
  function memo(fn, options = {}) {
2742
3016
  const {
2743
3017
  // Caching strategy
2744
3018
  strategy = "lru",
2745
3019
  // 'lru', 'ttl', 'weak', 'simple'
2746
3020
  maxSize = 100,
2747
- // Maximum cache entries
3021
+ // Maximum cache entries ('lru' and 'ttl')
2748
3022
  ttl = null,
2749
3023
  // Time to live in milliseconds
2750
3024
  // Key generation
2751
3025
  keyFn = null,
2752
3026
  // Custom key function
2753
- keySerializer = JSON.stringify,
2754
- // Default serialization
2755
- // Comparison
2756
- // eslint-disable-next-line no-unused-vars
2757
- compareFn = null,
2758
- // Custom equality comparison
2759
- // eslint-disable-next-line no-unused-vars
2760
- shallow = false,
2761
- // Shallow comparison for objects
3027
+ keySerializer = serializeMemoKey,
2762
3028
  // Lifecycle hooks
2763
3029
  onHit = null,
2764
3030
  // Called on cache hit
@@ -2773,63 +3039,74 @@ function memo(fn, options = {}) {
2773
3039
  debug = false
2774
3040
  // Debug logging
2775
3041
  } = options;
2776
- let cache;
2777
- const stats_data = stats ? { hits: 0, misses: 0, evictions: 0 } : null;
2778
- switch (strategy) {
2779
- case "lru":
2780
- cache = new LRUCache(maxSize, { onEvict });
2781
- break;
2782
- case "ttl":
2783
- cache = new TTLCache(ttl, { onEvict });
2784
- break;
2785
- case "weak":
2786
- cache = /* @__PURE__ */ new WeakMap();
2787
- break;
2788
- default:
2789
- cache = /* @__PURE__ */ new Map();
2790
- }
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;
2791
3051
  const generateKey = keyFn || ((...args) => {
2792
3052
  if (args.length === 0) return "__empty__";
2793
3053
  if (args.length === 1) return keySerializer(args[0]);
2794
3054
  return keySerializer(args);
2795
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
+ };
2796
3069
  const memoizedFn = (...args) => {
2797
- const key = generateKey(...args);
2798
- if (cache.has(key)) {
2799
- const cached = cache.get(key);
2800
- if (cached && (!cached.expires || Date.now() < cached.expires)) {
2801
- 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)}`);
2802
3076
  if (onHit) onHit(key, cached.value, args);
2803
- if (stats_data) stats_data.hits++;
2804
- return cached.value || cached;
2805
- } else {
2806
- cache.delete(key);
3077
+ statsData.hits++;
3078
+ return cached.value;
2807
3079
  }
3080
+ entries.delete(key);
2808
3081
  }
2809
- if (debug) console.log(`Memo cache miss for key: ${key}`);
3082
+ if (debug) console.log(`Memo cache miss for key: ${String(key)}`);
2810
3083
  if (onMiss) onMiss(key, args);
2811
- if (stats_data) stats_data.misses++;
2812
- const result = fn(...args);
2813
- const cacheEntry = ttl ? { value: result, expires: Date.now() + ttl } : result;
2814
- cache.set(key, cacheEntry);
2815
- 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();
3092
+ };
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);
2816
3097
  };
2817
- memoizedFn.cache = cache;
2818
- memoizedFn.clear = () => cache.clear();
2819
- memoizedFn.delete = (key) => cache.delete(key);
2820
- memoizedFn.has = (key) => cache.has(key);
2821
- memoizedFn.size = () => cache.size;
2822
- if (stats_data) {
2823
- memoizedFn.stats = () => ({ ...stats_data });
3098
+ memoizedFn.size = () => weakStore ? void 0 : store.size;
3099
+ if (stats) {
3100
+ memoizedFn.stats = () => ({ ...statsData });
2824
3101
  memoizedFn.resetStats = () => {
2825
- stats_data.hits = 0;
2826
- stats_data.misses = 0;
2827
- stats_data.evictions = 0;
3102
+ statsData.hits = 0;
3103
+ statsData.misses = 0;
3104
+ statsData.evictions = 0;
2828
3105
  };
2829
3106
  }
2830
3107
  memoizedFn.refresh = (...args) => {
2831
- const key = generateKey(...args);
2832
- cache.delete(key);
3108
+ const { cacheable, key, entries } = lookup(args);
3109
+ if (cacheable) entries.delete(key);
2833
3110
  return memoizedFn(...args);
2834
3111
  };
2835
3112
  return memoizedFn;
@@ -2893,66 +3170,6 @@ var LRUCache = class {
2893
3170
  return this.cache.size;
2894
3171
  }
2895
3172
  };
2896
- var TTLCache = class {
2897
- constructor(ttl, options = {}) {
2898
- this.ttl = ttl;
2899
- this.cache = /* @__PURE__ */ new Map();
2900
- this.timers = /* @__PURE__ */ new Map();
2901
- this.onEvict = options.onEvict;
2902
- }
2903
- get(key) {
2904
- if (this.cache.has(key)) {
2905
- const entry = this.cache.get(key);
2906
- if (Date.now() < entry.expires) {
2907
- return entry.value;
2908
- } else {
2909
- this.delete(key);
2910
- }
2911
- }
2912
- return void 0;
2913
- }
2914
- set(key, value) {
2915
- if (this.timers.has(key)) {
2916
- clearTimeout(this.timers.get(key));
2917
- }
2918
- const expires = Date.now() + this.ttl;
2919
- this.cache.set(key, { value, expires });
2920
- const timer = setTimeout(() => {
2921
- this.delete(key);
2922
- }, this.ttl);
2923
- this.timers.set(key, timer);
2924
- }
2925
- has(key) {
2926
- if (this.cache.has(key)) {
2927
- const entry = this.cache.get(key);
2928
- return Date.now() < entry.expires;
2929
- }
2930
- return false;
2931
- }
2932
- delete(key) {
2933
- const had = this.cache.has(key);
2934
- if (had) {
2935
- const entry = this.cache.get(key);
2936
- this.cache.delete(key);
2937
- if (this.timers.has(key)) {
2938
- clearTimeout(this.timers.get(key));
2939
- this.timers.delete(key);
2940
- }
2941
- if (this.onEvict) {
2942
- this.onEvict(key, entry.value);
2943
- }
2944
- }
2945
- return had;
2946
- }
2947
- clear() {
2948
- this.timers.forEach((timer) => clearTimeout(timer));
2949
- this.timers.clear();
2950
- this.cache.clear();
2951
- }
2952
- get size() {
2953
- return this.cache.size;
2954
- }
2955
- };
2956
3173
  function shallowEqual(a, b) {
2957
3174
  if (a === b) return true;
2958
3175
  if (!a || !b) return false;
@@ -3146,7 +3363,9 @@ function createStateContainer(initialState, options) {
3146
3363
  storageKey,
3147
3364
  storage,
3148
3365
  reducer,
3149
- middleware,
3366
+ // Defaulted here: withStateUtils.shared() passes {}, and spreading
3367
+ // an undefined middleware list threw "middleware is not iterable".
3368
+ middleware = [],
3150
3369
  validator,
3151
3370
  onStateChange,
3152
3371
  debug
@@ -3201,7 +3420,7 @@ function createStateContainer(initialState, options) {
3201
3420
  try {
3202
3421
  listener(state, prevState);
3203
3422
  } catch (_error) {
3204
- if (debug) console.error("State listener _error:", _error);
3423
+ if (debug) console.error("State listener error:", _error);
3205
3424
  }
3206
3425
  });
3207
3426
  if (onStateChange) {
@@ -3345,8 +3564,10 @@ var withStateUtils = {
3345
3564
  }
3346
3565
  }),
3347
3566
  /**
3348
- * State with loading/_error handling
3567
+ * State with loading/error handling
3349
3568
  */
3569
+ // `_loading` / `_error` are the documented state keys (docs/components/state.md).
3570
+ /* eslint-disable no-restricted-syntax */
3350
3571
  withLoading: async (initialState) => withState({
3351
3572
  ...initialState,
3352
3573
  _loading: false,
@@ -3376,6 +3597,7 @@ var withStateUtils = {
3376
3597
  }
3377
3598
  }
3378
3599
  }),
3600
+ /* eslint-enable no-restricted-syntax */
3379
3601
  /**
3380
3602
  * State with undo/redo functionality
3381
3603
  */
@@ -3476,7 +3698,7 @@ var LIFECYCLE_PHASES = {
3476
3698
  UPDATED: "updated",
3477
3699
  BEFORE_UNMOUNT: "beforeUnmount",
3478
3700
  UNMOUNTED: "unmounted",
3479
- ERROR: "_error"
3701
+ ERROR: "error"
3480
3702
  };
3481
3703
  var componentInstances = /* @__PURE__ */ new WeakMap();
3482
3704
  var componentRegistry = /* @__PURE__ */ new Map();
@@ -3796,7 +4018,7 @@ var ComponentEventSystem = class {
3796
4018
  handler(event);
3797
4019
  } catch (_error) {
3798
4020
  globalErrorHandler.handle(_error, {
3799
- type: "event-handler-_error",
4021
+ type: "event-handler-error",
3800
4022
  context: { event, handler: handler.toString() }
3801
4023
  });
3802
4024
  }
@@ -3815,7 +4037,7 @@ var ComponentEventSystem = class {
3815
4037
  handler(event);
3816
4038
  } catch (_error) {
3817
4039
  globalErrorHandler.handle(_error, {
3818
- type: "global-event-handler-_error",
4040
+ type: "global-event-handler-error",
3819
4041
  context: { event, handler: handler.toString() }
3820
4042
  });
3821
4043
  }
@@ -4138,6 +4360,7 @@ var ComponentCache = class {
4138
4360
  this.cleanupTimer = setInterval(() => {
4139
4361
  this.cleanup();
4140
4362
  }, this.options.cleanupInterval);
4363
+ this.cleanupTimer.unref?.();
4141
4364
  }
4142
4365
  }
4143
4366
  /**
@@ -4427,6 +4650,37 @@ function memoize(component, keyGenerator, options = {}) {
4427
4650
  }
4428
4651
 
4429
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
+ }
4430
4684
  var ErrorBoundaryState = class {
4431
4685
  constructor() {
4432
4686
  this.hasError = false;
@@ -4458,19 +4712,23 @@ function createErrorBoundary(options = {}) {
4458
4712
  maxErrors = Infinity,
4459
4713
  resetTimeout = null
4460
4714
  } = options;
4461
- const state = new ErrorBoundaryState();
4715
+ const sharedState = new ErrorBoundaryState();
4462
4716
  let previousProps = {};
4463
4717
  let resetTimer = null;
4464
4718
  return function errorBoundaryWrapper(component) {
4465
4719
  return function wrappedComponent(props = {}) {
4720
+ const persistent = isBrowser();
4721
+ const state = persistent ? sharedState : new ErrorBoundaryState();
4466
4722
  try {
4467
- if (resetOnPropsChange && shouldReset(props, previousProps, resetKeys)) {
4723
+ if (persistent && resetOnPropsChange && shouldReset(props, previousProps, resetKeys)) {
4468
4724
  state.reset();
4469
4725
  if (onReset) {
4470
4726
  onReset();
4471
4727
  }
4472
4728
  }
4473
- previousProps = { ...props };
4729
+ if (persistent) {
4730
+ previousProps = { ...props };
4731
+ }
4474
4732
  if (state.hasError) {
4475
4733
  if (state.errorCount >= maxErrors) {
4476
4734
  return typeof fallback === "function" ? fallback(state.error, state.errorInfo, { permanent: true }) : fallback;
@@ -4487,7 +4745,7 @@ function createErrorBoundary(options = {}) {
4487
4745
  return fallbackComponent;
4488
4746
  }
4489
4747
  const result = typeof component === "function" ? component(props) : component;
4490
- return result;
4748
+ return resolveNestedComponents(result);
4491
4749
  } catch (error) {
4492
4750
  const errorInfo = {
4493
4751
  componentStack: error.stack,
@@ -4502,7 +4760,7 @@ function createErrorBoundary(options = {}) {
4502
4760
  console.error("Error in onError callback:", callbackError);
4503
4761
  }
4504
4762
  }
4505
- if (resetTimeout && !resetTimer) {
4763
+ if (persistent && resetTimeout && !resetTimer) {
4506
4764
  resetTimer = setTimeout(() => {
4507
4765
  state.reset();
4508
4766
  resetTimer = null;
@@ -4618,9 +4876,10 @@ function createAsyncErrorBoundary(options = {}) {
4618
4876
  } = options;
4619
4877
  return function asyncBoundaryWrapper(asyncComponent) {
4620
4878
  return async function wrappedAsyncComponent(props = {}) {
4879
+ let timer;
4621
4880
  try {
4622
4881
  const timeoutPromise = new Promise((_, reject) => {
4623
- setTimeout(() => reject(new Error("Component load timeout")), timeout);
4882
+ timer = setTimeout(() => reject(new Error("Component load timeout")), timeout);
4624
4883
  });
4625
4884
  const result = await Promise.race([
4626
4885
  typeof asyncComponent === "function" ? asyncComponent(props) : asyncComponent,
@@ -4632,6 +4891,8 @@ function createAsyncErrorBoundary(options = {}) {
4632
4891
  onError(error, { props, async: true });
4633
4892
  }
4634
4893
  return typeof errorFallback === "function" ? errorFallback(error, { props }) : errorFallback;
4894
+ } finally {
4895
+ clearTimeout(timer);
4635
4896
  }
4636
4897
  };
4637
4898
  };
@@ -4726,7 +4987,7 @@ function renderWithTemplate(component, options = {}) {
4726
4987
  template = "<!DOCTYPE html>\n{{content}}"
4727
4988
  } = options;
4728
4989
  const html = renderWithMonitoring(component, options);
4729
- return template.replace("{{content}}", html);
4990
+ return template.replace("{{content}}", () => html);
4730
4991
  }
4731
4992
  async function renderComponentFactory(componentFactory, factoryArgs, options = {}) {
4732
4993
  const component = await Promise.resolve(
@@ -4914,7 +5175,8 @@ function renderComponentContent(obj) {
4914
5175
  if (value === true) return attrName;
4915
5176
  return `${attrName}="${escapeHTML(String(value))}"`;
4916
5177
  }).join(" ");
4917
- const openTag = attrsStr ? `<${tagName} ${attrsStr}>` : `<${tagName}>`;
5178
+ const attrsPart = attrsStr ? ` ${attrsStr}` : "";
5179
+ const openTag = `<${tagName}${attrsPart}>`;
4918
5180
  if ([
4919
5181
  "area",
4920
5182
  "base",
@@ -4931,7 +5193,7 @@ function renderComponentContent(obj) {
4931
5193
  "track",
4932
5194
  "wbr"
4933
5195
  ].includes(tagName)) {
4934
- return openTag.replace(">", " />");
5196
+ return `<${tagName}${attrsPart} />`;
4935
5197
  }
4936
5198
  let content = "";
4937
5199
  if (text !== void 0) {
@@ -5263,6 +5525,29 @@ var EventBus = class {
5263
5525
  }
5264
5526
  return false;
5265
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
+ }
5266
5551
  /**
5267
5552
  * Remove all listeners for an event
5268
5553
  */
@@ -5876,7 +6161,8 @@ function withEventBus(options = {}) {
5876
6161
  function EventBusComponent(props = {}, state = {}, context = {}) {
5877
6162
  const bus = scope ? eventBus.createScope(scope) : eventBus;
5878
6163
  const listenerIds = /* @__PURE__ */ new Map();
5879
- Object.entries(events).forEach(([event, handler]) => {
6164
+ const interactive = typeof window !== "undefined" && typeof document !== "undefined";
6165
+ if (interactive) Object.entries(events).forEach(([event, handler]) => {
5880
6166
  const listenerId = bus.on(event, (data, eventName) => {
5881
6167
  if (typeof handler === "function") {
5882
6168
  handler.call(this, data, eventName, { props, state, context });
@@ -5884,7 +6170,7 @@ function withEventBus(options = {}) {
5884
6170
  });
5885
6171
  listenerIds.set(event, listenerId);
5886
6172
  });
5887
- Object.entries(actions).forEach(([action, handler]) => {
6173
+ if (interactive) Object.entries(actions).forEach(([action, handler]) => {
5888
6174
  bus.registerAction(action, (actionContext) => {
5889
6175
  if (typeof handler === "function") {
5890
6176
  handler.call(this, actionContext, { props, state, context });
@@ -5928,7 +6214,10 @@ function withEventBus(options = {}) {
5928
6214
  originalUnmount.call(this);
5929
6215
  };
5930
6216
  } else {
5931
- result.__eventBusCleanup = eventUtils.cleanup;
6217
+ Object.defineProperty(result, "__eventBusCleanup", {
6218
+ value: eventUtils.cleanup,
6219
+ configurable: true
6220
+ });
5932
6221
  }
5933
6222
  }
5934
6223
  return result;
@@ -6256,6 +6545,7 @@ var eventSystem2 = {
6256
6545
  registerAction: globalEventBus.registerAction.bind(globalEventBus),
6257
6546
  registerActions: globalEventBus.registerActions.bind(globalEventBus),
6258
6547
  handleAction: globalEventBus.handleAction.bind(globalEventBus),
6548
+ createScope: globalEventBus.createScope.bind(globalEventBus),
6259
6549
  // Statistics and debugging
6260
6550
  getStats: globalEventBus.getStats.bind(globalEventBus),
6261
6551
  resetStats: globalEventBus.resetStats.bind(globalEventBus),
@@ -6359,23 +6649,74 @@ var compose = {
6359
6649
  };
6360
6650
 
6361
6651
  // src/index.js
6362
- var scopeCounter = { value: 0 };
6363
- function generateScopeId() {
6364
- 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}]`;
6365
6684
  }
6366
6685
  function scopeCSS(css, scopeId) {
6367
6686
  if (!css || typeof css !== "string") return css;
6368
- return css.replace(/([^{}]*)\s*{/g, (match, selector) => {
6369
- const selectors = selector.split(",").map((s) => {
6370
- const trimmed = s.trim();
6371
- if (!trimmed) return s;
6372
- if (trimmed.includes(":")) {
6373
- return trimmed.replace(/([^:]+)(:.*)?/, `$1[${scopeId}]$2`);
6374
- }
6375
- return `${trimmed}[${scopeId}]`;
6376
- });
6377
- return `${selectors.join(", ")} {`;
6378
- });
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;
6379
6720
  }
6380
6721
  function applyScopeToElement(element, scopeId) {
6381
6722
  if (typeof element === "string" || typeof element === "number" || !element) {
@@ -6403,10 +6744,7 @@ function applyScopeToElement(element, scopeId) {
6403
6744
  return element;
6404
6745
  }
6405
6746
  function dangerouslySetInnerContent(content) {
6406
- return {
6407
- __html: content,
6408
- __trusted: true
6409
- };
6747
+ return createTrustedContent(content);
6410
6748
  }
6411
6749
  function injectHydrationAttributes(component, options) {
6412
6750
  if (!component || typeof component !== "object" || Array.isArray(component)) {
@@ -6438,20 +6776,42 @@ function Island(componentFn) {
6438
6776
  });
6439
6777
  };
6440
6778
  }
6441
- function render2(obj, options = {}) {
6779
+ function prepareRender(obj, options) {
6442
6780
  const scoped = options.scoped ?? options.encapsulate ?? false;
6443
6781
  const { scoped: _scoped, encapsulate: _encapsulate, hydratable: _hydratable, island: _island, ...rendererOptions } = options;
6444
- let component = scoped ? renderScopedComponent(obj) : obj;
6445
- if (typeof component === "function") {
6446
- component = component(options);
6782
+ let component = typeof obj === "function" ? obj(options) : obj;
6783
+ if (scoped) {
6784
+ component = renderScopedComponent(component);
6447
6785
  }
6448
6786
  if (_hydratable || _island) {
6449
6787
  component = injectHydrationAttributes(component, { hydratable: _hydratable, island: _island });
6450
6788
  }
6789
+ return { component, rendererOptions };
6790
+ }
6791
+ function render2(obj, options = {}) {
6792
+ const { component, rendererOptions } = prepareRender(obj, options);
6451
6793
  return render(component, rendererOptions);
6452
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
+ }
6453
6813
  function renderScopedComponent(component) {
6454
- const scopeId = generateScopeId();
6814
+ const scopeId = generateScopeId(collectStyleText(component).join("\n"));
6455
6815
  function processScopedElement(element) {
6456
6816
  if (!element || typeof element !== "object") {
6457
6817
  return element;
@@ -6482,21 +6842,13 @@ function renderScopedComponent(component) {
6482
6842
  const scopedComponent = applyScopeToElement(processedComponent, scopeId);
6483
6843
  return scopedComponent;
6484
6844
  }
6485
- var memoCache = /* @__PURE__ */ new Map();
6486
- function memo2(component, keyGenerator) {
6487
- return function MemoizedComponent(props = {}) {
6488
- const key = keyGenerator ? keyGenerator(props) : JSON.stringify(props);
6489
- if (memoCache.has(key)) {
6490
- return memoCache.get(key);
6491
- }
6492
- const result = component(props);
6493
- memoCache.set(key, result);
6494
- if (memoCache.size > 100) {
6495
- const firstKey = memoCache.keys().next().value;
6496
- memoCache.delete(firstKey);
6497
- }
6498
- return result;
6499
- };
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);
6500
6852
  }
6501
6853
  function validateComponent2(obj) {
6502
6854
  if (!obj || typeof obj !== "object") {
@@ -6517,7 +6869,7 @@ function deepClone2(obj) {
6517
6869
  }
6518
6870
  return cloned;
6519
6871
  }
6520
- var VERSION = true ? "1.1.0" : 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;
6521
6873
  var fp = {
6522
6874
  /**
6523
6875
  * Curried map: fp.map(fn)(array)
@@ -6529,6 +6881,7 @@ var fp = {
6529
6881
  var coherent = {
6530
6882
  // Core rendering
6531
6883
  render: render2,
6884
+ renderToStream: renderToStream2,
6532
6885
  // Shadow DOM (client-side only)
6533
6886
  shadowDOM: shadow_dom_exports,
6534
6887
  // Component system
@@ -6646,6 +6999,7 @@ export {
6646
6999
  isLazy,
6647
7000
  isPeerDependencyAvailable,
6648
7001
  isTrustedContent,
7002
+ isValidAttributeName,
6649
7003
  isVoidElement,
6650
7004
  lazy,
6651
7005
  componentUtils as lifecycleUtils,
@@ -6661,9 +7015,11 @@ export {
6661
7015
  registerComponent,
6662
7016
  render2 as render,
6663
7017
  renderComponentFactory,
7018
+ renderToStream2 as renderToStream,
6664
7019
  renderWithMonitoring,
6665
7020
  renderWithTemplate,
6666
7021
  shadow_dom_exports as shadowDOM,
7022
+ streamingUtils,
6667
7023
  useHooks,
6668
7024
  validateComponent2 as validateComponent,
6669
7025
  validateNesting,