tova 0.2.9 → 0.3.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/bin/tova.js CHANGED
@@ -79,10 +79,10 @@ async function main() {
79
79
  await runFile(args.filter(a => a !== '--strict')[1], { strict: isStrict });
80
80
  break;
81
81
  case 'build':
82
- buildProject(args.slice(1));
82
+ await buildProject(args.slice(1));
83
83
  break;
84
84
  case 'dev':
85
- devServer(args.slice(1));
85
+ await devServer(args.slice(1));
86
86
  break;
87
87
  case 'repl':
88
88
  await startRepl();
@@ -1302,7 +1302,7 @@ async function migrateUp(args) {
1302
1302
  await db.exec(sql);
1303
1303
  }
1304
1304
  const ph = db.driver === 'postgres' ? '$1' : '?';
1305
- await db.exec(`INSERT INTO __migrations (name) VALUES ('${file.replace(/'/g, "''")}')`);
1305
+ await db.query(`INSERT INTO __migrations (name) VALUES (${ph})`, file);
1306
1306
  console.log(` ✓ ${file}`);
1307
1307
  }
1308
1308
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tova",
3
- "version": "0.2.9",
3
+ "version": "0.3.0",
4
4
  "description": "Tova — a modern programming language that transpiles to JavaScript, unifying frontend and backend",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -298,7 +298,9 @@ export class Analyzer {
298
298
  if (['+', '-', '*', '/', '%', '**'].includes(expr.operator)) {
299
299
  const lt = this._inferType(expr.left);
300
300
  const rt = this._inferType(expr.right);
301
+ if (!lt && !rt) return null;
301
302
  if (lt === 'Float' || rt === 'Float') return 'Float';
303
+ if (lt === 'String' || rt === 'String') return 'String';
302
304
  return 'Int';
303
305
  }
304
306
  if (['==', '!=', '<', '>', '<=', '>='].includes(expr.operator)) return 'Bool';
@@ -567,8 +569,16 @@ export class Analyzer {
567
569
  return;
568
570
  case 'ObjectLiteral':
569
571
  for (const prop of node.properties) {
570
- this.visitExpression(prop.key);
571
- this.visitExpression(prop.value);
572
+ if (prop.spread) {
573
+ // Spread property: {...expr}
574
+ this.visitExpression(prop.argument);
575
+ } else if (prop.shorthand) {
576
+ // Shorthand: {name} — key IS the variable reference
577
+ this.visitExpression(prop.key);
578
+ } else {
579
+ // Non-shorthand: {key: value} — only visit value, key is a label
580
+ this.visitExpression(prop.value);
581
+ }
572
582
  }
573
583
  return;
574
584
  case 'ListComprehension':
@@ -712,10 +722,10 @@ export class Analyzer {
712
722
  } finally {
713
723
  this.currentScope = prevScope;
714
724
  }
715
- // Promote shared symbols (types, functions) to parent scope
716
- // so server/client blocks can reference them
725
+ // Promote shared types and functions to parent scope
726
+ // so server/client blocks can reference them (but not variables)
717
727
  for (const [name, sym] of sharedScope.symbols) {
718
- if (!prevScope.symbols.has(name)) {
728
+ if (!prevScope.symbols.has(name) && (sym.kind === 'type' || sym.kind === 'function')) {
719
729
  prevScope.symbols.set(name, sym);
720
730
  }
721
731
  }
@@ -832,7 +842,12 @@ export class Analyzer {
832
842
  // Push expected return type for return-statement checking
833
843
  const expectedReturn = node.returnType ? this._typeAnnotationToString(node.returnType) : null;
834
844
  this._functionReturnTypeStack.push(expectedReturn);
835
- if (node.isAsync) this._asyncDepth++;
845
+ const prevAsyncDepth = this._asyncDepth;
846
+ if (node.isAsync) {
847
+ this._asyncDepth++;
848
+ } else {
849
+ this._asyncDepth = 0; // Non-async function resets async context
850
+ }
836
851
 
837
852
  try {
838
853
  for (const param of node.params) {
@@ -861,7 +876,7 @@ export class Analyzer {
861
876
  }
862
877
  }
863
878
  } finally {
864
- if (node.isAsync) this._asyncDepth--;
879
+ this._asyncDepth = prevAsyncDepth;
865
880
  this._functionReturnTypeStack.pop();
866
881
  this.currentScope = prevScope;
867
882
  }
@@ -1211,10 +1226,13 @@ export class Analyzer {
1211
1226
  this.error(e.message);
1212
1227
  }
1213
1228
  }
1214
- for (const child of node.body) {
1215
- this.visitNode(child);
1229
+ try {
1230
+ for (const child of node.body) {
1231
+ this.visitNode(child);
1232
+ }
1233
+ } finally {
1234
+ this.currentScope = prevScope;
1216
1235
  }
1217
- this.currentScope = prevScope;
1218
1236
  }
1219
1237
 
1220
1238
  visitStoreDeclaration(node) {
@@ -1231,10 +1249,13 @@ export class Analyzer {
1231
1249
 
1232
1250
  const prevScope = this.currentScope;
1233
1251
  this.currentScope = this.currentScope.child('block');
1234
- for (const child of node.body) {
1235
- this.visitNode(child);
1252
+ try {
1253
+ for (const child of node.body) {
1254
+ this.visitNode(child);
1255
+ }
1256
+ } finally {
1257
+ this.currentScope = prevScope;
1236
1258
  }
1237
- this.currentScope = prevScope;
1238
1259
  }
1239
1260
 
1240
1261
  visitRouteDeclaration(node) {
@@ -1288,8 +1309,11 @@ export class Analyzer {
1288
1309
  this.error(e.message);
1289
1310
  }
1290
1311
  }
1291
- this.visitNode(node.body);
1292
- this.currentScope = prevScope;
1312
+ try {
1313
+ this.visitNode(node.body);
1314
+ } finally {
1315
+ this.currentScope = prevScope;
1316
+ }
1293
1317
  }
1294
1318
 
1295
1319
  visitHealthCheckDeclaration(node) {
@@ -1324,8 +1348,11 @@ export class Analyzer {
1324
1348
  this.error(e.message);
1325
1349
  }
1326
1350
  }
1327
- this.visitNode(node.body);
1328
- this.currentScope = prevScope;
1351
+ try {
1352
+ this.visitNode(node.body);
1353
+ } finally {
1354
+ this.currentScope = prevScope;
1355
+ }
1329
1356
  }
1330
1357
 
1331
1358
  visitWebSocketDeclaration(node) {
@@ -1345,8 +1372,11 @@ export class Analyzer {
1345
1372
  this.error(e.message);
1346
1373
  }
1347
1374
  }
1348
- this.visitNode(handler.body);
1349
- this.currentScope = prevScope;
1375
+ try {
1376
+ this.visitNode(handler.body);
1377
+ } finally {
1378
+ this.currentScope = prevScope;
1379
+ }
1350
1380
  }
1351
1381
  }
1352
1382
 
@@ -1418,8 +1448,11 @@ export class Analyzer {
1418
1448
  this.error(e.message);
1419
1449
  }
1420
1450
  }
1421
- this.visitNode(node.body);
1422
- this.currentScope = prevScope;
1451
+ try {
1452
+ this.visitNode(node.body);
1453
+ } finally {
1454
+ this.currentScope = prevScope;
1455
+ }
1423
1456
  }
1424
1457
 
1425
1458
  visitSubscribeDeclaration(node) {
@@ -1437,8 +1470,11 @@ export class Analyzer {
1437
1470
  this.error(e.message);
1438
1471
  }
1439
1472
  }
1440
- this.visitNode(node.body);
1441
- this.currentScope = prevScope;
1473
+ try {
1474
+ this.visitNode(node.body);
1475
+ } finally {
1476
+ this.currentScope = prevScope;
1477
+ }
1442
1478
  }
1443
1479
 
1444
1480
  visitEnvDeclaration(node) {
@@ -1480,8 +1516,11 @@ export class Analyzer {
1480
1516
  this.error(e.message);
1481
1517
  }
1482
1518
  }
1483
- this.visitNode(node.body);
1484
- this.currentScope = prevScope;
1519
+ try {
1520
+ this.visitNode(node.body);
1521
+ } finally {
1522
+ this.currentScope = prevScope;
1523
+ }
1485
1524
  }
1486
1525
 
1487
1526
  visitUploadDeclaration(node) {
@@ -1555,8 +1594,11 @@ export class Analyzer {
1555
1594
  this.error(e.message);
1556
1595
  }
1557
1596
  }
1558
- this.visitNode(node.body);
1559
- this.currentScope = prevScope;
1597
+ try {
1598
+ this.visitNode(node.body);
1599
+ } finally {
1600
+ this.currentScope = prevScope;
1601
+ }
1560
1602
  }
1561
1603
 
1562
1604
  visitCacheDeclaration(node) {
@@ -1579,10 +1621,13 @@ export class Analyzer {
1579
1621
  for (const p of node.params) {
1580
1622
  this.currentScope.define(p.name, { kind: 'param' });
1581
1623
  }
1582
- for (const stmt of node.body.body || []) {
1583
- this.visitNode(stmt);
1624
+ try {
1625
+ for (const stmt of node.body.body || []) {
1626
+ this.visitNode(stmt);
1627
+ }
1628
+ } finally {
1629
+ this.currentScope = prevScope;
1584
1630
  }
1585
- this.currentScope = prevScope;
1586
1631
  }
1587
1632
 
1588
1633
  visitModelDeclaration(node) {
@@ -1600,10 +1645,13 @@ export class Analyzer {
1600
1645
  visitTestBlock(node) {
1601
1646
  const prevScope = this.currentScope;
1602
1647
  this.currentScope = this.currentScope.child('block');
1603
- for (const stmt of node.body) {
1604
- this.visitNode(stmt);
1648
+ try {
1649
+ for (const stmt of node.body) {
1650
+ this.visitNode(stmt);
1651
+ }
1652
+ } finally {
1653
+ this.currentScope = prevScope;
1605
1654
  }
1606
- this.currentScope = prevScope;
1607
1655
  }
1608
1656
 
1609
1657
  // ─── Expression visitors ──────────────────────────────────
@@ -1639,7 +1687,12 @@ export class Analyzer {
1639
1687
 
1640
1688
  const expectedReturn = node.returnType ? this._typeAnnotationToString(node.returnType) : null;
1641
1689
  this._functionReturnTypeStack.push(expectedReturn);
1642
- if (node.isAsync) this._asyncDepth++;
1690
+ const prevAsyncDepth = this._asyncDepth;
1691
+ if (node.isAsync) {
1692
+ this._asyncDepth++;
1693
+ } else {
1694
+ this._asyncDepth = 0; // Non-async lambda resets async context
1695
+ }
1643
1696
 
1644
1697
  try {
1645
1698
  for (const param of node.params) {
@@ -1662,7 +1715,7 @@ export class Analyzer {
1662
1715
  this.visitExpression(node.body);
1663
1716
  }
1664
1717
  } finally {
1665
- if (node.isAsync) this._asyncDepth--;
1718
+ this._asyncDepth = prevAsyncDepth;
1666
1719
  this._functionReturnTypeStack.pop();
1667
1720
  this.currentScope = prevScope;
1668
1721
  }
@@ -1764,31 +1817,35 @@ export class Analyzer {
1764
1817
  }
1765
1818
  }
1766
1819
 
1767
- // Check user-defined types — look up in _variantFields from the global scope
1768
- // Collect all known type variants by iterating type declarations
1769
- for (const topNode of this.ast.body) {
1770
- this._collectTypeVariants(topNode, variantNames, coveredVariants, node.loc);
1771
- }
1772
- }
1773
- }
1774
-
1775
- _collectTypeVariants(node, allVariants, coveredVariants, matchLoc) {
1776
- if (node.type === 'TypeDeclaration') {
1777
- const typeVariants = node.variants.filter(v => v.type === 'TypeVariant').map(v => v.name);
1778
- // If any of the match arms reference a variant from this type, check all
1779
- const relevantVariants = typeVariants.filter(v => coveredVariants.has(v));
1780
- if (relevantVariants.length > 0) {
1820
+ // Check user-defined types — find the single best-matching type whose variants
1821
+ // contain ALL covered variant names (avoids false positives with shared names)
1822
+ const candidates = [];
1823
+ this._collectTypeCandidates(this.ast.body, coveredVariants, candidates);
1824
+ // Only warn if exactly one type contains all covered variants
1825
+ if (candidates.length === 1) {
1826
+ const [typeName, typeVariants] = candidates[0];
1781
1827
  for (const v of typeVariants) {
1782
1828
  if (!coveredVariants.has(v)) {
1783
- this.warn(`Non-exhaustive match: missing '${v}' variant from type '${node.name}'`, matchLoc);
1829
+ this.warn(`Non-exhaustive match: missing '${v}' variant from type '${typeName}'`, node.loc);
1784
1830
  }
1785
1831
  }
1786
1832
  }
1787
1833
  }
1788
- // Recurse into blocks
1789
- if (node.type === 'SharedBlock' || node.type === 'ServerBlock' || node.type === 'ClientBlock') {
1790
- for (const child of node.body) {
1791
- this._collectTypeVariants(child, allVariants, coveredVariants, matchLoc);
1834
+ }
1835
+
1836
+ _collectTypeCandidates(nodes, coveredVariants, candidates) {
1837
+ for (const node of nodes) {
1838
+ if (node.type === 'TypeDeclaration') {
1839
+ const typeVariants = node.variants.filter(v => v.type === 'TypeVariant').map(v => v.name);
1840
+ if (typeVariants.length === 0) continue;
1841
+ // All covered variants must be contained in this type's variants
1842
+ const allCovered = [...coveredVariants].every(v => typeVariants.includes(v));
1843
+ if (allCovered) {
1844
+ candidates.push([node.name, typeVariants]);
1845
+ }
1846
+ }
1847
+ if (node.type === 'SharedBlock' || node.type === 'ServerBlock' || node.type === 'ClientBlock') {
1848
+ this._collectTypeCandidates(node.body, coveredVariants, candidates);
1792
1849
  }
1793
1850
  }
1794
1851
  }
@@ -1963,7 +2020,8 @@ export class Analyzer {
1963
2020
  return true;
1964
2021
  case 'BlockStatement':
1965
2022
  if (node.body.length === 0) return false;
1966
- return this._definitelyReturns(node.body[node.body.length - 1]);
2023
+ // Any statement that definitely returns makes the block definitely return
2024
+ return node.body.some(stmt => this._definitelyReturns(stmt));
1967
2025
  case 'IfStatement':
1968
2026
  if (!node.elseBody) return false;
1969
2027
  const consequentReturns = this._definitelyReturns(node.consequent);
@@ -1971,8 +2029,9 @@ export class Analyzer {
1971
2029
  const allAlternatesReturn = (node.alternates || []).every(alt => this._definitelyReturns(alt.body));
1972
2030
  return consequentReturns && elseReturns && allAlternatesReturn;
1973
2031
  case 'GuardStatement':
1974
- // Guard's else block always runs if condition fails if it returns, the guard is a definite return path
1975
- return this._definitelyReturns(node.elseBody);
2032
+ // Guard only handles the failure casewhen condition is true, execution falls through
2033
+ // A guard alone never guarantees return on ALL paths
2034
+ return false;
1976
2035
  case 'MatchExpression': {
1977
2036
  const hasWildcard = node.arms.some(arm =>
1978
2037
  arm.pattern.type === 'WildcardPattern' ||
@@ -1983,15 +2042,15 @@ export class Analyzer {
1983
2042
  }
1984
2043
  case 'TryCatchStatement': {
1985
2044
  const tryReturns = node.tryBody.length > 0 &&
1986
- this._definitelyReturns(node.tryBody[node.tryBody.length - 1]);
2045
+ node.tryBody.some(s => this._definitelyReturns(s));
1987
2046
  const catchReturns = !node.catchBody || (node.catchBody.length > 0 &&
1988
- this._definitelyReturns(node.catchBody[node.catchBody.length - 1]));
2047
+ node.catchBody.some(s => this._definitelyReturns(s)));
1989
2048
  return tryReturns && catchReturns;
1990
2049
  }
1991
2050
  case 'ExpressionStatement':
1992
2051
  return this._definitelyReturns(node.expression);
1993
2052
  case 'CallExpression':
1994
- return true;
2053
+ return false;
1995
2054
  default:
1996
2055
  return false;
1997
2056
  }
@@ -2059,6 +2118,12 @@ export class Analyzer {
2059
2118
  this.strictError(`Type mismatch: '++' expects String on right side, but got ${rightType}`, node.loc);
2060
2119
  }
2061
2120
  } else if (['-', '*', '/', '%', '**'].includes(op)) {
2121
+ // String literal * Int is valid (string repeat) — skip warning for that case
2122
+ if (op === '*') {
2123
+ const leftIsStr = node.left.type === 'StringLiteral' || node.left.type === 'TemplateLiteral';
2124
+ const rightIsStr = node.right.type === 'StringLiteral' || node.right.type === 'TemplateLiteral';
2125
+ if (leftIsStr || rightIsStr) return;
2126
+ }
2062
2127
  // Arithmetic: both sides must be numeric
2063
2128
  const numerics = new Set(['Int', 'Float']);
2064
2129
  if (leftType && !numerics.has(leftType) && leftType !== 'Any') {
@@ -679,11 +679,15 @@ export class BaseCodegen {
679
679
  const right = this.genExpression(node.right);
680
680
  const op = node.operator;
681
681
 
682
- // String multiply: "ha" * 3 => "ha".repeat(3)
682
+ // String multiply: "ha" * 3 => "ha".repeat(3), also x * 3 when x is string
683
683
  if (op === '*' &&
684
684
  (node.left.type === 'StringLiteral' || node.left.type === 'TemplateLiteral')) {
685
685
  return `${left}.repeat(${right})`;
686
686
  }
687
+ if (op === '*' &&
688
+ (node.right.type === 'StringLiteral' || node.right.type === 'TemplateLiteral')) {
689
+ return `${right}.repeat(${left})`;
690
+ }
687
691
 
688
692
  // Tova ?? is NaN-safe: catches null, undefined, AND NaN
689
693
  if (op === '??') {
@@ -1027,7 +1031,7 @@ export class BaseCodegen {
1027
1031
  if (node.type === 'TemplateLiteral') {
1028
1032
  // Template literal with column references
1029
1033
  const parts = node.parts.map(p => {
1030
- if (p.type === 'text') return p.value;
1034
+ if (p.type === 'text') return p.value.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
1031
1035
  return `\${${this._genColumnBody(p.value)}}`;
1032
1036
  });
1033
1037
  return '`' + parts.join('') + '`';
@@ -27,6 +27,7 @@ export class ClientCodegen extends BaseCodegen {
27
27
  }
28
28
  if (node.type === 'IfExpression') {
29
29
  return this._containsRPC(node.condition) || this._containsRPC(node.consequent) ||
30
+ (node.alternates && node.alternates.some(a => this._containsRPC(a.condition) || this._containsRPC(a.body))) ||
30
31
  this._containsRPC(node.elseBody);
31
32
  }
32
33
  if (node.type === 'ForStatement') return this._containsRPC(node.iterable) || this._containsRPC(node.body);
@@ -45,14 +46,15 @@ export class ClientCodegen extends BaseCodegen {
45
46
  return this._containsRPC(node.subject) || node.arms.some(a => this._containsRPC(a.body));
46
47
  }
47
48
  if (node.type === 'TryCatchStatement') {
48
- return this._containsRPC(node.tryBlock) || this._containsRPC(node.catchBlock) ||
49
- this._containsRPC(node.finallyBlock);
49
+ return (node.tryBody && node.tryBody.some(s => this._containsRPC(s))) ||
50
+ (node.catchBody && node.catchBody.some(s => this._containsRPC(s))) ||
51
+ (node.finallyBody && node.finallyBody.some(s => this._containsRPC(s)));
50
52
  }
51
53
  if (node.type === 'PipeExpression') {
52
54
  return this._containsRPC(node.left) || this._containsRPC(node.right);
53
55
  }
54
56
  if (node.type === 'GuardStatement') {
55
- return this._containsRPC(node.condition) || this._containsRPC(node.elseBlock);
57
+ return this._containsRPC(node.condition) || this._containsRPC(node.elseBody);
56
58
  }
57
59
  if (node.type === 'LetDestructure') return this._containsRPC(node.value);
58
60
  if (node.type === 'ArrayLiteral') return node.elements.some(e => this._containsRPC(e));
@@ -61,7 +63,7 @@ export class ClientCodegen extends BaseCodegen {
61
63
  if (node.type === 'AwaitExpression') return this._containsRPC(node.argument);
62
64
  if (node.type === 'PropagateExpression') return this._containsRPC(node.expression);
63
65
  if (node.type === 'UnaryExpression') return this._containsRPC(node.operand);
64
- if (node.type === 'TemplateLiteral') return node.parts.some(p => p.type === 'expr' && this._containsRPC(p.expression));
66
+ if (node.type === 'TemplateLiteral') return node.parts.some(p => p.type === 'expr' && this._containsRPC(p.value));
65
67
  if (node.type === 'ChainedComparison') return node.operands.some(o => this._containsRPC(o));
66
68
  if (node.type === 'RangeExpression') return this._containsRPC(node.start) || this._containsRPC(node.end);
67
69
  if (node.type === 'SliceExpression') return this._containsRPC(node.object) || this._containsRPC(node.start) || this._containsRPC(node.end) || this._containsRPC(node.step);
@@ -602,6 +604,7 @@ export class ClientCodegen extends BaseCodegen {
602
604
  if (node.type === 'ObjectLiteral') return node.properties.some(p => this._exprReadsSignal(p.value));
603
605
  if (node.type === 'IfExpression') {
604
606
  return this._exprReadsSignal(node.condition) || this._exprReadsSignal(node.consequent) ||
607
+ (node.alternates && node.alternates.some(a => this._exprReadsSignal(a.condition) || this._exprReadsSignal(a.body))) ||
605
608
  this._exprReadsSignal(node.elseBody);
606
609
  }
607
610
  if (node.type === 'MatchExpression') {
@@ -876,6 +879,16 @@ export class ClientCodegen extends BaseCodegen {
876
879
  return `() => ${result}`;
877
880
  }
878
881
 
882
+ // Override to add await for piped RPC calls
883
+ genPipeExpression(node) {
884
+ const result = super.genPipeExpression(node);
885
+ // If the pipe target is an RPC call and we're in async context, wrap with await
886
+ if (this._asyncContext && this._containsRPC(node.right)) {
887
+ return `await ${result}`;
888
+ }
889
+ return result;
890
+ }
891
+
879
892
  // Override function declaration to make async if it contains server.* calls
880
893
  genFunctionDeclaration(node) {
881
894
  const hasRPC = this._containsRPC(node.body);
@@ -474,6 +474,7 @@ export class ServerCodegen extends BaseCodegen {
474
474
  lines.push(this._getAiRuntime());
475
475
  lines.push('');
476
476
 
477
+ let hasDefaultAi = false;
477
478
  for (const aiConf of aiConfigs) {
478
479
  const configParts = [];
479
480
  for (const [key, valueNode] of Object.entries(aiConf.config)) {
@@ -487,8 +488,13 @@ export class ServerCodegen extends BaseCodegen {
487
488
  } else {
488
489
  // Default provider: ai { ... } → const ai = __createAI({...})
489
490
  lines.push(`const ai = __createAI(${configStr});`);
491
+ hasDefaultAi = true;
490
492
  }
491
493
  }
494
+ // If no default ai config, create a default for one-off calls
495
+ if (!hasDefaultAi) {
496
+ lines.push('const ai = __createAI({});');
497
+ }
492
498
  lines.push('');
493
499
  }
494
500
 
@@ -2672,7 +2678,6 @@ function __createAI(config) {
2672
2678
  classify(text, categories, opts) { return __aiRequest('classify', [text, categories, opts || {}], opts); },
2673
2679
  };
2674
2680
  }
2675
- // Default AI object for one-off calls (no config block required)
2676
- const ai = typeof ai === 'undefined' ? __createAI({}) : ai;`;
2681
+ `;
2677
2682
  }
2678
2683
  }
@@ -86,7 +86,7 @@ export class Lexer {
86
86
  const prev = this.tokens.length > 1 ? this.tokens[this.tokens.length - 2] : null;
87
87
  if (!prev) return true;
88
88
  const valueTypes = [TokenType.IDENTIFIER, TokenType.NUMBER, TokenType.STRING,
89
- TokenType.STRING_TEMPLATE, TokenType.RPAREN, TokenType.RBRACKET,
89
+ TokenType.STRING_TEMPLATE, TokenType.RPAREN, TokenType.RBRACKET, TokenType.RBRACE,
90
90
  TokenType.TRUE, TokenType.FALSE, TokenType.NIL];
91
91
  return !valueTypes.includes(prev.type);
92
92
  }
package/src/lsp/server.js CHANGED
@@ -13,7 +13,7 @@ class TovaLanguageServer {
13
13
  static MAX_CACHE_SIZE = 100; // max cached diagnostics entries
14
14
 
15
15
  constructor() {
16
- this._buffer = '';
16
+ this._buffer = Buffer.alloc(0);
17
17
  this._documents = new Map(); // uri -> { text, version }
18
18
  this._diagnosticsCache = new Map(); // uri -> { ast, analyzer, errors, typeRegistry }
19
19
  this._initialized = false;
@@ -22,7 +22,7 @@ class TovaLanguageServer {
22
22
  }
23
23
 
24
24
  start() {
25
- process.stdin.setEncoding('utf8');
25
+ // Do NOT set encoding — use raw Buffers for correct byte-based Content-Length (LSP protocol)
26
26
  process.stdin.on('data', (chunk) => this._onData(chunk));
27
27
  process.stdin.on('end', () => process.exit(0));
28
28
 
@@ -46,12 +46,13 @@ class TovaLanguageServer {
46
46
  // ─── JSON-RPC Transport ────────────────────────────────────
47
47
 
48
48
  _onData(chunk) {
49
- this._buffer += chunk;
49
+ this._buffer = Buffer.concat([this._buffer, typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk]);
50
50
  while (true) {
51
- const headerEnd = this._buffer.indexOf('\r\n\r\n');
51
+ const sep = Buffer.from('\r\n\r\n');
52
+ const headerEnd = this._buffer.indexOf(sep);
52
53
  if (headerEnd === -1) break;
53
54
 
54
- const header = this._buffer.slice(0, headerEnd);
55
+ const header = this._buffer.slice(0, headerEnd).toString('utf8');
55
56
  const match = header.match(/Content-Length:\s*(\d+)/i);
56
57
  if (!match) {
57
58
  this._buffer = this._buffer.slice(headerEnd + 4);
@@ -62,7 +63,7 @@ class TovaLanguageServer {
62
63
  const start = headerEnd + 4;
63
64
  if (this._buffer.length < start + contentLength) break;
64
65
 
65
- const body = this._buffer.slice(start, start + contentLength);
66
+ const body = this._buffer.slice(start, start + contentLength).toString('utf8');
66
67
  this._buffer = this._buffer.slice(start + contentLength);
67
68
 
68
69
  try {
@@ -305,7 +306,7 @@ class TovaLanguageServer {
305
306
  diagnostics.push({
306
307
  range: {
307
308
  start: { line: (e.line || 1) - 1, character: (e.column || 1) - 1 },
308
- end: { line: (e.line || 1) - 1, character: (e.column || 1) + 10 },
309
+ end: { line: (e.line || 1) - 1, character: (e.column || 1) - 1 + 10 },
309
310
  },
310
311
  severity: 1,
311
312
  source: 'tova',
@@ -714,11 +715,32 @@ class TovaLanguageServer {
714
715
  const line = doc.text.split('\n')[position.line] || '';
715
716
  const before = line.slice(0, position.character);
716
717
 
717
- // Find function name before (
718
- const match = before.match(/(\w+)\s*\([^)]*$/);
719
- if (!match) return this._respond(msg.id, null);
718
+ // Walk backwards to find the immediately enclosing function call (handles nesting)
719
+ let depth = 0;
720
+ let parenPos = -1;
721
+ for (let i = before.length - 1; i >= 0; i--) {
722
+ if (before[i] === ')') depth++;
723
+ else if (before[i] === '(') {
724
+ if (depth === 0) { parenPos = i; break; }
725
+ depth--;
726
+ }
727
+ }
728
+ if (parenPos === -1) return this._respond(msg.id, null);
729
+
730
+ const funcMatch = before.slice(0, parenPos).match(/(\w+)\s*$/);
731
+ if (!funcMatch) return this._respond(msg.id, null);
732
+
733
+ const funcName = funcMatch[1];
720
734
 
721
- const funcName = match[1];
735
+ // Count commas at depth 0 after the enclosing paren (ignores nested call commas)
736
+ const afterParen = before.slice(parenPos + 1);
737
+ let activeParam = 0;
738
+ let parenDepth = 0;
739
+ for (const ch of afterParen) {
740
+ if (ch === '(') parenDepth++;
741
+ else if (ch === ')') parenDepth--;
742
+ else if (ch === ',' && parenDepth === 0) activeParam++;
743
+ }
722
744
 
723
745
  // Built-in signatures
724
746
  const signatures = {
@@ -737,10 +759,6 @@ class TovaLanguageServer {
737
759
 
738
760
  const sig = signatures[funcName];
739
761
  if (sig) {
740
- // Count commas to determine active parameter
741
- const afterParen = before.slice(before.lastIndexOf('(') + 1);
742
- const activeParam = (afterParen.match(/,/g) || []).length;
743
-
744
762
  return this._respond(msg.id, {
745
763
  signatures: [{
746
764
  label: sig.label,
@@ -755,17 +773,14 @@ class TovaLanguageServer {
755
773
  const cached = this._diagnosticsCache.get(textDocument.uri);
756
774
  if (cached?.analyzer) {
757
775
  const symbol = this._findSymbolInScopes(cached.analyzer, funcName);
758
- if (symbol?.params) {
759
- const afterParen = before.slice(before.lastIndexOf('(') + 1);
760
- const activeParam = (afterParen.match(/,/g) || []).length;
761
-
776
+ if (symbol?._params) {
762
777
  return this._respond(msg.id, {
763
778
  signatures: [{
764
- label: `${funcName}(${symbol.params.join(', ')})`,
765
- parameters: symbol.params.map(p => ({ label: p })),
779
+ label: `${funcName}(${symbol._params.join(', ')})`,
780
+ parameters: symbol._params.map(p => ({ label: p })),
766
781
  }],
767
782
  activeSignature: 0,
768
- activeParameter: Math.min(activeParam, symbol.params.length - 1),
783
+ activeParameter: Math.max(0, Math.min(activeParam, symbol._params.length - 1)),
769
784
  });
770
785
  }
771
786
  }
@@ -901,7 +916,12 @@ class TovaLanguageServer {
901
916
 
902
917
  _uriToPath(uri) {
903
918
  if (uri.startsWith('file://')) {
904
- return decodeURIComponent(uri.slice(7));
919
+ let path = decodeURIComponent(uri.slice(7));
920
+ // On Windows, file:///C:/path becomes /C:/path — strip leading slash
921
+ if (/^\/[a-zA-Z]:/.test(path)) {
922
+ path = path.slice(1);
923
+ }
924
+ return path;
905
925
  }
906
926
  return uri;
907
927
  }
@@ -2110,7 +2110,8 @@ export class Parser {
2110
2110
  parsePipe() {
2111
2111
  let left = this.parseNullCoalesce();
2112
2112
  while (this.match(TokenType.PIPE)) {
2113
- const l = this.loc();
2113
+ const opTok = this.tokens[this.pos - 1];
2114
+ const l = { line: opTok.line, column: opTok.column, file: this.filename };
2114
2115
  // Method pipe: |> .method(args) — parse as MemberExpression with empty Identifier
2115
2116
  if (this.check(TokenType.DOT)) {
2116
2117
  this.advance(); // consume .
@@ -2134,7 +2135,8 @@ export class Parser {
2134
2135
  parseNullCoalesce() {
2135
2136
  let left = this.parseOr();
2136
2137
  while (this.match(TokenType.QUESTION_QUESTION)) {
2137
- const l = this.loc();
2138
+ const opTok = this.tokens[this.pos - 1];
2139
+ const l = { line: opTok.line, column: opTok.column, file: this.filename };
2138
2140
  const right = this.parseOr();
2139
2141
  left = new AST.BinaryExpression('??', left, right, l);
2140
2142
  }
@@ -2144,7 +2146,8 @@ export class Parser {
2144
2146
  parseOr() {
2145
2147
  let left = this.parseAnd();
2146
2148
  while (this.match(TokenType.OR_OR) || this.match(TokenType.OR)) {
2147
- const l = this.loc();
2149
+ const opTok = this.tokens[this.pos - 1];
2150
+ const l = { line: opTok.line, column: opTok.column, file: this.filename };
2148
2151
  const right = this.parseAnd();
2149
2152
  left = new AST.LogicalExpression('or', left, right, l);
2150
2153
  }
@@ -2154,7 +2157,8 @@ export class Parser {
2154
2157
  parseAnd() {
2155
2158
  let left = this.parseNot();
2156
2159
  while (this.match(TokenType.AND_AND) || this.match(TokenType.AND)) {
2157
- const l = this.loc();
2160
+ const opTok = this.tokens[this.pos - 1];
2161
+ const l = { line: opTok.line, column: opTok.column, file: this.filename };
2158
2162
  const right = this.parseNot();
2159
2163
  left = new AST.LogicalExpression('and', left, right, l);
2160
2164
  }
@@ -2163,7 +2167,8 @@ export class Parser {
2163
2167
 
2164
2168
  parseNot() {
2165
2169
  if (this.match(TokenType.NOT) || this.match(TokenType.BANG)) {
2166
- const l = this.loc();
2170
+ const opTok = this.tokens[this.pos - 1];
2171
+ const l = { line: opTok.line, column: opTok.column, file: this.filename };
2167
2172
  const operand = this.parseNot();
2168
2173
  return new AST.UnaryExpression('not', operand, true, l);
2169
2174
  }
@@ -2349,6 +2354,10 @@ export class Parser {
2349
2354
  }
2350
2355
 
2351
2356
  if (this.check(TokenType.LPAREN)) {
2357
+ // Don't treat ( as call if it's on a new line (avoids ambiguity with grouped expressions)
2358
+ const prevLine = this.pos > 0 ? this.tokens[this.pos - 1].line : 0;
2359
+ const curLine = this.current().line;
2360
+ if (curLine > prevLine) break;
2352
2361
  expr = this.parseCallExpression(expr);
2353
2362
  continue;
2354
2363
  }
@@ -1,6 +1,6 @@
1
1
  // Auto-generated by scripts/embed-runtime.js — do not edit
2
2
 
3
- export const REACTIVITY_SOURCE = "// Fine-grained reactivity system for Tova (signals-based)\n\nconst __DEV__ = typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production';\n\n// ─── DevTools hooks (zero-cost when disabled) ────────────\nlet __devtools_hooks = null;\nexport function __enableDevTools(hooks) {\n __devtools_hooks = hooks;\n}\n\nlet currentEffect = null;\nconst effectStack = [];\n\n// ─── Ownership System ─────────────────────────────────────\nlet currentOwner = null;\nconst ownerStack = [];\n\n// ─── Batching ────────────────────────────────────────────\n// Default: synchronous flush after each setter (backward compatible).\n// Inside batch(): effects are deferred and flushed once when batch ends.\n// This means setA(1); setB(2) causes 2 runs by default, but\n// batch(() => { setA(1); setB(2); }) causes only 1 run.\n\nlet pendingEffects = new Set();\nlet batchDepth = 0;\nlet flushing = false;\n\nfunction flush() {\n if (flushing) return; // prevent re-entrant flush\n flushing = true;\n let iterations = 0;\n try {\n while (pendingEffects.size > 0) {\n if (++iterations > 100) {\n console.error('Tova: Possible infinite loop in reactive updates (>100 flush iterations). Aborting.');\n pendingEffects.clear();\n break;\n }\n const toRun = [...pendingEffects];\n pendingEffects.clear();\n for (const effect of toRun) {\n if (!effect._disposed) {\n effect();\n }\n }\n }\n } finally {\n flushing = false;\n }\n}\n\nexport function batch(fn) {\n batchDepth++;\n try {\n fn();\n } finally {\n batchDepth--;\n if (batchDepth === 0) {\n flush();\n }\n }\n}\n\n// ─── Ownership Root ──────────────────────────────────────\n\nexport function createRoot(fn) {\n const root = {\n _children: [],\n _disposed: false,\n _cleanups: [],\n _contexts: null,\n _owner: currentOwner,\n dispose() {\n if (root._disposed) return;\n root._disposed = true;\n // Dispose children in reverse order\n for (let i = root._children.length - 1; i >= 0; i--) {\n const child = root._children[i];\n if (typeof child.dispose === 'function') child.dispose();\n }\n root._children.length = 0;\n // Run cleanups in reverse order\n for (let i = root._cleanups.length - 1; i >= 0; i--) {\n try { root._cleanups[i](); } catch (e) { console.error('Tova: root cleanup error:', e); }\n }\n root._cleanups.length = 0;\n }\n };\n ownerStack.push(currentOwner);\n currentOwner = root;\n try {\n return fn(root.dispose.bind(root));\n } finally {\n currentOwner = ownerStack.pop();\n }\n}\n\n// ─── Dependency Cleanup ──────────────────────────────────\n\nfunction cleanupDeps(subscriber) {\n if (subscriber._deps) {\n for (const depSet of subscriber._deps) {\n depSet.delete(subscriber);\n }\n subscriber._deps.clear();\n }\n}\n\nfunction trackDep(subscriber, subscriberSet) {\n subscriberSet.add(subscriber);\n if (!subscriber._deps) subscriber._deps = new Set();\n subscriber._deps.add(subscriberSet);\n}\n\n// ─── Signals ─────────────────────────────────────────────\n\nexport function createSignal(initialValue, name) {\n let value = initialValue;\n const subscribers = new Set();\n let signalId = null;\n\n if (__devtools_hooks) {\n signalId = __devtools_hooks.onSignalCreate(\n () => value,\n (v) => setter(v),\n name,\n );\n }\n\n function getter() {\n if (currentEffect) {\n trackDep(currentEffect, subscribers);\n }\n return value;\n }\n\n function setter(newValue) {\n if (typeof newValue === 'function') {\n newValue = newValue(value);\n }\n if (value !== newValue) {\n const oldValue = value;\n value = newValue;\n if (__devtools_hooks && signalId != null) {\n __devtools_hooks.onSignalUpdate(signalId, oldValue, newValue);\n }\n for (const sub of [...subscribers]) {\n if (sub._isComputed) {\n sub(); // propagate dirty flags synchronously through computed graph\n } else {\n pendingEffects.add(sub);\n }\n }\n if (batchDepth === 0) {\n flush();\n }\n }\n }\n\n return [getter, setter];\n}\n\n// ─── Effects ─────────────────────────────────────────────\n\nfunction runCleanups(effect) {\n if (effect._cleanup) {\n try { effect._cleanup(); } catch (e) { console.error('Tova: cleanup error:', e); }\n effect._cleanup = null;\n }\n if (effect._cleanups && effect._cleanups.length > 0) {\n for (const cb of effect._cleanups) {\n try { cb(); } catch (e) { console.error('Tova: cleanup error:', e); }\n }\n effect._cleanups = [];\n }\n}\n\nexport function createEffect(fn) {\n function effect() {\n if (effect._running) return;\n if (effect._disposed) return;\n effect._running = true;\n\n // Run cleanups from previous execution\n runCleanups(effect);\n\n // Remove from all previous dependency subscriber sets\n cleanupDeps(effect);\n\n effectStack.push(effect);\n currentEffect = effect;\n const startTime = __devtools_hooks && typeof performance !== 'undefined' ? performance.now() : 0;\n try {\n const result = fn();\n // If effect returns a function, use as cleanup\n if (typeof result === 'function') {\n effect._cleanup = result;\n }\n } catch (e) {\n console.error('Tova: Error in effect:', e);\n if (currentErrorHandler) {\n currentErrorHandler(e);\n }\n } finally {\n if (__devtools_hooks) {\n const duration = typeof performance !== 'undefined' ? performance.now() - startTime : 0;\n __devtools_hooks.onEffectRun(effect, duration);\n }\n effectStack.pop();\n currentEffect = effectStack[effectStack.length - 1] || null;\n effect._running = false;\n }\n }\n\n effect._deps = new Set();\n effect._running = false;\n effect._disposed = false;\n effect._cleanup = null;\n effect._cleanups = [];\n effect._owner = currentOwner;\n\n if (__devtools_hooks) {\n __devtools_hooks.onEffectCreate(effect);\n }\n\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._children.push(effect);\n }\n\n effect.dispose = function () {\n effect._disposed = true;\n runCleanups(effect);\n cleanupDeps(effect);\n pendingEffects.delete(effect);\n // Remove from owner's children\n if (effect._owner) {\n const idx = effect._owner._children.indexOf(effect);\n if (idx >= 0) effect._owner._children.splice(idx, 1);\n }\n };\n\n // Run immediately (synchronous first run)\n effect();\n return effect;\n}\n\n// ─── Computed (lazy/pull-based for glitch-free reads) ────\n\nexport function createComputed(fn) {\n let value;\n let dirty = true;\n const subscribers = new Set();\n\n // notify is called synchronously when a source signal changes.\n // It marks the computed dirty and propagates to downstream subscribers.\n function notify() {\n if (!dirty) {\n dirty = true;\n for (const sub of [...subscribers]) {\n if (sub._isComputed) {\n sub(); // cascade dirty flags synchronously\n } else {\n pendingEffects.add(sub);\n }\n }\n }\n }\n\n notify._deps = new Set();\n notify._disposed = false;\n notify._isComputed = true;\n notify._owner = currentOwner;\n\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._children.push(notify);\n }\n\n notify.dispose = function () {\n notify._disposed = true;\n cleanupDeps(notify);\n if (notify._owner) {\n const idx = notify._owner._children.indexOf(notify);\n if (idx >= 0) notify._owner._children.splice(idx, 1);\n }\n };\n\n function recompute() {\n cleanupDeps(notify);\n\n effectStack.push(notify);\n currentEffect = notify;\n try {\n value = fn();\n dirty = false;\n } finally {\n effectStack.pop();\n currentEffect = effectStack[effectStack.length - 1] || null;\n }\n }\n\n // Initial computation\n recompute();\n\n function getter() {\n if (currentEffect) {\n trackDep(currentEffect, subscribers);\n }\n if (dirty) {\n recompute();\n }\n return value;\n }\n\n return getter;\n}\n\n// ─── Lifecycle Hooks ─────────────────────────────────────\n\nexport function onMount(fn) {\n const owner = currentOwner;\n queueMicrotask(() => {\n const result = fn();\n if (typeof result === 'function' && owner && !owner._disposed) {\n owner._cleanups.push(result);\n }\n });\n}\n\nexport function onUnmount(fn) {\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._cleanups.push(fn);\n }\n}\n\nexport function onCleanup(fn) {\n if (currentEffect) {\n if (!currentEffect._cleanups) currentEffect._cleanups = [];\n currentEffect._cleanups.push(fn);\n }\n}\n\n// ─── Untrack ─────────────────────────────────────────────\n// Run a function without tracking any signal reads (opt out of reactivity)\n\nexport function untrack(fn) {\n const prev = currentEffect;\n currentEffect = null;\n try {\n return fn();\n } finally {\n currentEffect = prev;\n }\n}\n\n// ─── Watch ───────────────────────────────────────────────\n// Watch a reactive expression, calling callback with (newValue, oldValue)\n// Returns a dispose function to stop watching.\n\nexport function watch(getter, callback, options = {}) {\n let oldValue = undefined;\n let initialized = false;\n\n const effect = createEffect(() => {\n const newValue = getter();\n if (initialized) {\n callback(newValue, oldValue);\n } else if (options.immediate) {\n callback(newValue, undefined);\n }\n oldValue = newValue;\n initialized = true;\n });\n\n return effect.dispose ? effect.dispose.bind(effect) : () => {\n effect._disposed = true;\n runCleanups(effect);\n cleanupDeps(effect);\n pendingEffects.delete(effect);\n };\n}\n\n// ─── Refs ────────────────────────────────────────────────\n\nexport function createRef(initialValue) {\n return { current: initialValue !== undefined ? initialValue : null };\n}\n\n// ─── Error Boundaries ────────────────────────────────────\n\n// Stack-based error handler for correct nested boundary propagation\nconst errorHandlerStack = [];\nlet currentErrorHandler = null;\n\nfunction pushErrorHandler(handler) {\n errorHandlerStack.push(currentErrorHandler);\n currentErrorHandler = handler;\n}\n\nfunction popErrorHandler() {\n currentErrorHandler = errorHandlerStack.pop() || null;\n}\n\n// Component name tracking for stack traces\nconst componentNameStack = [];\n\nexport function pushComponentName(name) {\n componentNameStack.push(name);\n}\n\nexport function popComponentName() {\n componentNameStack.pop();\n}\n\nfunction buildComponentStack() {\n return [...componentNameStack].reverse();\n}\n\nexport function createErrorBoundary(options = {}) {\n const { onError, onReset } = options;\n const [error, setError] = createSignal(null);\n\n function run(fn) {\n pushErrorHandler((e) => {\n const stack = buildComponentStack();\n if (e && typeof e === 'object') e.__tovaComponentStack = stack;\n setError(e);\n if (onError) onError({ error: e, componentStack: stack });\n });\n try {\n return fn();\n } catch (e) {\n const stack = buildComponentStack();\n if (e && typeof e === 'object') e.__tovaComponentStack = stack;\n setError(e);\n if (onError) onError({ error: e, componentStack: stack });\n return null;\n } finally {\n popErrorHandler();\n }\n }\n\n function reset() {\n setError(null);\n if (onReset) onReset();\n }\n\n return { error, run, reset };\n}\n\nexport function ErrorBoundary({ fallback, children, onError, onReset, retry = 0 }) {\n const [error, setError] = createSignal(null);\n const [retryCount, setRetryCount] = createSignal(0);\n\n function handleError(e) {\n const stack = buildComponentStack();\n if (e && typeof e === 'object') e.__tovaComponentStack = stack;\n if (retryCount() < retry) {\n setRetryCount(c => c + 1);\n setError(null); // clear to re-trigger render\n return;\n }\n setError(e);\n if (onError) onError({ error: e, componentStack: stack });\n }\n\n pushErrorHandler(handleError);\n\n // Return a reactive wrapper that switches between children and fallback\n const childContent = children && children.length === 1 ? children[0] : tova_fragment(children || []);\n\n popErrorHandler();\n\n const vnode = {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n _fallback: fallback,\n _componentName: 'ErrorBoundary',\n compute: () => {\n const err = error();\n if (err) {\n // Render fallback — if fallback itself throws, propagate to parent boundary\n try {\n return typeof fallback === 'function'\n ? fallback({\n error: err,\n reset: () => {\n setRetryCount(0);\n setError(null);\n if (onReset) onReset();\n },\n })\n : fallback;\n } catch (fallbackError) {\n // Fallback threw — propagate to parent error boundary\n if (currentErrorHandler) {\n currentErrorHandler(fallbackError);\n }\n return null;\n }\n }\n return childContent;\n },\n };\n\n return vnode;\n}\n\n// ─── Dynamic Component ──────────────────────────────────\n// Renders a component dynamically based on a reactive signal.\n// Usage: Dynamic({ component: mySignal, ...props })\n\nexport function Dynamic({ component, ...rest }) {\n return {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n compute: () => {\n const comp = typeof component === 'function' && !component.__tova ? component() : component;\n if (!comp) return null;\n if (typeof comp === 'function') {\n return comp(rest);\n }\n return comp;\n },\n };\n}\n\n// ─── Portal ─────────────────────────────────────────────\n// Renders children into a different DOM target.\n// Usage: Portal({ target: \"#modal-root\", children })\n\nexport function Portal({ target, children }) {\n return {\n __tova: true,\n tag: '__portal',\n props: { target },\n children: children || [],\n };\n}\n\n// ─── Lazy ───────────────────────────────────────────────\n// Async component loading with optional fallback.\n// Usage: const LazyComp = lazy(() => import('./HeavyComponent.js'))\n\nexport function lazy(loader) {\n let resolved = null;\n let promise = null;\n\n return function LazyWrapper(props) {\n if (resolved) {\n return resolved(props);\n }\n\n const [comp, setComp] = createSignal(null);\n const [err, setErr] = createSignal(null);\n\n if (!promise) {\n promise = loader()\n .then(mod => {\n resolved = mod.default || mod;\n setComp(() => resolved);\n })\n .catch(e => setErr(e));\n }\n\n return {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n compute: () => {\n const e = err();\n if (e) return tova_el('span', { className: 'tova-error' }, [String(e)]);\n const c = comp();\n if (c) return c(props);\n // Fallback while loading\n return props && props.fallback ? props.fallback : null;\n },\n };\n };\n}\n\n// ─── Context (Provide/Inject) ────────────────────────────\n// Tree-based: values are stored on the ownership tree, inject walks up.\n\nexport function createContext(defaultValue) {\n const id = Symbol('context');\n return { _id: id, _default: defaultValue };\n}\n\nexport function provide(context, value) {\n const owner = currentOwner;\n if (owner) {\n if (!owner._contexts) owner._contexts = new Map();\n owner._contexts.set(context._id, value);\n }\n}\n\nexport function inject(context) {\n let owner = currentOwner;\n while (owner) {\n if (owner._contexts && owner._contexts.has(context._id)) {\n return owner._contexts.get(context._id);\n }\n owner = owner._owner;\n }\n return context._default;\n}\n\n// ─── DOM Rendering ────────────────────────────────────────\n\n// Inject scoped CSS into the page (idempotent — only injects once per id)\nconst __tovaInjectedStyles = new Set();\nexport function tova_inject_css(id, css) {\n if (__tovaInjectedStyles.has(id)) return;\n __tovaInjectedStyles.add(id);\n const style = document.createElement('style');\n style.setAttribute('data-tova-style', id);\n style.textContent = css;\n document.head.appendChild(style);\n}\n\nexport function tova_el(tag, props = {}, children = []) {\n return { __tova: true, tag, props, children };\n}\n\nexport function tova_fragment(children) {\n return { __tova: true, tag: '__fragment', props: {}, children };\n}\n\n// Inject a key prop into a vnode for keyed reconciliation\nexport function tova_keyed(key, vnode) {\n if (vnode && vnode.__tova) {\n vnode.props = { ...vnode.props, key };\n }\n return vnode;\n}\n\n// Flatten nested arrays and vnodes into a flat list of vnodes\nfunction flattenVNodes(children) {\n const result = [];\n for (const child of children) {\n if (child === null || child === undefined) {\n continue;\n } else if (Array.isArray(child)) {\n result.push(...flattenVNodes(child));\n } else {\n result.push(child);\n }\n }\n return result;\n}\n\n// ─── Marker-based DOM helpers ─────────────────────────────\n// Instead of wrapping dynamic blocks/fragments in <span style=\"display:contents\">,\n// we use comment node markers. A marker's __tovaNodes tracks its content nodes.\n// Content nodes have __tovaOwner pointing to their owning marker.\n\n// Recursively dispose ownership roots attached to a DOM subtree\nfunction disposeNode(node) {\n if (!node) return;\n if (node.__tovaRoot) {\n node.__tovaRoot();\n node.__tovaRoot = null;\n }\n // If this is a marker, dispose and remove its content nodes\n if (node.__tovaNodes) {\n for (const cn of node.__tovaNodes) {\n disposeNode(cn);\n if (cn.parentNode) cn.parentNode.removeChild(cn);\n }\n node.__tovaNodes = [];\n }\n if (node.childNodes) {\n for (const child of Array.from(node.childNodes)) {\n disposeNode(child);\n }\n }\n}\n\n// Check if a node is transitively owned by a marker (walks __tovaOwner chain)\nfunction isOwnedBy(node, marker) {\n let owner = node.__tovaOwner;\n while (owner) {\n if (owner === marker) return true;\n owner = owner.__tovaOwner;\n }\n return false;\n}\n\n// Get logical children of a parent element (skips marker content nodes)\nfunction getLogicalChildren(parent) {\n const logical = [];\n for (let i = 0; i < parent.childNodes.length; i++) {\n const node = parent.childNodes[i];\n if (!node.__tovaOwner) {\n logical.push(node);\n }\n }\n return logical;\n}\n\n// Find the first DOM sibling after all of a marker's content\nfunction nextSiblingAfterMarker(marker) {\n if (!marker.__tovaNodes || marker.__tovaNodes.length === 0) {\n return marker.nextSibling;\n }\n let last = marker.__tovaNodes[marker.__tovaNodes.length - 1];\n // If last content is itself a marker, recurse to find physical end\n while (last && last.__tovaNodes && last.__tovaNodes.length > 0) {\n last = last.__tovaNodes[last.__tovaNodes.length - 1];\n }\n return last ? last.nextSibling : marker.nextSibling;\n}\n\n// Remove a logical node (marker + its content, or a regular node) from the DOM\nfunction removeLogicalNode(parent, node) {\n disposeNode(node);\n if (node.parentNode === parent) parent.removeChild(node);\n}\n\n// Insert rendered result (could be single node or DocumentFragment) before ref,\n// setting __tovaOwner on top-level inserted nodes. Returns array of inserted nodes.\nfunction insertRendered(parent, rendered, ref, owner) {\n if (rendered.nodeType === 11) {\n const nodes = Array.from(rendered.childNodes);\n for (const n of nodes) {\n if (!n.__tovaOwner) n.__tovaOwner = owner;\n }\n parent.insertBefore(rendered, ref);\n return nodes;\n }\n if (!rendered.__tovaOwner) rendered.__tovaOwner = owner;\n parent.insertBefore(rendered, ref);\n return [rendered];\n}\n\n// Clear a marker's content from the DOM and reset __tovaNodes\nfunction clearMarkerContent(marker) {\n for (const node of marker.__tovaNodes) {\n disposeNode(node);\n if (node.parentNode) node.parentNode.removeChild(node);\n }\n marker.__tovaNodes = [];\n}\n\n// ─── Render ───────────────────────────────────────────────\n\n// Create real DOM nodes from a vnode (with fine-grained reactive bindings).\n// Returns a single DOM node for elements/text, or a DocumentFragment for\n// markers (dynamic blocks, fragments) containing [marker, ...content].\nexport function render(vnode) {\n if (vnode === null || vnode === undefined) {\n return document.createTextNode('');\n }\n\n // Reactive dynamic block (JSXIf, JSXFor, reactive text, etc.)\n if (typeof vnode === 'function') {\n const marker = document.createComment('');\n marker.__tovaDynamic = true;\n marker.__tovaNodes = [];\n\n const frag = document.createDocumentFragment();\n frag.appendChild(marker);\n\n createEffect(() => {\n const val = vnode();\n const parent = marker.parentNode;\n const ref = nextSiblingAfterMarker(marker);\n\n // Array: keyed or positional reconciliation within marker range\n if (Array.isArray(val)) {\n const flat = flattenVNodes(val);\n const hasKeys = flat.some(c => getKey(c) != null);\n if (hasKeys) {\n patchKeyedInMarker(marker, flat);\n } else {\n patchPositionalInMarker(marker, flat);\n }\n return;\n }\n\n // Text: optimize single text node update in place\n if (val == null || typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {\n const text = val == null ? '' : String(val);\n if (marker.__tovaNodes.length === 1 && marker.__tovaNodes[0].nodeType === 3) {\n if (marker.__tovaNodes[0].textContent !== text) {\n marker.__tovaNodes[0].textContent = text;\n }\n return;\n }\n clearMarkerContent(marker);\n const textNode = document.createTextNode(text);\n textNode.__tovaOwner = marker;\n parent.insertBefore(textNode, ref);\n marker.__tovaNodes = [textNode];\n return;\n }\n\n // Vnode or other: clear and re-render\n clearMarkerContent(marker);\n if (val && val.__tova) {\n const rendered = render(val);\n marker.__tovaNodes = insertRendered(parent, rendered, ref, marker);\n } else {\n const textNode = document.createTextNode(String(val));\n textNode.__tovaOwner = marker;\n parent.insertBefore(textNode, ref);\n marker.__tovaNodes = [textNode];\n }\n });\n\n return frag;\n }\n\n if (typeof vnode === 'string' || typeof vnode === 'number' || typeof vnode === 'boolean') {\n return document.createTextNode(String(vnode));\n }\n\n if (Array.isArray(vnode)) {\n const fragment = document.createDocumentFragment();\n for (const child of vnode) {\n fragment.appendChild(render(child));\n }\n return fragment;\n }\n\n if (!vnode.__tova) {\n return document.createTextNode(String(vnode));\n }\n\n // Fragment — marker + children (no wrapper element)\n if (vnode.tag === '__fragment') {\n const marker = document.createComment('');\n marker.__tovaFragment = true;\n marker.__tovaNodes = [];\n marker.__vnode = vnode;\n\n const frag = document.createDocumentFragment();\n frag.appendChild(marker);\n\n for (const child of flattenVNodes(vnode.children)) {\n const rendered = render(child);\n const inserted = insertRendered(frag, rendered, null, marker);\n marker.__tovaNodes.push(...inserted);\n }\n\n return frag;\n }\n\n // Dynamic reactive node (ErrorBoundary, Dynamic component, etc.)\n if (vnode.tag === '__dynamic' && typeof vnode.compute === 'function') {\n const marker = document.createComment('');\n marker.__tovaDynamic = true;\n marker.__tovaNodes = [];\n\n const frag = document.createDocumentFragment();\n frag.appendChild(marker);\n\n let prevDispose = null;\n createEffect(() => {\n const inner = vnode.compute();\n const parent = marker.parentNode;\n const ref = nextSiblingAfterMarker(marker);\n\n if (prevDispose) {\n prevDispose();\n prevDispose = null;\n }\n clearMarkerContent(marker);\n\n createRoot((dispose) => {\n prevDispose = dispose;\n const rendered = render(inner);\n marker.__tovaNodes = insertRendered(parent, rendered, ref, marker);\n });\n });\n\n return frag;\n }\n\n // Portal — render children into a different DOM target\n if (vnode.tag === '__portal') {\n const placeholder = document.createComment('portal');\n const targetSelector = vnode.props.target;\n queueMicrotask(() => {\n const targetEl = typeof targetSelector === 'string'\n ? document.querySelector(targetSelector)\n : targetSelector;\n if (targetEl) {\n for (const child of flattenVNodes(vnode.children)) {\n targetEl.appendChild(render(child));\n }\n }\n });\n return placeholder;\n }\n\n // Element\n const el = document.createElement(vnode.tag);\n applyReactiveProps(el, vnode.props);\n\n // Set data-tova-component attribute for DevTools\n if (vnode._componentName) {\n el.setAttribute('data-tova-component', vnode._componentName);\n if (__devtools_hooks && __devtools_hooks.onComponentRender) {\n __devtools_hooks.onComponentRender(vnode._componentName, el, 0);\n }\n }\n\n // Render children\n for (const child of flattenVNodes(vnode.children)) {\n el.appendChild(render(child));\n }\n\n // Store vnode reference for patching\n el.__vnode = vnode;\n\n return el;\n}\n\n// Apply reactive props — function-valued props get their own effect\nfunction applyReactiveProps(el, props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'ref') {\n if (typeof value === 'object' && value !== null && 'current' in value) {\n value.current = el;\n } else if (typeof value === 'function') {\n value(el);\n }\n } else if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n } else if (key === 'key') {\n // Skip\n } else if (typeof value === 'function' && !key.startsWith('on')) {\n // Reactive prop — create effect for fine-grained updates\n createEffect(() => {\n const val = value();\n applyPropValue(el, key, val);\n });\n } else {\n applyPropValue(el, key, value);\n }\n }\n}\n\nfunction applyPropValue(el, key, val) {\n if (key === 'className') {\n if (el.className !== val) el.className = val || '';\n } else if (key === 'innerHTML' || key === 'dangerouslySetInnerHTML') {\n const html = typeof val === 'object' && val !== null ? val.__html || '' : val || '';\n if (el.innerHTML !== html) el.innerHTML = html;\n } else if (key === 'value') {\n if (el !== document.activeElement && el.value !== val) {\n el.value = val;\n }\n } else if (key === 'checked') {\n el.checked = !!val;\n } else if (key === 'disabled' || key === 'readOnly' || key === 'hidden') {\n el[key] = !!val;\n } else if (key === 'style' && typeof val === 'object') {\n Object.assign(el.style, val);\n } else {\n const s = val == null ? '' : String(val);\n if (el.getAttribute(key) !== s) {\n el.setAttribute(key, s);\n }\n }\n}\n\n// Apply/update props on a DOM element (used by patcher for full-tree mode)\nfunction applyProps(el, newProps, oldProps) {\n // Remove old props that are no longer present\n for (const key of Object.keys(oldProps)) {\n if (!(key in newProps)) {\n if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n if (el.__handlers && el.__handlers[eventName]) {\n el.removeEventListener(eventName, el.__handlers[eventName]);\n delete el.__handlers[eventName];\n }\n } else if (key === 'className') {\n el.className = '';\n } else if (key === 'style') {\n el.removeAttribute('style');\n } else {\n el.removeAttribute(key);\n }\n }\n }\n\n // Apply new props\n for (const [key, value] of Object.entries(newProps)) {\n if (key === 'className') {\n const val = typeof value === 'function' ? value() : value;\n if (el.className !== val) el.className = val;\n } else if (key === 'ref') {\n if (typeof value === 'object' && value !== null && 'current' in value) {\n value.current = el;\n } else if (typeof value === 'function') {\n value(el);\n }\n } else if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n const oldHandler = el.__handlers && el.__handlers[eventName];\n if (oldHandler !== value) {\n if (oldHandler) el.removeEventListener(eventName, oldHandler);\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n }\n } else if (key === 'style' && typeof value === 'object') {\n Object.assign(el.style, value);\n } else if (key === 'key') {\n // Skip\n } else if (key === 'value') {\n const val = typeof value === 'function' ? value() : value;\n if (el !== document.activeElement && el.value !== val) {\n el.value = val;\n }\n } else if (key === 'checked') {\n el.checked = !!value;\n } else {\n const val = typeof value === 'function' ? value() : value;\n if (el.getAttribute(key) !== String(val)) {\n el.setAttribute(key, val);\n }\n }\n }\n}\n\n// ─── Keyed Reconciliation ────────────────────────────────\n\nfunction getKey(vnode) {\n if (vnode && vnode.__tova && vnode.props) return vnode.props.key;\n return undefined;\n}\n\nfunction getNodeKey(node) {\n if (node && node.__vnode && node.__vnode.props) return node.__vnode.props.key;\n return undefined;\n}\n\n// Keyed reconciliation within a marker's content range\nfunction patchKeyedInMarker(marker, newVNodes) {\n const parent = marker.parentNode;\n const oldNodes = [...marker.__tovaNodes];\n const oldKeyMap = new Map();\n\n for (const node of oldNodes) {\n const key = getNodeKey(node);\n if (key != null) oldKeyMap.set(key, node);\n }\n\n const newNodes = [];\n const usedOld = new Set();\n\n for (const newChild of newVNodes) {\n const key = getKey(newChild);\n\n if (key != null && oldKeyMap.has(key)) {\n const oldNode = oldKeyMap.get(key);\n usedOld.add(oldNode);\n\n if (oldNode.nodeType === 1 && newChild.__tova &&\n oldNode.tagName.toLowerCase() === newChild.tag.toLowerCase()) {\n const oldVNode = oldNode.__vnode || { props: {}, children: [] };\n applyProps(oldNode, newChild.props, oldVNode.props);\n patchChildrenOfElement(oldNode, flattenVNodes(newChild.children));\n oldNode.__vnode = newChild;\n newNodes.push(oldNode);\n } else {\n const node = render(newChild);\n // render may return Fragment — collect nodes\n if (node.nodeType === 11) {\n const nodes = Array.from(node.childNodes);\n for (const n of nodes) { if (!n.__tovaOwner) n.__tovaOwner = marker; }\n parent.insertBefore(node, nextSiblingAfterMarker(marker));\n newNodes.push(...nodes);\n } else {\n if (!node.__tovaOwner) node.__tovaOwner = marker;\n newNodes.push(node);\n }\n }\n } else {\n const node = render(newChild);\n if (node.nodeType === 11) {\n const nodes = Array.from(node.childNodes);\n for (const n of nodes) { if (!n.__tovaOwner) n.__tovaOwner = marker; }\n parent.insertBefore(node, nextSiblingAfterMarker(marker));\n newNodes.push(...nodes);\n } else {\n if (!node.__tovaOwner) node.__tovaOwner = marker;\n newNodes.push(node);\n }\n }\n }\n\n // Remove unused old nodes\n for (const node of oldNodes) {\n if (!usedOld.has(node)) {\n disposeNode(node);\n if (node.parentNode === parent) parent.removeChild(node);\n }\n }\n\n // Arrange in correct order after marker using cursor approach\n let cursor = marker.nextSibling;\n for (const node of newNodes) {\n if (node === cursor) {\n cursor = node.nextSibling;\n } else {\n parent.insertBefore(node, cursor);\n }\n }\n\n marker.__tovaNodes = newNodes;\n}\n\n// Positional reconciliation within a marker's content range\nfunction patchPositionalInMarker(marker, newChildren) {\n const parent = marker.parentNode;\n const oldNodes = [...marker.__tovaNodes];\n const oldCount = oldNodes.length;\n const newCount = newChildren.length;\n\n // Patch in place\n const patchCount = Math.min(oldCount, newCount);\n for (let i = 0; i < patchCount; i++) {\n patchSingle(parent, oldNodes[i], newChildren[i]);\n }\n\n // Append new children\n const ref = nextSiblingAfterMarker(marker);\n for (let i = oldCount; i < newCount; i++) {\n const rendered = render(newChildren[i]);\n const inserted = insertRendered(parent, rendered, ref, marker);\n oldNodes.push(...inserted);\n }\n\n // Remove excess children\n for (let i = newCount; i < oldCount; i++) {\n disposeNode(oldNodes[i]);\n if (oldNodes[i].parentNode === parent) parent.removeChild(oldNodes[i]);\n }\n\n marker.__tovaNodes = oldNodes.slice(0, Math.max(newCount, oldCount > newCount ? newCount : oldNodes.length));\n // Simplify: rebuild __tovaNodes from what should remain\n if (newCount <= oldCount) {\n marker.__tovaNodes = oldNodes.slice(0, newCount);\n }\n}\n\n// Keyed reconciliation for children of an element (not marker-based)\nfunction patchKeyedChildren(parent, newVNodes) {\n const logical = getLogicalChildren(parent);\n const oldKeyMap = new Map();\n\n for (const node of logical) {\n const key = getNodeKey(node);\n if (key != null) oldKeyMap.set(key, node);\n }\n\n const newNodes = [];\n const usedOld = new Set();\n\n for (const newChild of newVNodes) {\n const key = getKey(newChild);\n\n if (key != null && oldKeyMap.has(key)) {\n const oldNode = oldKeyMap.get(key);\n usedOld.add(oldNode);\n\n if (oldNode.nodeType === 1 && newChild.__tova &&\n oldNode.tagName.toLowerCase() === newChild.tag.toLowerCase()) {\n const oldVNode = oldNode.__vnode || { props: {}, children: [] };\n applyProps(oldNode, newChild.props, oldVNode.props);\n patchChildrenOfElement(oldNode, flattenVNodes(newChild.children));\n oldNode.__vnode = newChild;\n newNodes.push(oldNode);\n } else {\n newNodes.push(render(newChild));\n }\n } else {\n newNodes.push(render(newChild));\n }\n }\n\n // Remove unused old logical nodes\n for (const node of logical) {\n if (!usedOld.has(node) && node.parentNode === parent) {\n removeLogicalNode(parent, node);\n }\n }\n\n // Arrange in correct order\n for (let i = 0; i < newNodes.length; i++) {\n const expected = newNodes[i];\n const logicalNow = getLogicalChildren(parent);\n const current = logicalNow[i];\n if (current !== expected) {\n parent.insertBefore(expected, current || null);\n }\n }\n}\n\n// Positional reconciliation for children of an element\nfunction patchPositionalChildren(parent, newChildren) {\n const logical = getLogicalChildren(parent);\n const oldCount = logical.length;\n const newCount = newChildren.length;\n\n for (let i = 0; i < Math.min(oldCount, newCount); i++) {\n patchSingle(parent, logical[i], newChildren[i]);\n }\n\n for (let i = oldCount; i < newCount; i++) {\n parent.appendChild(render(newChildren[i]));\n }\n\n // Remove excess logical children\n const currentLogical = getLogicalChildren(parent);\n while (currentLogical.length > newCount) {\n const node = currentLogical.pop();\n removeLogicalNode(parent, node);\n }\n}\n\n// Patch children of a regular element\nfunction patchChildrenOfElement(el, newChildren) {\n const hasKeys = newChildren.some(c => getKey(c) != null);\n if (hasKeys) {\n patchKeyedChildren(el, newChildren);\n } else {\n patchPositionalChildren(el, newChildren);\n }\n}\n\n// Patch a single logical node in place\nfunction patchSingle(parent, existing, newVNode) {\n if (!existing) {\n parent.appendChild(render(newVNode));\n return;\n }\n\n if (newVNode === null || newVNode === undefined) {\n removeLogicalNode(parent, existing);\n return;\n }\n\n // Function vnode — replace with new dynamic block\n if (typeof newVNode === 'function') {\n const rendered = render(newVNode);\n if (existing.__tovaNodes) {\n // Existing is a marker — clear its content and replace\n clearMarkerContent(existing);\n parent.replaceChild(rendered, existing);\n } else {\n disposeNode(existing);\n parent.replaceChild(rendered, existing);\n }\n return;\n }\n\n // Text\n if (typeof newVNode === 'string' || typeof newVNode === 'number' || typeof newVNode === 'boolean') {\n const text = String(newVNode);\n if (existing.nodeType === 3) {\n if (existing.textContent !== text) existing.textContent = text;\n } else {\n removeLogicalNode(parent, existing);\n parent.insertBefore(document.createTextNode(text), null);\n }\n return;\n }\n\n if (!newVNode.__tova) {\n const text = String(newVNode);\n if (existing.nodeType === 3) {\n if (existing.textContent !== text) existing.textContent = text;\n } else {\n removeLogicalNode(parent, existing);\n parent.insertBefore(document.createTextNode(text), null);\n }\n return;\n }\n\n // Fragment — patch marker content\n if (newVNode.tag === '__fragment') {\n if (existing.__tovaFragment) {\n // Patch children within the marker range\n const oldNodes = [...existing.__tovaNodes];\n const newChildren = flattenVNodes(newVNode.children);\n // Simple approach: clear and re-render fragment content\n clearMarkerContent(existing);\n const ref = nextSiblingAfterMarker(existing);\n for (const child of newChildren) {\n const rendered = render(child);\n const inserted = insertRendered(parent, rendered, ref, existing);\n existing.__tovaNodes.push(...inserted);\n }\n existing.__vnode = newVNode;\n return;\n }\n removeLogicalNode(parent, existing);\n parent.appendChild(render(newVNode));\n return;\n }\n\n // Element — patch in place\n if (existing.nodeType === 1 && newVNode.tag &&\n existing.tagName.toLowerCase() === newVNode.tag.toLowerCase()) {\n const oldVNode = existing.__vnode || { props: {}, children: [] };\n applyProps(existing, newVNode.props, oldVNode.props);\n patchChildrenOfElement(existing, flattenVNodes(newVNode.children));\n existing.__vnode = newVNode;\n return;\n }\n\n // Different type — full replace\n removeLogicalNode(parent, existing);\n parent.appendChild(render(newVNode));\n}\n\n// ─── Hydration (SSR) ─────────────────────────────────────\n// SSR renders flat HTML without markers. Hydration attaches reactivity\n// to existing DOM nodes and inserts markers for dynamic blocks.\n\n// Dev-mode hydration mismatch detection\nfunction checkHydrationMismatch(domNode, vnode) {\n if (!__DEV__) return;\n if (!domNode || !vnode || !vnode.__tova) return;\n\n const props = vnode.props || {};\n\n // Check className\n if (props.className !== undefined) {\n const expected = typeof props.className === 'function' ? props.className() : props.className;\n const actual = domNode.className || '';\n if (expected && actual !== expected) {\n console.warn(`Tova hydration mismatch: <${vnode.tag}> class expected \"${expected}\" but got \"${actual}\"`);\n }\n }\n\n // Check attributes\n for (const [key, value] of Object.entries(props)) {\n if (key === 'key' || key === 'ref' || key === 'className' || key.startsWith('on')) continue;\n if (typeof value === 'function') continue; // reactive props — skip static check\n\n if (domNode.getAttribute) {\n const attrName = key === 'className' ? 'class' : key;\n const actual = domNode.getAttribute(attrName);\n const expected = String(value);\n if (actual !== null && actual !== expected) {\n console.warn(`Tova hydration mismatch: <${vnode.tag}> attribute \"${key}\" expected \"${expected}\" but got \"${actual}\"`);\n }\n }\n }\n}\n\n// Check if a DOM node is an SSR marker comment (<!--tova-s:ID-->)\nfunction isSSRMarker(node) {\n return node && node.nodeType === 8 && typeof node.data === 'string' && node.data.startsWith('tova-s:');\n}\n\n// Find the closing SSR marker and collect content nodes between them\nfunction collectSSRMarkerContent(startMarker) {\n const id = startMarker.data.replace('tova-s:', '');\n const closingText = `/tova-s:${id}`;\n const content = [];\n let cursor = startMarker.nextSibling;\n while (cursor) {\n if (cursor.nodeType === 8 && cursor.data === closingText) {\n return { content, endMarker: cursor };\n }\n content.push(cursor);\n cursor = cursor.nextSibling;\n }\n return { content, endMarker: null };\n}\n\nfunction hydrateVNode(domNode, vnode) {\n if (!domNode) return null;\n if (vnode === null || vnode === undefined) return domNode;\n\n // Function vnode (reactive text, JSXIf, JSXFor)\n if (typeof vnode === 'function') {\n if (domNode.nodeType === 3) {\n // Dev-mode: warn if text content differs\n if (__DEV__) {\n const val = vnode();\n const expected = val == null ? '' : String(val);\n if (domNode.textContent !== expected) {\n console.warn(`Tova hydration mismatch: text expected \"${expected}\" but got \"${domNode.textContent}\"`);\n }\n }\n // Reactive text: attach effect to existing text node\n domNode.__tovaReactive = true;\n createEffect(() => {\n const val = vnode();\n const text = val == null ? '' : String(val);\n if (domNode.textContent !== text) domNode.textContent = text;\n });\n return domNode.nextSibling;\n }\n // Complex dynamic block: insert marker-based render, replace SSR node\n const parent = domNode.parentNode;\n const next = domNode.nextSibling;\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return next;\n }\n\n // Primitive text — already correct from SSR\n if (typeof vnode === 'string' || typeof vnode === 'number' || typeof vnode === 'boolean') {\n if (__DEV__ && domNode.nodeType === 3) {\n const expected = String(vnode);\n if (domNode.textContent !== expected) {\n console.warn(`Tova hydration mismatch: text expected \"${expected}\" but got \"${domNode.textContent}\"`);\n }\n }\n return domNode.nextSibling;\n }\n\n // Array\n if (Array.isArray(vnode)) {\n let cursor = domNode;\n for (const child of flattenVNodes(vnode)) {\n if (!cursor) break;\n cursor = hydrateVNode(cursor, child);\n }\n return cursor;\n }\n\n if (!vnode.__tova) return domNode.nextSibling;\n\n // Fragment — children rendered inline in SSR (no wrapper)\n if (vnode.tag === '__fragment') {\n const children = flattenVNodes(vnode.children);\n let cursor = domNode;\n for (const child of children) {\n if (!cursor) break;\n cursor = hydrateVNode(cursor, child);\n }\n return cursor;\n }\n\n // Dynamic node — SSR marker-aware hydration\n if (vnode.tag === '__dynamic' && typeof vnode.compute === 'function') {\n // Check if current domNode is an SSR marker (<!--tova-s:ID-->)\n if (isSSRMarker(domNode)) {\n const { content, endMarker } = collectSSRMarkerContent(domNode);\n const parent = domNode.parentNode;\n\n // Remove SSR markers and content, replace with reactive marker\n const afterEnd = endMarker ? endMarker.nextSibling : null;\n for (const node of content) {\n if (node.parentNode === parent) parent.removeChild(node);\n }\n if (endMarker && endMarker.parentNode === parent) parent.removeChild(endMarker);\n\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return afterEnd;\n }\n\n // No SSR marker — fall back to standard behavior\n const parent = domNode.parentNode;\n const next = domNode.nextSibling;\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return next;\n }\n\n // Element — attach event handlers, reactive props, refs\n if (domNode.nodeType === 1 && domNode.tagName.toLowerCase() === vnode.tag.toLowerCase()) {\n if (__DEV__) checkHydrationMismatch(domNode, vnode);\n hydrateProps(domNode, vnode.props);\n domNode.__vnode = vnode;\n\n const children = flattenVNodes(vnode.children || []);\n let cursor = domNode.firstChild;\n for (const child of children) {\n if (!cursor) break;\n cursor = hydrateVNode(cursor, child);\n }\n return domNode.nextSibling;\n }\n\n // Tag mismatch — fall back to full render\n if (__DEV__) {\n const expectedTag = vnode.tag || '(unknown)';\n const actualTag = domNode.tagName ? domNode.tagName.toLowerCase() : `nodeType:${domNode.nodeType}`;\n console.warn(`Tova hydration mismatch: expected <${expectedTag}> but got <${actualTag}>, falling back to full render`);\n }\n const parent = domNode.parentNode;\n const next = domNode.nextSibling;\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return next;\n}\n\nfunction hydrateProps(el, props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'ref') {\n if (typeof value === 'object' && value !== null && 'current' in value) {\n value.current = el;\n } else if (typeof value === 'function') {\n value(el);\n }\n } else if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n } else if (key === 'key') {\n // Skip\n } else if (typeof value === 'function' && !key.startsWith('on')) {\n createEffect(() => {\n const val = value();\n applyPropValue(el, key, val);\n });\n }\n }\n}\n\nexport function hydrate(component, container) {\n if (!container) {\n console.error('Tova: Hydration target not found');\n return;\n }\n\n const startTime = typeof performance !== 'undefined' ? performance.now() : 0;\n\n const result = createRoot(() => {\n const vnode = typeof component === 'function' ? component() : component;\n if (container.firstChild) {\n hydrateVNode(container.firstChild, vnode);\n }\n });\n\n // Dispatch hydration completion event\n const duration = typeof performance !== 'undefined' ? performance.now() - startTime : 0;\n if (typeof CustomEvent !== 'undefined' && typeof container.dispatchEvent === 'function') {\n container.dispatchEvent(new CustomEvent('tova:hydrated', { detail: { duration }, bubbles: true }));\n }\n\n if (__devtools_hooks && __devtools_hooks.onHydrate) {\n __devtools_hooks.onHydrate({ duration });\n }\n\n return result;\n}\n\nexport function mount(component, container) {\n if (!container) {\n console.error('Tova: Mount target not found');\n return;\n }\n\n const result = createRoot((dispose) => {\n const vnode = typeof component === 'function' ? component() : component;\n container.innerHTML = '';\n container.appendChild(render(vnode));\n return dispose;\n });\n\n if (__devtools_hooks && __devtools_hooks.onMount) {\n __devtools_hooks.onMount();\n }\n\n return result;\n}\n\n// ─── Progressive Hydration ──────────────────────────────────\n// Hydrate a component only when it becomes visible in the viewport.\n\nexport function hydrateWhenVisible(component, domNode, options = {}) {\n if (typeof IntersectionObserver === 'undefined') {\n // Fallback: hydrate immediately\n return hydrate(component, domNode);\n }\n\n const { rootMargin = '200px' } = options;\n let hydrated = false;\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting && !hydrated) {\n hydrated = true;\n observer.disconnect();\n hydrate(component, domNode);\n }\n }\n },\n { rootMargin },\n );\n\n observer.observe(domNode);\n\n return () => {\n observer.disconnect();\n };\n}\n";
3
+ export const REACTIVITY_SOURCE = "// Fine-grained reactivity system for Tova (signals-based)\n\nconst __DEV__ = typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production';\n\n// ─── DevTools hooks (zero-cost when disabled) ────────────\nlet __devtools_hooks = null;\nexport function __enableDevTools(hooks) {\n __devtools_hooks = hooks;\n}\n\nlet currentEffect = null;\nconst effectStack = [];\n\n// ─── Ownership System ─────────────────────────────────────\nlet currentOwner = null;\nconst ownerStack = [];\n\n// ─── Batching ────────────────────────────────────────────\n// Default: synchronous flush after each setter (backward compatible).\n// Inside batch(): effects are deferred and flushed once when batch ends.\n// This means setA(1); setB(2) causes 2 runs by default, but\n// batch(() => { setA(1); setB(2); }) causes only 1 run.\n\nlet pendingEffects = new Set();\nlet batchDepth = 0;\nlet flushing = false;\n\nfunction flush() {\n if (flushing) return; // prevent re-entrant flush\n flushing = true;\n let iterations = 0;\n try {\n while (pendingEffects.size > 0) {\n if (++iterations > 100) {\n console.error('Tova: Possible infinite loop in reactive updates (>100 flush iterations). Aborting.');\n pendingEffects.clear();\n break;\n }\n const toRun = [...pendingEffects];\n pendingEffects.clear();\n for (const effect of toRun) {\n if (!effect._disposed) {\n effect();\n }\n }\n }\n } finally {\n flushing = false;\n }\n}\n\nexport function batch(fn) {\n batchDepth++;\n try {\n fn();\n } finally {\n batchDepth--;\n if (batchDepth === 0) {\n flush();\n }\n }\n}\n\n// ─── Ownership Root ──────────────────────────────────────\n\nexport function createRoot(fn) {\n const root = {\n _children: [],\n _disposed: false,\n _cleanups: [],\n _contexts: null,\n _owner: currentOwner,\n dispose() {\n if (root._disposed) return;\n root._disposed = true;\n // Dispose children in reverse order\n for (let i = root._children.length - 1; i >= 0; i--) {\n const child = root._children[i];\n if (typeof child.dispose === 'function') child.dispose();\n }\n root._children.length = 0;\n // Run cleanups in reverse order\n for (let i = root._cleanups.length - 1; i >= 0; i--) {\n try { root._cleanups[i](); } catch (e) { console.error('Tova: root cleanup error:', e); }\n }\n root._cleanups.length = 0;\n }\n };\n ownerStack.push(currentOwner);\n currentOwner = root;\n try {\n return fn(root.dispose.bind(root));\n } finally {\n currentOwner = ownerStack.pop();\n }\n}\n\n// ─── Dependency Cleanup ──────────────────────────────────\n\nfunction cleanupDeps(subscriber) {\n if (subscriber._deps) {\n for (const depSet of subscriber._deps) {\n depSet.delete(subscriber);\n }\n subscriber._deps.clear();\n }\n}\n\nfunction trackDep(subscriber, subscriberSet) {\n subscriberSet.add(subscriber);\n if (!subscriber._deps) subscriber._deps = new Set();\n subscriber._deps.add(subscriberSet);\n}\n\n// ─── Signals ─────────────────────────────────────────────\n\nexport function createSignal(initialValue, name) {\n let value = initialValue;\n const subscribers = new Set();\n let signalId = null;\n\n if (__devtools_hooks) {\n signalId = __devtools_hooks.onSignalCreate(\n () => value,\n (v) => setter(v),\n name,\n );\n }\n\n function getter() {\n if (currentEffect) {\n trackDep(currentEffect, subscribers);\n }\n return value;\n }\n\n function setter(newValue) {\n if (typeof newValue === 'function') {\n newValue = newValue(value);\n }\n if (value !== newValue) {\n const oldValue = value;\n value = newValue;\n if (__devtools_hooks && signalId != null) {\n __devtools_hooks.onSignalUpdate(signalId, oldValue, newValue);\n }\n for (const sub of [...subscribers]) {\n if (sub._isComputed) {\n sub(); // propagate dirty flags synchronously through computed graph\n } else {\n pendingEffects.add(sub);\n }\n }\n if (batchDepth === 0) {\n flush();\n }\n }\n }\n\n return [getter, setter];\n}\n\n// ─── Effects ─────────────────────────────────────────────\n\nfunction runCleanups(effect) {\n if (effect._cleanup) {\n try { effect._cleanup(); } catch (e) { console.error('Tova: cleanup error:', e); }\n effect._cleanup = null;\n }\n if (effect._cleanups && effect._cleanups.length > 0) {\n for (const cb of effect._cleanups) {\n try { cb(); } catch (e) { console.error('Tova: cleanup error:', e); }\n }\n effect._cleanups = [];\n }\n}\n\nexport function createEffect(fn) {\n function effect() {\n if (effect._running) return;\n if (effect._disposed) return;\n effect._running = true;\n\n // Run cleanups from previous execution\n runCleanups(effect);\n\n // Remove from all previous dependency subscriber sets\n cleanupDeps(effect);\n\n effectStack.push(effect);\n currentEffect = effect;\n const startTime = __devtools_hooks && typeof performance !== 'undefined' ? performance.now() : 0;\n try {\n const result = fn();\n // If effect returns a function, use as cleanup\n if (typeof result === 'function') {\n effect._cleanup = result;\n }\n } catch (e) {\n console.error('Tova: Error in effect:', e);\n if (currentErrorHandler) {\n currentErrorHandler(e);\n }\n } finally {\n if (__devtools_hooks) {\n const duration = typeof performance !== 'undefined' ? performance.now() - startTime : 0;\n __devtools_hooks.onEffectRun(effect, duration);\n }\n effectStack.pop();\n currentEffect = effectStack[effectStack.length - 1] || null;\n effect._running = false;\n }\n }\n\n effect._deps = new Set();\n effect._running = false;\n effect._disposed = false;\n effect._cleanup = null;\n effect._cleanups = [];\n effect._owner = currentOwner;\n\n if (__devtools_hooks) {\n __devtools_hooks.onEffectCreate(effect);\n }\n\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._children.push(effect);\n }\n\n effect.dispose = function () {\n effect._disposed = true;\n runCleanups(effect);\n cleanupDeps(effect);\n pendingEffects.delete(effect);\n // Remove from owner's children\n if (effect._owner) {\n const idx = effect._owner._children.indexOf(effect);\n if (idx >= 0) effect._owner._children.splice(idx, 1);\n }\n };\n\n // Run immediately (synchronous first run)\n effect();\n return effect;\n}\n\n// ─── Computed (lazy/pull-based for glitch-free reads) ────\n\nexport function createComputed(fn) {\n let value;\n let dirty = true;\n const subscribers = new Set();\n\n // notify is called synchronously when a source signal changes.\n // It marks the computed dirty and propagates to downstream subscribers.\n function notify() {\n if (!dirty) {\n dirty = true;\n for (const sub of [...subscribers]) {\n if (sub._isComputed) {\n sub(); // cascade dirty flags synchronously\n } else {\n pendingEffects.add(sub);\n }\n }\n }\n }\n\n notify._deps = new Set();\n notify._disposed = false;\n notify._isComputed = true;\n notify._owner = currentOwner;\n\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._children.push(notify);\n }\n\n notify.dispose = function () {\n notify._disposed = true;\n cleanupDeps(notify);\n if (notify._owner) {\n const idx = notify._owner._children.indexOf(notify);\n if (idx >= 0) notify._owner._children.splice(idx, 1);\n }\n };\n\n function recompute() {\n cleanupDeps(notify);\n\n effectStack.push(notify);\n currentEffect = notify;\n try {\n value = fn();\n dirty = false;\n } finally {\n effectStack.pop();\n currentEffect = effectStack[effectStack.length - 1] || null;\n }\n }\n\n // Initial computation\n recompute();\n\n function getter() {\n if (currentEffect) {\n trackDep(currentEffect, subscribers);\n }\n if (dirty) {\n recompute();\n }\n return value;\n }\n\n return getter;\n}\n\n// ─── Lifecycle Hooks ─────────────────────────────────────\n\nexport function onMount(fn) {\n const owner = currentOwner;\n queueMicrotask(() => {\n const result = fn();\n if (typeof result === 'function' && owner && !owner._disposed) {\n owner._cleanups.push(result);\n }\n });\n}\n\nexport function onUnmount(fn) {\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._cleanups.push(fn);\n }\n}\n\nexport function onCleanup(fn) {\n if (currentEffect) {\n if (!currentEffect._cleanups) currentEffect._cleanups = [];\n currentEffect._cleanups.push(fn);\n }\n}\n\n// ─── Untrack ─────────────────────────────────────────────\n// Run a function without tracking any signal reads (opt out of reactivity)\n\nexport function untrack(fn) {\n const prev = currentEffect;\n currentEffect = null;\n try {\n return fn();\n } finally {\n currentEffect = prev;\n }\n}\n\n// ─── Watch ───────────────────────────────────────────────\n// Watch a reactive expression, calling callback with (newValue, oldValue)\n// Returns a dispose function to stop watching.\n\nexport function watch(getter, callback, options = {}) {\n let oldValue = undefined;\n let initialized = false;\n\n const effect = createEffect(() => {\n const newValue = getter();\n if (initialized) {\n untrack(() => callback(newValue, oldValue));\n } else if (options.immediate) {\n untrack(() => callback(newValue, undefined));\n }\n oldValue = newValue;\n initialized = true;\n });\n\n return effect.dispose ? effect.dispose.bind(effect) : () => {\n effect._disposed = true;\n runCleanups(effect);\n cleanupDeps(effect);\n pendingEffects.delete(effect);\n };\n}\n\n// ─── Refs ────────────────────────────────────────────────\n\nexport function createRef(initialValue) {\n return { current: initialValue !== undefined ? initialValue : null };\n}\n\n// ─── Error Boundaries ────────────────────────────────────\n\n// Stack-based error handler for correct nested boundary propagation\nconst errorHandlerStack = [];\nlet currentErrorHandler = null;\n\nfunction pushErrorHandler(handler) {\n errorHandlerStack.push(currentErrorHandler);\n currentErrorHandler = handler;\n}\n\nfunction popErrorHandler() {\n currentErrorHandler = errorHandlerStack.pop() || null;\n}\n\n// Component name tracking for stack traces\nconst componentNameStack = [];\n\nexport function pushComponentName(name) {\n componentNameStack.push(name);\n}\n\nexport function popComponentName() {\n componentNameStack.pop();\n}\n\nfunction buildComponentStack() {\n return [...componentNameStack].reverse();\n}\n\nexport function createErrorBoundary(options = {}) {\n const { onError, onReset } = options;\n const [error, setError] = createSignal(null);\n\n function run(fn) {\n pushErrorHandler((e) => {\n const stack = buildComponentStack();\n if (e && typeof e === 'object') e.__tovaComponentStack = stack;\n setError(e);\n if (onError) onError({ error: e, componentStack: stack });\n });\n try {\n return fn();\n } catch (e) {\n const stack = buildComponentStack();\n if (e && typeof e === 'object') e.__tovaComponentStack = stack;\n setError(e);\n if (onError) onError({ error: e, componentStack: stack });\n return null;\n } finally {\n popErrorHandler();\n }\n }\n\n function reset() {\n setError(null);\n if (onReset) onReset();\n }\n\n return { error, run, reset };\n}\n\nexport function ErrorBoundary({ fallback, children, onError, onReset, retry = 0 }) {\n const [error, setError] = createSignal(null);\n const [retryCount, setRetryCount] = createSignal(0);\n\n function handleError(e) {\n const stack = buildComponentStack();\n if (e && typeof e === 'object') e.__tovaComponentStack = stack;\n if (retryCount() < retry) {\n setRetryCount(c => c + 1);\n setError(null); // clear to re-trigger render\n return;\n }\n setError(e);\n if (onError) onError({ error: e, componentStack: stack });\n }\n\n // Return a reactive wrapper that switches between children and fallback\n const childContent = children && children.length === 1 ? children[0] : tova_fragment(children || []);\n\n const vnode = {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n _fallback: fallback,\n _componentName: 'ErrorBoundary',\n _errorHandler: handleError, // Active during __dynamic effect render cycle\n compute: () => {\n const err = error();\n if (err) {\n // Render fallback — if fallback itself throws, propagate to parent boundary\n try {\n return typeof fallback === 'function'\n ? fallback({\n error: err,\n reset: () => {\n setRetryCount(0);\n setError(null);\n if (onReset) onReset();\n },\n })\n : fallback;\n } catch (fallbackError) {\n // Fallback threw — propagate to parent error boundary\n if (currentErrorHandler) {\n currentErrorHandler(fallbackError);\n }\n return null;\n }\n }\n return childContent;\n },\n };\n\n return vnode;\n}\n\n// ─── Dynamic Component ──────────────────────────────────\n// Renders a component dynamically based on a reactive signal.\n// Usage: Dynamic({ component: mySignal, ...props })\n\nexport function Dynamic({ component, ...rest }) {\n return {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n compute: () => {\n const comp = typeof component === 'function' && !component.__tova ? component() : component;\n if (!comp) return null;\n if (typeof comp === 'function') {\n return comp(rest);\n }\n return comp;\n },\n };\n}\n\n// ─── Portal ─────────────────────────────────────────────\n// Renders children into a different DOM target.\n// Usage: Portal({ target: \"#modal-root\", children })\n\nexport function Portal({ target, children }) {\n return {\n __tova: true,\n tag: '__portal',\n props: { target },\n children: children || [],\n };\n}\n\n// ─── Lazy ───────────────────────────────────────────────\n// Async component loading with optional fallback.\n// Usage: const LazyComp = lazy(() => import('./HeavyComponent.js'))\n\nexport function lazy(loader) {\n let resolved = null;\n let loadError = null;\n let promise = null;\n\n return function LazyWrapper(props) {\n if (resolved) {\n return resolved(props);\n }\n\n if (!promise) {\n promise = loader()\n .then(mod => {\n resolved = mod.default || mod;\n })\n .catch(e => { loadError = e; });\n }\n\n const [tick, setTick] = createSignal(0);\n\n // Trigger re-render when promise settles\n promise.then(() => setTick(1)).catch(() => setTick(1));\n\n return {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n compute: () => {\n tick(); // Track for reactivity\n if (loadError) return tova_el('span', { className: 'tova-error' }, [String(loadError)]);\n if (resolved) return resolved(props);\n // Fallback while loading\n return props && props.fallback ? props.fallback : null;\n },\n };\n };\n}\n\n// ─── Context (Provide/Inject) ────────────────────────────\n// Tree-based: values are stored on the ownership tree, inject walks up.\n\nexport function createContext(defaultValue) {\n const id = Symbol('context');\n return { _id: id, _default: defaultValue };\n}\n\nexport function provide(context, value) {\n const owner = currentOwner;\n if (owner) {\n if (!owner._contexts) owner._contexts = new Map();\n owner._contexts.set(context._id, value);\n }\n}\n\nexport function inject(context) {\n let owner = currentOwner;\n while (owner) {\n if (owner._contexts && owner._contexts.has(context._id)) {\n return owner._contexts.get(context._id);\n }\n owner = owner._owner;\n }\n return context._default;\n}\n\n// ─── DOM Rendering ────────────────────────────────────────\n\n// Inject scoped CSS into the page (idempotent — only injects once per id)\nconst __tovaInjectedStyles = new Set();\nexport function tova_inject_css(id, css) {\n if (__tovaInjectedStyles.has(id)) return;\n __tovaInjectedStyles.add(id);\n const style = document.createElement('style');\n style.setAttribute('data-tova-style', id);\n style.textContent = css;\n document.head.appendChild(style);\n}\n\nexport function tova_el(tag, props = {}, children = []) {\n return { __tova: true, tag, props, children };\n}\n\nexport function tova_fragment(children) {\n return { __tova: true, tag: '__fragment', props: {}, children };\n}\n\n// Inject a key prop into a vnode for keyed reconciliation\nexport function tova_keyed(key, vnode) {\n if (vnode && vnode.__tova) {\n vnode.props = { ...vnode.props, key };\n }\n return vnode;\n}\n\n// Flatten nested arrays and vnodes into a flat list of vnodes\nfunction flattenVNodes(children) {\n const result = [];\n for (const child of children) {\n if (child === null || child === undefined) {\n continue;\n } else if (Array.isArray(child)) {\n result.push(...flattenVNodes(child));\n } else {\n result.push(child);\n }\n }\n return result;\n}\n\n// ─── Marker-based DOM helpers ─────────────────────────────\n// Instead of wrapping dynamic blocks/fragments in <span style=\"display:contents\">,\n// we use comment node markers. A marker's __tovaNodes tracks its content nodes.\n// Content nodes have __tovaOwner pointing to their owning marker.\n\n// Recursively dispose ownership roots attached to a DOM subtree\nfunction disposeNode(node) {\n if (!node) return;\n if (node.__tovaRoot) {\n node.__tovaRoot();\n node.__tovaRoot = null;\n }\n // If this is a marker, dispose and remove its content nodes\n if (node.__tovaNodes) {\n for (const cn of node.__tovaNodes) {\n disposeNode(cn);\n if (cn.parentNode) cn.parentNode.removeChild(cn);\n }\n node.__tovaNodes = [];\n }\n if (node.childNodes) {\n for (const child of Array.from(node.childNodes)) {\n disposeNode(child);\n }\n }\n}\n\n// Check if a node is transitively owned by a marker (walks __tovaOwner chain)\nfunction isOwnedBy(node, marker) {\n let owner = node.__tovaOwner;\n while (owner) {\n if (owner === marker) return true;\n owner = owner.__tovaOwner;\n }\n return false;\n}\n\n// Get logical children of a parent element (skips marker content nodes)\nfunction getLogicalChildren(parent) {\n const logical = [];\n for (let i = 0; i < parent.childNodes.length; i++) {\n const node = parent.childNodes[i];\n if (!node.__tovaOwner) {\n logical.push(node);\n }\n }\n return logical;\n}\n\n// Find the first DOM sibling after all of a marker's content\nfunction nextSiblingAfterMarker(marker) {\n if (!marker.__tovaNodes || marker.__tovaNodes.length === 0) {\n return marker.nextSibling;\n }\n let last = marker.__tovaNodes[marker.__tovaNodes.length - 1];\n // If last content is itself a marker, recurse to find physical end\n while (last && last.__tovaNodes && last.__tovaNodes.length > 0) {\n last = last.__tovaNodes[last.__tovaNodes.length - 1];\n }\n return last ? last.nextSibling : marker.nextSibling;\n}\n\n// Remove a logical node (marker + its content, or a regular node) from the DOM\nfunction removeLogicalNode(parent, node) {\n disposeNode(node);\n if (node.parentNode === parent) parent.removeChild(node);\n}\n\n// Insert rendered result (could be single node or DocumentFragment) before ref,\n// setting __tovaOwner on top-level inserted nodes. Returns array of inserted nodes.\nfunction insertRendered(parent, rendered, ref, owner) {\n if (rendered.nodeType === 11) {\n const nodes = Array.from(rendered.childNodes);\n for (const n of nodes) {\n if (!n.__tovaOwner) n.__tovaOwner = owner;\n }\n parent.insertBefore(rendered, ref);\n return nodes;\n }\n if (!rendered.__tovaOwner) rendered.__tovaOwner = owner;\n parent.insertBefore(rendered, ref);\n return [rendered];\n}\n\n// Clear a marker's content from the DOM and reset __tovaNodes\nfunction clearMarkerContent(marker) {\n for (const node of marker.__tovaNodes) {\n disposeNode(node);\n if (node.parentNode) node.parentNode.removeChild(node);\n }\n marker.__tovaNodes = [];\n}\n\n// ─── Render ───────────────────────────────────────────────\n\n// Create real DOM nodes from a vnode (with fine-grained reactive bindings).\n// Returns a single DOM node for elements/text, or a DocumentFragment for\n// markers (dynamic blocks, fragments) containing [marker, ...content].\nexport function render(vnode) {\n if (vnode === null || vnode === undefined) {\n return document.createTextNode('');\n }\n\n // Reactive dynamic block (JSXIf, JSXFor, reactive text, etc.)\n if (typeof vnode === 'function') {\n const marker = document.createComment('');\n marker.__tovaDynamic = true;\n marker.__tovaNodes = [];\n\n const frag = document.createDocumentFragment();\n frag.appendChild(marker);\n\n createEffect(() => {\n const val = vnode();\n const parent = marker.parentNode;\n const ref = nextSiblingAfterMarker(marker);\n\n // Array: keyed or positional reconciliation within marker range\n if (Array.isArray(val)) {\n const flat = flattenVNodes(val);\n const hasKeys = flat.some(c => getKey(c) != null);\n if (hasKeys) {\n patchKeyedInMarker(marker, flat);\n } else {\n patchPositionalInMarker(marker, flat);\n }\n return;\n }\n\n // Text: optimize single text node update in place\n if (val == null || typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {\n const text = val == null ? '' : String(val);\n if (marker.__tovaNodes.length === 1 && marker.__tovaNodes[0].nodeType === 3) {\n if (marker.__tovaNodes[0].textContent !== text) {\n marker.__tovaNodes[0].textContent = text;\n }\n return;\n }\n clearMarkerContent(marker);\n const textNode = document.createTextNode(text);\n textNode.__tovaOwner = marker;\n parent.insertBefore(textNode, ref);\n marker.__tovaNodes = [textNode];\n return;\n }\n\n // Vnode or other: clear and re-render\n clearMarkerContent(marker);\n if (val && val.__tova) {\n const rendered = render(val);\n marker.__tovaNodes = insertRendered(parent, rendered, ref, marker);\n } else {\n const textNode = document.createTextNode(String(val));\n textNode.__tovaOwner = marker;\n parent.insertBefore(textNode, ref);\n marker.__tovaNodes = [textNode];\n }\n });\n\n return frag;\n }\n\n if (typeof vnode === 'string' || typeof vnode === 'number' || typeof vnode === 'boolean') {\n return document.createTextNode(String(vnode));\n }\n\n if (Array.isArray(vnode)) {\n const fragment = document.createDocumentFragment();\n for (const child of vnode) {\n fragment.appendChild(render(child));\n }\n return fragment;\n }\n\n if (!vnode.__tova) {\n return document.createTextNode(String(vnode));\n }\n\n // Fragment — marker + children (no wrapper element)\n if (vnode.tag === '__fragment') {\n const marker = document.createComment('');\n marker.__tovaFragment = true;\n marker.__tovaNodes = [];\n marker.__vnode = vnode;\n\n const frag = document.createDocumentFragment();\n frag.appendChild(marker);\n\n for (const child of flattenVNodes(vnode.children)) {\n const rendered = render(child);\n const inserted = insertRendered(frag, rendered, null, marker);\n marker.__tovaNodes.push(...inserted);\n }\n\n return frag;\n }\n\n // Dynamic reactive node (ErrorBoundary, Dynamic component, etc.)\n if (vnode.tag === '__dynamic' && typeof vnode.compute === 'function') {\n const marker = document.createComment('');\n marker.__tovaDynamic = true;\n marker.__tovaNodes = [];\n\n const frag = document.createDocumentFragment();\n frag.appendChild(marker);\n\n let prevDispose = null;\n const errHandler = vnode._errorHandler || null;\n createEffect(() => {\n if (errHandler) pushErrorHandler(errHandler);\n try {\n const inner = vnode.compute();\n const parent = marker.parentNode;\n const ref = nextSiblingAfterMarker(marker);\n\n if (prevDispose) {\n prevDispose();\n prevDispose = null;\n }\n clearMarkerContent(marker);\n\n createRoot((dispose) => {\n prevDispose = dispose;\n const rendered = render(inner);\n marker.__tovaNodes = insertRendered(parent, rendered, ref, marker);\n });\n } catch (e) {\n if (errHandler) {\n errHandler(e);\n } else if (currentErrorHandler) {\n currentErrorHandler(e);\n } else {\n console.error('Uncaught error during render:', e);\n }\n } finally {\n if (errHandler) popErrorHandler();\n }\n });\n\n return frag;\n }\n\n // Portal — render children into a different DOM target\n if (vnode.tag === '__portal') {\n const placeholder = document.createComment('portal');\n const targetSelector = vnode.props.target;\n queueMicrotask(() => {\n const targetEl = typeof targetSelector === 'string'\n ? document.querySelector(targetSelector)\n : targetSelector;\n if (targetEl) {\n for (const child of flattenVNodes(vnode.children)) {\n targetEl.appendChild(render(child));\n }\n }\n });\n return placeholder;\n }\n\n // Element\n const el = document.createElement(vnode.tag);\n applyReactiveProps(el, vnode.props);\n\n // Set data-tova-component attribute for DevTools\n if (vnode._componentName) {\n el.setAttribute('data-tova-component', vnode._componentName);\n if (__devtools_hooks && __devtools_hooks.onComponentRender) {\n __devtools_hooks.onComponentRender(vnode._componentName, el, 0);\n }\n }\n\n // Render children\n for (const child of flattenVNodes(vnode.children)) {\n el.appendChild(render(child));\n }\n\n // Store vnode reference for patching\n el.__vnode = vnode;\n\n return el;\n}\n\n// Apply reactive props — function-valued props get their own effect\nfunction applyReactiveProps(el, props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'ref') {\n if (typeof value === 'object' && value !== null && 'current' in value) {\n value.current = el;\n } else if (typeof value === 'function') {\n value(el);\n }\n } else if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n } else if (key === 'key') {\n // Skip\n } else if (typeof value === 'function' && !key.startsWith('on')) {\n // Reactive prop — create effect for fine-grained updates\n createEffect(() => {\n const val = value();\n applyPropValue(el, key, val);\n });\n } else {\n applyPropValue(el, key, value);\n }\n }\n}\n\nfunction applyPropValue(el, key, val) {\n if (key === 'className') {\n if (el.className !== val) el.className = val || '';\n } else if (key === 'innerHTML' || key === 'dangerouslySetInnerHTML') {\n const html = typeof val === 'object' && val !== null ? val.__html || '' : val || '';\n if (el.innerHTML !== html) el.innerHTML = html;\n } else if (key === 'value') {\n if (el !== document.activeElement && el.value !== val) {\n el.value = val;\n }\n } else if (key === 'checked') {\n el.checked = !!val;\n } else if (key === 'disabled' || key === 'readOnly' || key === 'hidden') {\n el[key] = !!val;\n } else if (key === 'style' && typeof val === 'object') {\n // Clear old properties not present in new style object\n for (let i = el.style.length - 1; i >= 0; i--) {\n const prop = el.style[i];\n const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());\n if (!(prop in val) && !(camel in val)) {\n el.style.removeProperty(prop);\n }\n }\n Object.assign(el.style, val);\n } else {\n const s = val == null ? '' : String(val);\n if (el.getAttribute(key) !== s) {\n el.setAttribute(key, s);\n }\n }\n}\n\n// Apply/update props on a DOM element (used by patcher for full-tree mode)\nfunction applyProps(el, newProps, oldProps) {\n // Remove old props that are no longer present\n for (const key of Object.keys(oldProps)) {\n if (!(key in newProps)) {\n if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n if (el.__handlers && el.__handlers[eventName]) {\n el.removeEventListener(eventName, el.__handlers[eventName]);\n delete el.__handlers[eventName];\n }\n } else if (key === 'className') {\n el.className = '';\n } else if (key === 'style') {\n el.removeAttribute('style');\n } else {\n el.removeAttribute(key);\n }\n }\n }\n\n // Apply new props\n for (const [key, value] of Object.entries(newProps)) {\n if (key === 'className') {\n const val = typeof value === 'function' ? value() : value;\n if (el.className !== val) el.className = val;\n } else if (key === 'ref') {\n if (typeof value === 'object' && value !== null && 'current' in value) {\n value.current = el;\n } else if (typeof value === 'function') {\n value(el);\n }\n } else if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n const oldHandler = el.__handlers && el.__handlers[eventName];\n if (oldHandler !== value) {\n if (oldHandler) el.removeEventListener(eventName, oldHandler);\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n }\n } else if (key === 'style' && typeof value === 'object') {\n Object.assign(el.style, value);\n } else if (key === 'key') {\n // Skip\n } else if (key === 'value') {\n const val = typeof value === 'function' ? value() : value;\n if (el !== document.activeElement && el.value !== val) {\n el.value = val;\n }\n } else if (key === 'checked') {\n el.checked = !!value;\n } else {\n const val = typeof value === 'function' ? value() : value;\n if (el.getAttribute(key) !== String(val)) {\n el.setAttribute(key, val);\n }\n }\n }\n}\n\n// ─── Keyed Reconciliation ────────────────────────────────\n\nfunction getKey(vnode) {\n if (vnode && vnode.__tova && vnode.props) return vnode.props.key;\n return undefined;\n}\n\nfunction getNodeKey(node) {\n if (node && node.__vnode && node.__vnode.props) return node.__vnode.props.key;\n return undefined;\n}\n\n// Keyed reconciliation within a marker's content range\nfunction patchKeyedInMarker(marker, newVNodes) {\n const parent = marker.parentNode;\n const oldNodes = [...marker.__tovaNodes];\n const oldKeyMap = new Map();\n\n for (const node of oldNodes) {\n const key = getNodeKey(node);\n if (key != null) oldKeyMap.set(key, node);\n }\n\n const newNodes = [];\n const usedOld = new Set();\n\n for (const newChild of newVNodes) {\n const key = getKey(newChild);\n\n if (key != null && oldKeyMap.has(key)) {\n const oldNode = oldKeyMap.get(key);\n usedOld.add(oldNode);\n\n if (oldNode.nodeType === 1 && newChild.__tova &&\n oldNode.tagName.toLowerCase() === newChild.tag.toLowerCase()) {\n const oldVNode = oldNode.__vnode || { props: {}, children: [] };\n applyProps(oldNode, newChild.props, oldVNode.props);\n patchChildrenOfElement(oldNode, flattenVNodes(newChild.children));\n oldNode.__vnode = newChild;\n newNodes.push(oldNode);\n } else {\n const node = render(newChild);\n // render may return Fragment — collect nodes\n if (node.nodeType === 11) {\n const nodes = Array.from(node.childNodes);\n for (const n of nodes) { if (!n.__tovaOwner) n.__tovaOwner = marker; }\n parent.insertBefore(node, nextSiblingAfterMarker(marker));\n newNodes.push(...nodes);\n } else {\n if (!node.__tovaOwner) node.__tovaOwner = marker;\n newNodes.push(node);\n }\n }\n } else {\n const node = render(newChild);\n if (node.nodeType === 11) {\n const nodes = Array.from(node.childNodes);\n for (const n of nodes) { if (!n.__tovaOwner) n.__tovaOwner = marker; }\n parent.insertBefore(node, nextSiblingAfterMarker(marker));\n newNodes.push(...nodes);\n } else {\n if (!node.__tovaOwner) node.__tovaOwner = marker;\n newNodes.push(node);\n }\n }\n }\n\n // Remove unused old nodes\n for (const node of oldNodes) {\n if (!usedOld.has(node)) {\n disposeNode(node);\n if (node.parentNode === parent) parent.removeChild(node);\n }\n }\n\n // Arrange in correct order after marker using cursor approach\n let cursor = marker.nextSibling;\n for (const node of newNodes) {\n if (node === cursor) {\n cursor = node.nextSibling;\n } else {\n parent.insertBefore(node, cursor);\n }\n }\n\n marker.__tovaNodes = newNodes;\n}\n\n// Positional reconciliation within a marker's content range\nfunction patchPositionalInMarker(marker, newChildren) {\n const parent = marker.parentNode;\n const oldNodes = [...marker.__tovaNodes];\n const oldCount = oldNodes.length;\n const newCount = newChildren.length;\n\n // Patch in place\n const patchCount = Math.min(oldCount, newCount);\n for (let i = 0; i < patchCount; i++) {\n patchSingle(parent, oldNodes[i], newChildren[i]);\n }\n\n // Append new children\n const ref = nextSiblingAfterMarker(marker);\n for (let i = oldCount; i < newCount; i++) {\n const rendered = render(newChildren[i]);\n const inserted = insertRendered(parent, rendered, ref, marker);\n oldNodes.push(...inserted);\n }\n\n // Remove excess children\n for (let i = newCount; i < oldCount; i++) {\n disposeNode(oldNodes[i]);\n if (oldNodes[i].parentNode === parent) parent.removeChild(oldNodes[i]);\n }\n\n marker.__tovaNodes = oldNodes.slice(0, newCount);\n}\n\n// Keyed reconciliation for children of an element (not marker-based)\nfunction patchKeyedChildren(parent, newVNodes) {\n const logical = getLogicalChildren(parent);\n const oldKeyMap = new Map();\n\n for (const node of logical) {\n const key = getNodeKey(node);\n if (key != null) oldKeyMap.set(key, node);\n }\n\n const newNodes = [];\n const usedOld = new Set();\n\n for (const newChild of newVNodes) {\n const key = getKey(newChild);\n\n if (key != null && oldKeyMap.has(key)) {\n const oldNode = oldKeyMap.get(key);\n usedOld.add(oldNode);\n\n if (oldNode.nodeType === 1 && newChild.__tova &&\n oldNode.tagName.toLowerCase() === newChild.tag.toLowerCase()) {\n const oldVNode = oldNode.__vnode || { props: {}, children: [] };\n applyProps(oldNode, newChild.props, oldVNode.props);\n patchChildrenOfElement(oldNode, flattenVNodes(newChild.children));\n oldNode.__vnode = newChild;\n newNodes.push(oldNode);\n } else {\n newNodes.push(render(newChild));\n }\n } else {\n newNodes.push(render(newChild));\n }\n }\n\n // Remove unused old logical nodes\n for (const node of logical) {\n if (!usedOld.has(node) && node.parentNode === parent) {\n removeLogicalNode(parent, node);\n }\n }\n\n // Arrange in correct order\n for (let i = 0; i < newNodes.length; i++) {\n const expected = newNodes[i];\n const logicalNow = getLogicalChildren(parent);\n const current = logicalNow[i];\n if (current !== expected) {\n parent.insertBefore(expected, current || null);\n }\n }\n}\n\n// Positional reconciliation for children of an element\nfunction patchPositionalChildren(parent, newChildren) {\n const logical = getLogicalChildren(parent);\n const oldCount = logical.length;\n const newCount = newChildren.length;\n\n for (let i = 0; i < Math.min(oldCount, newCount); i++) {\n patchSingle(parent, logical[i], newChildren[i]);\n }\n\n for (let i = oldCount; i < newCount; i++) {\n parent.appendChild(render(newChildren[i]));\n }\n\n // Remove excess logical children\n const currentLogical = getLogicalChildren(parent);\n while (currentLogical.length > newCount) {\n const node = currentLogical.pop();\n removeLogicalNode(parent, node);\n }\n}\n\n// Patch children of a regular element\nfunction patchChildrenOfElement(el, newChildren) {\n const hasKeys = newChildren.some(c => getKey(c) != null);\n if (hasKeys) {\n patchKeyedChildren(el, newChildren);\n } else {\n patchPositionalChildren(el, newChildren);\n }\n}\n\n// Patch a single logical node in place\nfunction patchSingle(parent, existing, newVNode) {\n if (!existing) {\n parent.appendChild(render(newVNode));\n return;\n }\n\n if (newVNode === null || newVNode === undefined) {\n removeLogicalNode(parent, existing);\n return;\n }\n\n // Function vnode — replace with new dynamic block\n if (typeof newVNode === 'function') {\n const rendered = render(newVNode);\n if (existing.__tovaNodes) {\n // Existing is a marker — clear its content and replace\n clearMarkerContent(existing);\n parent.replaceChild(rendered, existing);\n } else {\n disposeNode(existing);\n parent.replaceChild(rendered, existing);\n }\n return;\n }\n\n // Text\n if (typeof newVNode === 'string' || typeof newVNode === 'number' || typeof newVNode === 'boolean') {\n const text = String(newVNode);\n if (existing.nodeType === 3) {\n if (existing.textContent !== text) existing.textContent = text;\n } else {\n removeLogicalNode(parent, existing);\n parent.insertBefore(document.createTextNode(text), null);\n }\n return;\n }\n\n if (!newVNode.__tova) {\n const text = String(newVNode);\n if (existing.nodeType === 3) {\n if (existing.textContent !== text) existing.textContent = text;\n } else {\n removeLogicalNode(parent, existing);\n parent.insertBefore(document.createTextNode(text), null);\n }\n return;\n }\n\n // Fragment — patch marker content\n if (newVNode.tag === '__fragment') {\n if (existing.__tovaFragment) {\n // Patch children within the marker range\n const oldNodes = [...existing.__tovaNodes];\n const newChildren = flattenVNodes(newVNode.children);\n // Simple approach: clear and re-render fragment content\n clearMarkerContent(existing);\n const ref = nextSiblingAfterMarker(existing);\n for (const child of newChildren) {\n const rendered = render(child);\n const inserted = insertRendered(parent, rendered, ref, existing);\n existing.__tovaNodes.push(...inserted);\n }\n existing.__vnode = newVNode;\n return;\n }\n removeLogicalNode(parent, existing);\n parent.appendChild(render(newVNode));\n return;\n }\n\n // Element — patch in place\n if (existing.nodeType === 1 && newVNode.tag &&\n existing.tagName.toLowerCase() === newVNode.tag.toLowerCase()) {\n const oldVNode = existing.__vnode || { props: {}, children: [] };\n applyProps(existing, newVNode.props, oldVNode.props);\n patchChildrenOfElement(existing, flattenVNodes(newVNode.children));\n existing.__vnode = newVNode;\n return;\n }\n\n // Different type — full replace\n removeLogicalNode(parent, existing);\n parent.appendChild(render(newVNode));\n}\n\n// ─── Hydration (SSR) ─────────────────────────────────────\n// SSR renders flat HTML without markers. Hydration attaches reactivity\n// to existing DOM nodes and inserts markers for dynamic blocks.\n\n// Dev-mode hydration mismatch detection\nfunction checkHydrationMismatch(domNode, vnode) {\n if (!__DEV__) return;\n if (!domNode || !vnode || !vnode.__tova) return;\n\n const props = vnode.props || {};\n\n // Check className\n if (props.className !== undefined) {\n const expected = typeof props.className === 'function' ? props.className() : props.className;\n const actual = domNode.className || '';\n if (expected && actual !== expected) {\n console.warn(`Tova hydration mismatch: <${vnode.tag}> class expected \"${expected}\" but got \"${actual}\"`);\n }\n }\n\n // Check attributes\n for (const [key, value] of Object.entries(props)) {\n if (key === 'key' || key === 'ref' || key === 'className' || key.startsWith('on')) continue;\n if (typeof value === 'function') continue; // reactive props — skip static check\n\n if (domNode.getAttribute) {\n const attrName = key === 'className' ? 'class' : key;\n const actual = domNode.getAttribute(attrName);\n const expected = String(value);\n if (actual !== null && actual !== expected) {\n console.warn(`Tova hydration mismatch: <${vnode.tag}> attribute \"${key}\" expected \"${expected}\" but got \"${actual}\"`);\n }\n }\n }\n}\n\n// Check if a DOM node is an SSR marker comment (<!--tova-s:ID-->)\nfunction isSSRMarker(node) {\n return node && node.nodeType === 8 && typeof node.data === 'string' && node.data.startsWith('tova-s:');\n}\n\n// Find the closing SSR marker and collect content nodes between them\nfunction collectSSRMarkerContent(startMarker) {\n const id = startMarker.data.replace('tova-s:', '');\n const closingText = `/tova-s:${id}`;\n const content = [];\n let cursor = startMarker.nextSibling;\n while (cursor) {\n if (cursor.nodeType === 8 && cursor.data === closingText) {\n return { content, endMarker: cursor };\n }\n content.push(cursor);\n cursor = cursor.nextSibling;\n }\n return { content, endMarker: null };\n}\n\nfunction hydrateVNode(domNode, vnode) {\n if (!domNode) return null;\n if (vnode === null || vnode === undefined) return domNode;\n\n // Function vnode (reactive text, JSXIf, JSXFor)\n if (typeof vnode === 'function') {\n if (domNode.nodeType === 3) {\n // Dev-mode: warn if text content differs\n if (__DEV__) {\n const val = vnode();\n const expected = val == null ? '' : String(val);\n if (domNode.textContent !== expected) {\n console.warn(`Tova hydration mismatch: text expected \"${expected}\" but got \"${domNode.textContent}\"`);\n }\n }\n // Reactive text: attach effect to existing text node\n domNode.__tovaReactive = true;\n createEffect(() => {\n const val = vnode();\n const text = val == null ? '' : String(val);\n if (domNode.textContent !== text) domNode.textContent = text;\n });\n return domNode.nextSibling;\n }\n // Complex dynamic block: insert marker-based render, replace SSR node\n const parent = domNode.parentNode;\n const next = domNode.nextSibling;\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return next;\n }\n\n // Primitive text — already correct from SSR\n if (typeof vnode === 'string' || typeof vnode === 'number' || typeof vnode === 'boolean') {\n if (__DEV__ && domNode.nodeType === 3) {\n const expected = String(vnode);\n if (domNode.textContent !== expected) {\n console.warn(`Tova hydration mismatch: text expected \"${expected}\" but got \"${domNode.textContent}\"`);\n }\n }\n return domNode.nextSibling;\n }\n\n // Array\n if (Array.isArray(vnode)) {\n let cursor = domNode;\n for (const child of flattenVNodes(vnode)) {\n if (!cursor) break;\n cursor = hydrateVNode(cursor, child);\n }\n return cursor;\n }\n\n if (!vnode.__tova) return domNode.nextSibling;\n\n // Fragment — children rendered inline in SSR (no wrapper)\n if (vnode.tag === '__fragment') {\n const children = flattenVNodes(vnode.children);\n let cursor = domNode;\n for (const child of children) {\n if (!cursor) break;\n cursor = hydrateVNode(cursor, child);\n }\n return cursor;\n }\n\n // Dynamic node — SSR marker-aware hydration\n if (vnode.tag === '__dynamic' && typeof vnode.compute === 'function') {\n // Check if current domNode is an SSR marker (<!--tova-s:ID-->)\n if (isSSRMarker(domNode)) {\n const { content, endMarker } = collectSSRMarkerContent(domNode);\n const parent = domNode.parentNode;\n\n // Remove SSR markers and content, replace with reactive marker\n const afterEnd = endMarker ? endMarker.nextSibling : null;\n for (const node of content) {\n if (node.parentNode === parent) parent.removeChild(node);\n }\n if (endMarker && endMarker.parentNode === parent) parent.removeChild(endMarker);\n\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return afterEnd;\n }\n\n // No SSR marker — fall back to standard behavior\n const parent = domNode.parentNode;\n const next = domNode.nextSibling;\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return next;\n }\n\n // Element — attach event handlers, reactive props, refs\n if (domNode.nodeType === 1 && domNode.tagName.toLowerCase() === vnode.tag.toLowerCase()) {\n if (__DEV__) checkHydrationMismatch(domNode, vnode);\n hydrateProps(domNode, vnode.props);\n domNode.__vnode = vnode;\n\n const children = flattenVNodes(vnode.children || []);\n let cursor = domNode.firstChild;\n for (const child of children) {\n if (!cursor) break;\n cursor = hydrateVNode(cursor, child);\n }\n return domNode.nextSibling;\n }\n\n // Tag mismatch — fall back to full render\n if (__DEV__) {\n const expectedTag = vnode.tag || '(unknown)';\n const actualTag = domNode.tagName ? domNode.tagName.toLowerCase() : `nodeType:${domNode.nodeType}`;\n console.warn(`Tova hydration mismatch: expected <${expectedTag}> but got <${actualTag}>, falling back to full render`);\n }\n const parent = domNode.parentNode;\n const next = domNode.nextSibling;\n const rendered = render(vnode);\n parent.replaceChild(rendered, domNode);\n return next;\n}\n\nfunction hydrateProps(el, props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'ref') {\n if (typeof value === 'object' && value !== null && 'current' in value) {\n value.current = el;\n } else if (typeof value === 'function') {\n value(el);\n }\n } else if (key.startsWith('on')) {\n const eventName = key.slice(2).toLowerCase();\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n } else if (key === 'key') {\n // Skip\n } else if (typeof value === 'function' && !key.startsWith('on')) {\n createEffect(() => {\n const val = value();\n applyPropValue(el, key, val);\n });\n }\n }\n}\n\nexport function hydrate(component, container) {\n if (!container) {\n console.error('Tova: Hydration target not found');\n return;\n }\n\n const startTime = typeof performance !== 'undefined' ? performance.now() : 0;\n\n const result = createRoot(() => {\n const vnode = typeof component === 'function' ? component() : component;\n if (container.firstChild) {\n hydrateVNode(container.firstChild, vnode);\n }\n });\n\n // Dispatch hydration completion event\n const duration = typeof performance !== 'undefined' ? performance.now() - startTime : 0;\n if (typeof CustomEvent !== 'undefined' && typeof container.dispatchEvent === 'function') {\n container.dispatchEvent(new CustomEvent('tova:hydrated', { detail: { duration }, bubbles: true }));\n }\n\n if (__devtools_hooks && __devtools_hooks.onHydrate) {\n __devtools_hooks.onHydrate({ duration });\n }\n\n return result;\n}\n\nexport function mount(component, container) {\n if (!container) {\n console.error('Tova: Mount target not found');\n return;\n }\n\n const result = createRoot((dispose) => {\n const vnode = typeof component === 'function' ? component() : component;\n container.innerHTML = '';\n container.appendChild(render(vnode));\n return dispose;\n });\n\n if (__devtools_hooks && __devtools_hooks.onMount) {\n __devtools_hooks.onMount();\n }\n\n return result;\n}\n\n// ─── Progressive Hydration ──────────────────────────────────\n// Hydrate a component only when it becomes visible in the viewport.\n\nexport function hydrateWhenVisible(component, domNode, options = {}) {\n if (typeof IntersectionObserver === 'undefined') {\n // Fallback: hydrate immediately\n return hydrate(component, domNode);\n }\n\n const { rootMargin = '200px' } = options;\n let hydrated = false;\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting && !hydrated) {\n hydrated = true;\n observer.disconnect();\n hydrate(component, domNode);\n }\n }\n },\n { rootMargin },\n );\n\n observer.observe(domNode);\n\n return () => {\n observer.disconnect();\n };\n}\n";
4
4
 
5
5
  export const RPC_SOURCE = "// RPC bridge — client calls to server functions are auto-routed via HTTP\n\nconst RPC_BASE = typeof window !== 'undefined'\n ? (window.__TOVA_RPC_BASE || '')\n : 'http://localhost:3000';\n\nexport async function rpc(functionName, args = []) {\n const url = `${RPC_BASE}/rpc/${functionName}`;\n\n // Convert positional args to object if needed\n let body;\n if (args.length === 1 && typeof args[0] === 'object' && !Array.isArray(args[0])) {\n body = args[0];\n } else if (args.length > 0) {\n // Send as array, server will handle positional mapping\n body = { __args: args };\n } else {\n body = {};\n }\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`RPC call to '${functionName}' failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n return data.result;\n } catch (error) {\n if (error.message.includes('RPC call')) throw error;\n throw new Error(`RPC call to '${functionName}' failed: ${error.message}`);\n }\n}\n\n// Configure RPC base URL\nexport function configureRPC(baseUrl) {\n if (typeof window !== 'undefined') {\n window.__TOVA_RPC_BASE = baseUrl;\n }\n}\n";
6
6
 
@@ -363,9 +363,9 @@ export function watch(getter, callback, options = {}) {
363
363
  const effect = createEffect(() => {
364
364
  const newValue = getter();
365
365
  if (initialized) {
366
- callback(newValue, oldValue);
366
+ untrack(() => callback(newValue, oldValue));
367
367
  } else if (options.immediate) {
368
- callback(newValue, undefined);
368
+ untrack(() => callback(newValue, undefined));
369
369
  }
370
370
  oldValue = newValue;
371
371
  initialized = true;
@@ -463,13 +463,9 @@ export function ErrorBoundary({ fallback, children, onError, onReset, retry = 0
463
463
  if (onError) onError({ error: e, componentStack: stack });
464
464
  }
465
465
 
466
- pushErrorHandler(handleError);
467
-
468
466
  // Return a reactive wrapper that switches between children and fallback
469
467
  const childContent = children && children.length === 1 ? children[0] : tova_fragment(children || []);
470
468
 
471
- popErrorHandler();
472
-
473
469
  const vnode = {
474
470
  __tova: true,
475
471
  tag: '__dynamic',
@@ -477,6 +473,7 @@ export function ErrorBoundary({ fallback, children, onError, onReset, retry = 0
477
473
  children: [],
478
474
  _fallback: fallback,
479
475
  _componentName: 'ErrorBoundary',
476
+ _errorHandler: handleError, // Active during __dynamic effect render cycle
480
477
  compute: () => {
481
478
  const err = error();
482
479
  if (err) {
@@ -547,6 +544,7 @@ export function Portal({ target, children }) {
547
544
 
548
545
  export function lazy(loader) {
549
546
  let resolved = null;
547
+ let loadError = null;
550
548
  let promise = null;
551
549
 
552
550
  return function LazyWrapper(props) {
@@ -554,28 +552,28 @@ export function lazy(loader) {
554
552
  return resolved(props);
555
553
  }
556
554
 
557
- const [comp, setComp] = createSignal(null);
558
- const [err, setErr] = createSignal(null);
559
-
560
555
  if (!promise) {
561
556
  promise = loader()
562
557
  .then(mod => {
563
558
  resolved = mod.default || mod;
564
- setComp(() => resolved);
565
559
  })
566
- .catch(e => setErr(e));
560
+ .catch(e => { loadError = e; });
567
561
  }
568
562
 
563
+ const [tick, setTick] = createSignal(0);
564
+
565
+ // Trigger re-render when promise settles
566
+ promise.then(() => setTick(1)).catch(() => setTick(1));
567
+
569
568
  return {
570
569
  __tova: true,
571
570
  tag: '__dynamic',
572
571
  props: {},
573
572
  children: [],
574
573
  compute: () => {
575
- const e = err();
576
- if (e) return tova_el('span', { className: 'tova-error' }, [String(e)]);
577
- const c = comp();
578
- if (c) return c(props);
574
+ tick(); // Track for reactivity
575
+ if (loadError) return tova_el('span', { className: 'tova-error' }, [String(loadError)]);
576
+ if (resolved) return resolved(props);
579
577
  // Fallback while loading
580
578
  return props && props.fallback ? props.fallback : null;
581
579
  },
@@ -861,22 +859,36 @@ export function render(vnode) {
861
859
  frag.appendChild(marker);
862
860
 
863
861
  let prevDispose = null;
862
+ const errHandler = vnode._errorHandler || null;
864
863
  createEffect(() => {
865
- const inner = vnode.compute();
866
- const parent = marker.parentNode;
867
- const ref = nextSiblingAfterMarker(marker);
864
+ if (errHandler) pushErrorHandler(errHandler);
865
+ try {
866
+ const inner = vnode.compute();
867
+ const parent = marker.parentNode;
868
+ const ref = nextSiblingAfterMarker(marker);
869
+
870
+ if (prevDispose) {
871
+ prevDispose();
872
+ prevDispose = null;
873
+ }
874
+ clearMarkerContent(marker);
868
875
 
869
- if (prevDispose) {
870
- prevDispose();
871
- prevDispose = null;
876
+ createRoot((dispose) => {
877
+ prevDispose = dispose;
878
+ const rendered = render(inner);
879
+ marker.__tovaNodes = insertRendered(parent, rendered, ref, marker);
880
+ });
881
+ } catch (e) {
882
+ if (errHandler) {
883
+ errHandler(e);
884
+ } else if (currentErrorHandler) {
885
+ currentErrorHandler(e);
886
+ } else {
887
+ console.error('Uncaught error during render:', e);
888
+ }
889
+ } finally {
890
+ if (errHandler) popErrorHandler();
872
891
  }
873
- clearMarkerContent(marker);
874
-
875
- createRoot((dispose) => {
876
- prevDispose = dispose;
877
- const rendered = render(inner);
878
- marker.__tovaNodes = insertRendered(parent, rendered, ref, marker);
879
- });
880
892
  });
881
893
 
882
894
  return frag;
@@ -965,6 +977,14 @@ function applyPropValue(el, key, val) {
965
977
  } else if (key === 'disabled' || key === 'readOnly' || key === 'hidden') {
966
978
  el[key] = !!val;
967
979
  } else if (key === 'style' && typeof val === 'object') {
980
+ // Clear old properties not present in new style object
981
+ for (let i = el.style.length - 1; i >= 0; i--) {
982
+ const prop = el.style[i];
983
+ const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
984
+ if (!(prop in val) && !(camel in val)) {
985
+ el.style.removeProperty(prop);
986
+ }
987
+ }
968
988
  Object.assign(el.style, val);
969
989
  } else {
970
990
  const s = val == null ? '' : String(val);
@@ -1150,11 +1170,7 @@ function patchPositionalInMarker(marker, newChildren) {
1150
1170
  if (oldNodes[i].parentNode === parent) parent.removeChild(oldNodes[i]);
1151
1171
  }
1152
1172
 
1153
- marker.__tovaNodes = oldNodes.slice(0, Math.max(newCount, oldCount > newCount ? newCount : oldNodes.length));
1154
- // Simplify: rebuild __tovaNodes from what should remain
1155
- if (newCount <= oldCount) {
1156
- marker.__tovaNodes = oldNodes.slice(0, newCount);
1157
- }
1173
+ marker.__tovaNodes = oldNodes.slice(0, newCount);
1158
1174
  }
1159
1175
 
1160
1176
  // Keyed reconciliation for children of an element (not marker-based)
@@ -25,8 +25,8 @@ export const BUILTIN_FUNCTIONS = {
25
25
  sorted: `function sorted(a, k) { const c = [...a]; if (k) c.sort((x, y) => { const kx = k(x), ky = k(y); return kx < ky ? -1 : kx > ky ? 1 : 0; }); else c.sort((x, y) => x < y ? -1 : x > y ? 1 : 0); return c; }`,
26
26
  reversed: `function reversed(a) { return [...a].reverse(); }`,
27
27
  zip: `function zip(...as) { if (as.length === 0) return []; const m = Math.min(...as.map(a => a.length)); const r = []; for (let i = 0; i < m; i++) r.push(as.map(a => a[i])); return r; }`,
28
- min: `function min(a) { return a.length === 0 ? null : Math.min(...a); }`,
29
- max: `function max(a) { return a.length === 0 ? null : Math.max(...a); }`,
28
+ min: `function min(a) { if (a.length === 0) return null; let m = a[0]; for (let i = 1; i < a.length; i++) if (a[i] < m) m = a[i]; return m; }`,
29
+ max: `function max(a) { if (a.length === 0) return null; let m = a[0]; for (let i = 1; i < a.length; i++) if (a[i] > m) m = a[i]; return m; }`,
30
30
  type_of: `function type_of(v) { if (v === null) return 'Nil'; if (Array.isArray(v)) return 'List'; if (v?.__tag) return v.__tag; const t = typeof v; switch(t) { case 'number': return Number.isInteger(v) ? 'Int' : 'Float'; case 'string': return 'String'; case 'boolean': return 'Bool'; case 'function': return 'Function'; case 'object': return 'Object'; default: return 'Unknown'; } }`,
31
31
  filter: `function filter(arr, fn) { return arr.filter(fn); }`,
32
32
  map: `function map(arr, fn) { return arr.map(fn); }`,
@@ -163,12 +163,12 @@ Table.prototype = { get rows() { return this._rows.length; }, get columns() { re
163
163
  agg_count: `function agg_count(fn) { if (!fn) return (rows) => rows.length; return (rows) => rows.filter(fn).length; }`,
164
164
  agg_mean: `function agg_mean(fn) { return (rows) => { if (rows.length === 0) return 0; return rows.reduce((a, r) => a + (typeof fn === 'function' ? fn(r) : r[fn]), 0) / rows.length; }; }`,
165
165
  agg_median: `function agg_median(fn) { return (rows) => { if (rows.length === 0) return 0; const vs = rows.map(r => typeof fn === 'function' ? fn(r) : r[fn]).sort((a, b) => a - b); const m = Math.floor(vs.length / 2); return vs.length % 2 !== 0 ? vs[m] : (vs[m - 1] + vs[m]) / 2; }; }`,
166
- agg_min: `function agg_min(fn) { return (rows) => rows.length === 0 ? null : Math.min(...rows.map(r => typeof fn === 'function' ? fn(r) : r[fn])); }`,
167
- agg_max: `function agg_max(fn) { return (rows) => rows.length === 0 ? null : Math.max(...rows.map(r => typeof fn === 'function' ? fn(r) : r[fn])); }`,
166
+ agg_min: `function agg_min(fn) { return (rows) => { if (rows.length === 0) return null; let m = typeof fn === 'function' ? fn(rows[0]) : rows[0][fn]; for (let i = 1; i < rows.length; i++) { const v = typeof fn === 'function' ? fn(rows[i]) : rows[i][fn]; if (v < m) m = v; } return m; }; }`,
167
+ agg_max: `function agg_max(fn) { return (rows) => { if (rows.length === 0) return null; let m = typeof fn === 'function' ? fn(rows[0]) : rows[0][fn]; for (let i = 1; i < rows.length; i++) { const v = typeof fn === 'function' ? fn(rows[i]) : rows[i][fn]; if (v > m) m = v; } return m; }; }`,
168
168
 
169
169
  // ── Data exploration ────────────────────────────────
170
170
  peek: `function peek(table, opts) { const o = typeof opts === 'object' ? opts : {}; console.log(table._format ? table._format(o.n || 10, o.title) : String(table)); return table; }`,
171
- describe: `function describe(table) { const stats = []; for (const col of table._columns) { const vals = table._rows.map(r => r[col]).filter(v => v != null); const st = { Column: col, Type: 'Unknown', 'Non-Null': vals.length }; if (vals.length > 0) { const s = vals[0]; if (typeof s === 'number') { st.Type = Number.isInteger(s) ? 'Int' : 'Float'; st.Mean = vals.reduce((a, b) => a + b, 0) / vals.length; st.Min = Math.min(...vals); st.Max = Math.max(...vals); } else if (typeof s === 'string') { st.Type = 'String'; st.Unique = new Set(vals).size; } else if (typeof s === 'boolean') { st.Type = 'Bool'; } } stats.push(st); } const dt = Table(stats); console.log(dt._format(100, 'describe()')); return dt; }`,
171
+ describe: `function describe(table) { const stats = []; for (const col of table._columns) { const vals = table._rows.map(r => r[col]).filter(v => v != null); const st = { Column: col, Type: 'Unknown', 'Non-Null': vals.length }; if (vals.length > 0) { const s = vals[0]; if (typeof s === 'number') { st.Type = Number.isInteger(s) ? 'Int' : 'Float'; st.Mean = vals.reduce((a, b) => a + b, 0) / vals.length; let mn = vals[0], mx = vals[0]; for (let i = 1; i < vals.length; i++) { if (vals[i] < mn) mn = vals[i]; if (vals[i] > mx) mx = vals[i]; } st.Min = mn; st.Max = mx; } else if (typeof s === 'string') { st.Type = 'String'; st.Unique = new Set(vals).size; } else if (typeof s === 'boolean') { st.Type = 'Bool'; } } stats.push(st); } const dt = Table(stats); console.log(dt._format(100, 'describe()')); return dt; }`,
172
172
  schema_of: `function schema_of(table) { const sc = {}; if (table._rows.length === 0) { for (const c of table._columns) sc[c] = 'Unknown'; } else { const s = table._rows[0]; for (const c of table._columns) { const v = s[c]; if (v == null) sc[c] = 'Nil'; else if (typeof v === 'number') sc[c] = Number.isInteger(v) ? 'Int' : 'Float'; else if (typeof v === 'string') sc[c] = 'String'; else if (typeof v === 'boolean') sc[c] = 'Bool'; else if (Array.isArray(v)) sc[c] = 'Array'; else sc[c] = 'Object'; } } console.log('Schema:'); for (const [c, t] of Object.entries(sc)) console.log(' ' + c + ': ' + t); return sc; }`,
173
173
 
174
174
  // ── Data cleaning ───────────────────────────────────
@@ -302,7 +302,7 @@ Table.prototype = { get rows() { return this._rows.length; }, get columns() { re
302
302
  // ── Math (new) ─────────────────────────────────────────
303
303
  hypot: `function hypot(a, b) { return Math.hypot(a, b); }`,
304
304
  lerp: `function lerp(a, b, t) { return a + (b - a) * t; }`,
305
- divmod: `function divmod(a, b) { return [Math.floor(a / b), a % b]; }`,
305
+ divmod: `function divmod(a, b) { const q = Math.floor(a / b); return [q, a - q * b]; }`,
306
306
  avg: `function avg(arr) { return arr.length === 0 ? 0 : arr.reduce((a, b) => a + b, 0) / arr.length; }`,
307
307
 
308
308
  // ── Date/Time (new) ────────────────────────────────────
@@ -374,7 +374,7 @@ Table.prototype = { get rows() { return this._rows.length; }, get columns() { re
374
374
  combinations: `function combinations(arr, r) { const result = []; const combo = []; function gen(start, depth) { if (depth === r) { result.push([...combo]); return; } for (let i = start; i < arr.length; i++) { combo.push(arr[i]); gen(i + 1, depth + 1); combo.pop(); } } gen(0, 0); return result; }`,
375
375
  permutations: `function permutations(arr, r) { const n = r === undefined ? arr.length : r; const result = []; const perm = []; const used = new Array(arr.length).fill(false); function gen() { if (perm.length === n) { result.push([...perm]); return; } for (let i = 0; i < arr.length; i++) { if (!used[i]) { used[i] = true; perm.push(arr[i]); gen(); perm.pop(); used[i] = false; } } } gen(); return result; }`,
376
376
  intersperse: `function intersperse(arr, sep) { if (arr.length <= 1) return [...arr]; const r = [arr[0]]; for (let i = 1; i < arr.length; i++) { r.push(sep, arr[i]); } return r; }`,
377
- interleave: `function interleave(...arrs) { const m = Math.max(...arrs.map(a => a.length)); const r = []; for (let i = 0; i < m; i++) { for (const a of arrs) { if (i < a.length) r.push(a[i]); } } return r; }`,
377
+ interleave: `function interleave(...arrs) { if (arrs.length === 0) return []; const m = Math.max(...arrs.map(a => a.length)); const r = []; for (let i = 0; i < m; i++) { for (const a of arrs) { if (i < a.length) r.push(a[i]); } } return r; }`,
378
378
  repeat_value: `function repeat_value(val, n) { return Array(n).fill(val); }`,
379
379
 
380
380
  // ── Array Utilities ────────────────────────────────────
package/src/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by scripts/embed-runtime.js — do not edit
2
- export const VERSION = "0.2.9";
2
+ export const VERSION = "0.3.0";