tova 0.9.11 → 0.9.13
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 +66 -17
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +28 -12
- package/src/analyzer/browser-analyzer.js +14 -3
- package/src/codegen/base-codegen.js +99 -3
- package/src/codegen/browser-codegen.js +2 -2
- package/src/codegen/edge-codegen.js +6 -4
- package/src/codegen/form-codegen.js +4 -4
- package/src/codegen/security-codegen.js +5 -4
- package/src/codegen/server-codegen.js +1 -0
- package/src/lexer/lexer.js +8 -0
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +1 -0
- package/src/stdlib/core.js +12 -2
- package/src/stdlib/inline.js +3 -3
- package/src/stdlib/math.js +1 -1
- package/src/version.js +1 -1
package/bin/tova.js
CHANGED
|
@@ -780,18 +780,40 @@ async function runFile(filePath, options = {}) {
|
|
|
780
780
|
return;
|
|
781
781
|
}
|
|
782
782
|
|
|
783
|
-
// Compile .tova dependencies and inline them
|
|
783
|
+
// Compile .tova dependencies and inline them (recursively)
|
|
784
784
|
let depCode = '';
|
|
785
785
|
if (hasTovaImports) {
|
|
786
786
|
const compiled = new Set();
|
|
787
|
-
|
|
788
|
-
if (compiled.has(
|
|
789
|
-
compiled.add(
|
|
790
|
-
|
|
791
|
-
const
|
|
787
|
+
const resolveTovaImportsRecursive = (filePath) => {
|
|
788
|
+
if (compiled.has(filePath)) return;
|
|
789
|
+
compiled.add(filePath);
|
|
790
|
+
|
|
791
|
+
const depSource = readFileSync(filePath, 'utf-8');
|
|
792
|
+
|
|
793
|
+
// Scan for transitive .tova imports
|
|
794
|
+
const transitiveRegex = /import\s+(?:\{[^}]*\}|[\w$]+|\*\s+as\s+[\w$]+)\s+from\s+['"]([^'"]+)['"]/g;
|
|
795
|
+
let transitiveMatch;
|
|
796
|
+
while ((transitiveMatch = transitiveRegex.exec(depSource)) !== null) {
|
|
797
|
+
const importSource = transitiveMatch[1];
|
|
798
|
+
if (!importSource.startsWith('.') && !importSource.startsWith('/')) continue;
|
|
799
|
+
let transitivePath = resolve(dirname(filePath), importSource);
|
|
800
|
+
if (!transitivePath.endsWith('.tova') && existsSync(transitivePath + '.tova')) {
|
|
801
|
+
transitivePath = transitivePath + '.tova';
|
|
802
|
+
}
|
|
803
|
+
if (transitivePath.endsWith('.tova') && existsSync(transitivePath)) {
|
|
804
|
+
resolveTovaImportsRecursive(transitivePath);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Compile this dependency
|
|
809
|
+
const dep = compileTova(depSource, filePath, { strict: options.strict });
|
|
792
810
|
let depShared = dep.shared || '';
|
|
793
811
|
depShared = depShared.replace(/^export /gm, '');
|
|
794
812
|
depCode += depShared + '\n';
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
for (const imp of tovaImportPaths) {
|
|
816
|
+
resolveTovaImportsRecursive(imp.resolved);
|
|
795
817
|
}
|
|
796
818
|
}
|
|
797
819
|
|
|
@@ -1305,7 +1327,7 @@ async function buildProject(args) {
|
|
|
1305
1327
|
if (scorecard) console.log(scorecard.format());
|
|
1306
1328
|
}
|
|
1307
1329
|
|
|
1308
|
-
if (errorCount > 0) process.exit(1);
|
|
1330
|
+
if (errorCount > 0 && !isWatch) process.exit(1);
|
|
1309
1331
|
|
|
1310
1332
|
// Watch mode for build command
|
|
1311
1333
|
if (isWatch) {
|
|
@@ -1318,7 +1340,11 @@ async function buildProject(args) {
|
|
|
1318
1340
|
const changedPath = resolve(srcDir, filename);
|
|
1319
1341
|
invalidateFile(changedPath);
|
|
1320
1342
|
if (!isQuiet) console.log(` Rebuilding (${filename} changed)...`);
|
|
1321
|
-
|
|
1343
|
+
try {
|
|
1344
|
+
await buildProject(args.filter(a => a !== '--watch'));
|
|
1345
|
+
} catch (err) {
|
|
1346
|
+
// Continue watching even on error
|
|
1347
|
+
}
|
|
1322
1348
|
if (!isQuiet) console.log(' Watching for changes...\n');
|
|
1323
1349
|
}, 100);
|
|
1324
1350
|
});
|
|
@@ -1811,6 +1837,8 @@ async function devServer(args) {
|
|
|
1811
1837
|
let rebuildBrowserCode = '';
|
|
1812
1838
|
let rebuildHasClient = false;
|
|
1813
1839
|
|
|
1840
|
+
// First pass: collect all outputs without writing server files yet
|
|
1841
|
+
const dirOutputs = [];
|
|
1814
1842
|
for (const [dir, files] of rebuildDirGroups) {
|
|
1815
1843
|
const dirName = basename(dir) === '.' ? 'app' : basename(dir);
|
|
1816
1844
|
const result = mergeDirectory(dir, srcDir, { strict: buildStrict, strictSecurity: buildStrictSecurity, isDev: true });
|
|
@@ -1841,6 +1869,21 @@ async function devServer(args) {
|
|
|
1841
1869
|
rebuildBrowserCode = fixedBrowser;
|
|
1842
1870
|
rebuildHasClient = true;
|
|
1843
1871
|
}
|
|
1872
|
+
|
|
1873
|
+
// Store outputs for second pass
|
|
1874
|
+
dirOutputs.push({ output, outBaseName });
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// Generate dev HTML with all shared code prepended to browser code
|
|
1878
|
+
if (rebuildHasClient) {
|
|
1879
|
+
const rebuildAllShared = rebuildSharedParts.join('\n').replace(/^export /gm, '');
|
|
1880
|
+
const rebuildFullClient = rebuildAllShared ? rebuildAllShared + '\n' + rebuildBrowserCode : rebuildBrowserCode;
|
|
1881
|
+
rebuildClientHTML = await generateDevHTML(rebuildFullClient, srcDir, actualReloadPort);
|
|
1882
|
+
writeFileSync(join(outDir, 'index.html'), rebuildClientHTML);
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// Second pass: write server files with correct __clientHTML
|
|
1886
|
+
for (const { output, outBaseName } of dirOutputs) {
|
|
1844
1887
|
if (output.server) {
|
|
1845
1888
|
let serverCode = output.server;
|
|
1846
1889
|
if (rebuildClientHTML) {
|
|
@@ -1859,14 +1902,6 @@ async function devServer(args) {
|
|
|
1859
1902
|
}
|
|
1860
1903
|
}
|
|
1861
1904
|
}
|
|
1862
|
-
|
|
1863
|
-
// Generate dev HTML with all shared code prepended to browser code
|
|
1864
|
-
if (rebuildHasClient) {
|
|
1865
|
-
const rebuildAllShared = rebuildSharedParts.join('\n').replace(/^export /gm, '');
|
|
1866
|
-
const rebuildFullClient = rebuildAllShared ? rebuildAllShared + '\n' + rebuildBrowserCode : rebuildBrowserCode;
|
|
1867
|
-
rebuildClientHTML = await generateDevHTML(rebuildFullClient, srcDir, actualReloadPort);
|
|
1868
|
-
writeFileSync(join(outDir, 'index.html'), rebuildClientHTML);
|
|
1869
|
-
}
|
|
1870
1905
|
} catch (err) {
|
|
1871
1906
|
console.error(` ✗ Rebuild failed: ${err.message}`);
|
|
1872
1907
|
return; // Keep old processes running
|
|
@@ -2685,6 +2720,13 @@ shared {
|
|
|
2685
2720
|
}
|
|
2686
2721
|
}
|
|
2687
2722
|
|
|
2723
|
+
security {
|
|
2724
|
+
cors {
|
|
2725
|
+
origins: ["http://localhost:3000"]
|
|
2726
|
+
methods: ["GET", "POST"]
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2688
2730
|
server {
|
|
2689
2731
|
fn get_message() {
|
|
2690
2732
|
Message("Hello from Tova!", Date.new().toLocaleTimeString())
|
|
@@ -3169,6 +3211,13 @@ browser {
|
|
|
3169
3211
|
file: 'src/app.tova',
|
|
3170
3212
|
content: name => `// ${name} — Built with Tova
|
|
3171
3213
|
|
|
3214
|
+
security {
|
|
3215
|
+
cors {
|
|
3216
|
+
origins: ["http://localhost:3000"]
|
|
3217
|
+
methods: ["GET", "POST", "PUT", "DELETE"]
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3172
3221
|
server {
|
|
3173
3222
|
fn health() {
|
|
3174
3223
|
{ status: "ok" }
|
|
@@ -4966,7 +5015,7 @@ async function productionBuild(srcDir, outDir, isStatic = false) {
|
|
|
4966
5015
|
for (const file of tovaFiles) {
|
|
4967
5016
|
try {
|
|
4968
5017
|
const source = readFileSync(file, 'utf-8');
|
|
4969
|
-
const output =
|
|
5018
|
+
const output = compileWithImports(source, file, srcDir);
|
|
4970
5019
|
|
|
4971
5020
|
if (output.shared) sharedParts.push(output.shared);
|
|
4972
5021
|
if (output.server) serverParts.push(output.server);
|
package/package.json
CHANGED
package/src/analyzer/analyzer.js
CHANGED
|
@@ -529,7 +529,8 @@ export class Analyzer {
|
|
|
529
529
|
const rt = this._inferType(expr.right);
|
|
530
530
|
if (!lt && !rt) return null;
|
|
531
531
|
if (lt === 'Float' || rt === 'Float') return 'Float';
|
|
532
|
-
|
|
532
|
+
// Bug fix 4: Tova uses ++ for concatenation, + is numeric only
|
|
533
|
+
// Remove String check for + operator
|
|
533
534
|
return 'Int';
|
|
534
535
|
}
|
|
535
536
|
if (BITWISE_OPS.has(expr.operator)) return 'Int';
|
|
@@ -2250,12 +2251,19 @@ export class Analyzer {
|
|
|
2250
2251
|
} else if (node.pattern.type === 'ArrayPattern' || node.pattern.type === 'TuplePattern') {
|
|
2251
2252
|
for (const el of node.pattern.elements) {
|
|
2252
2253
|
if (el) {
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2254
|
+
// Bug fix 1: check if element is a string before calling .startsWith()
|
|
2255
|
+
// Elements can be nested pattern objects
|
|
2256
|
+
if (typeof el === 'string') {
|
|
2257
|
+
const varName = el.startsWith('...') ? el.slice(3) : el;
|
|
2258
|
+
try {
|
|
2259
|
+
this.currentScope.define(varName,
|
|
2260
|
+
new Symbol(varName, 'variable', null, false, node.loc));
|
|
2261
|
+
} catch (e) {
|
|
2262
|
+
this.error(e.message);
|
|
2263
|
+
}
|
|
2264
|
+
} else if (el && typeof el === 'object') {
|
|
2265
|
+
// Nested pattern - recursively extract variables using visitPattern
|
|
2266
|
+
this.visitPattern(el);
|
|
2259
2267
|
}
|
|
2260
2268
|
}
|
|
2261
2269
|
}
|
|
@@ -2700,13 +2708,21 @@ export class Analyzer {
|
|
|
2700
2708
|
this.visitExpression(node.iterable);
|
|
2701
2709
|
|
|
2702
2710
|
// Define loop variable(s)
|
|
2711
|
+
// Bug fix 2: handle destructured loop variables (pattern objects)
|
|
2703
2712
|
const vars = Array.isArray(node.variable) ? node.variable : [node.variable];
|
|
2704
2713
|
for (const v of vars) {
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2714
|
+
// Check if v is a pattern object (has a .type property like ObjectPattern or ArrayPattern)
|
|
2715
|
+
if (typeof v === 'object' && v && v.type) {
|
|
2716
|
+
// It's a pattern - use visitPattern to extract and define all bound variables
|
|
2717
|
+
this.visitPattern(v);
|
|
2718
|
+
} else {
|
|
2719
|
+
// It's a plain string variable name
|
|
2720
|
+
try {
|
|
2721
|
+
this.currentScope.define(v,
|
|
2722
|
+
new Symbol(v, 'variable', null, false, node.loc));
|
|
2723
|
+
} catch (e) {
|
|
2724
|
+
this.error(e.message);
|
|
2725
|
+
}
|
|
2710
2726
|
}
|
|
2711
2727
|
}
|
|
2712
2728
|
|
|
@@ -278,9 +278,20 @@ export function installBrowserAnalyzer(AnalyzerClass) {
|
|
|
278
278
|
AnalyzerClass.prototype.visitJSXMatch = function(node) {
|
|
279
279
|
this.visitExpression(node.subject);
|
|
280
280
|
for (const arm of node.arms) {
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
281
|
+
// Bug fix 3: Create child scope and visit pattern to define bound variables
|
|
282
|
+
// Match the pattern used in visitMatchExpression
|
|
283
|
+
const prevScope = this.currentScope;
|
|
284
|
+
this.currentScope = this.currentScope.child('block');
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
this.visitPattern(arm.pattern);
|
|
288
|
+
if (arm.guard) this.visitExpression(arm.guard);
|
|
289
|
+
|
|
290
|
+
for (const child of arm.body) {
|
|
291
|
+
this.visitNode(child);
|
|
292
|
+
}
|
|
293
|
+
} finally {
|
|
294
|
+
this.currentScope = prevScope;
|
|
284
295
|
}
|
|
285
296
|
}
|
|
286
297
|
};
|
|
@@ -32,6 +32,8 @@ export class BaseCodegen {
|
|
|
32
32
|
this._fastMode = false;
|
|
33
33
|
this._typedArrayParams = new Map(); // paramName -> 'Float64Array' | 'Int32Array' | 'Uint8Array'
|
|
34
34
|
this._typedArrayLocals = new Map(); // varName -> 'Float64Array' | 'Int32Array' | 'Uint8Array'
|
|
35
|
+
// Track shadowed parameter names to prevent incorrect substitution in nested lambdas
|
|
36
|
+
this._substitutionShadowed = new Set(); // names shadowed by lambda/function params
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
static TYPED_ARRAY_MAP = {
|
|
@@ -328,7 +330,8 @@ export class BaseCodegen {
|
|
|
328
330
|
switch (node.type) {
|
|
329
331
|
case 'Identifier':
|
|
330
332
|
// Parameter substitution for map chain fusion
|
|
331
|
-
if
|
|
333
|
+
// Skip substitution if the name is shadowed by a nested lambda/function parameter
|
|
334
|
+
if (this._paramSubstitutions && this._paramSubstitutions.has(node.name) && !this._substitutionShadowed.has(node.name)) {
|
|
332
335
|
return this._paramSubstitutions.get(node.name);
|
|
333
336
|
}
|
|
334
337
|
// Track builtin identifier usage (e.g., None used without call)
|
|
@@ -556,6 +559,19 @@ export class BaseCodegen {
|
|
|
556
559
|
const exportPrefix = node.isPublic ? 'export ' : '';
|
|
557
560
|
const asyncPrefix = node.isAsync ? 'async ' : '';
|
|
558
561
|
const genStar = isGenerator ? '*' : '';
|
|
562
|
+
|
|
563
|
+
// Track which parameter names shadow active substitutions
|
|
564
|
+
const paramNames = this._collectParamNames(node.params);
|
|
565
|
+
const shadowedNames = [];
|
|
566
|
+
if (this._paramSubstitutions) {
|
|
567
|
+
for (const name of paramNames) {
|
|
568
|
+
if (this._paramSubstitutions.has(name)) {
|
|
569
|
+
shadowedNames.push(name);
|
|
570
|
+
this._substitutionShadowed.add(name);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
559
575
|
this.pushScope();
|
|
560
576
|
for (const p of node.params) {
|
|
561
577
|
if (p.destructure) {
|
|
@@ -566,6 +582,11 @@ export class BaseCodegen {
|
|
|
566
582
|
}
|
|
567
583
|
const body = this.genBlockBody(node.body);
|
|
568
584
|
this.popScope();
|
|
585
|
+
|
|
586
|
+
// Restore shadowing state
|
|
587
|
+
for (const name of shadowedNames) {
|
|
588
|
+
this._substitutionShadowed.delete(name);
|
|
589
|
+
}
|
|
569
590
|
const lines = [];
|
|
570
591
|
lines.push(`${this.i()}${exportPrefix}${asyncPrefix}function${genStar} ${node.name}(${params}) {`);
|
|
571
592
|
// In @fast mode, convert typed array params at function entry
|
|
@@ -1722,6 +1743,53 @@ export class BaseCodegen {
|
|
|
1722
1743
|
return result;
|
|
1723
1744
|
}
|
|
1724
1745
|
|
|
1746
|
+
// Collect all parameter names from a param list (including destructured names)
|
|
1747
|
+
_collectParamNames(params) {
|
|
1748
|
+
const names = [];
|
|
1749
|
+
for (const p of params) {
|
|
1750
|
+
if (p.destructure) {
|
|
1751
|
+
// Recursively collect names from destructuring patterns
|
|
1752
|
+
this._collectDestructureNames(p.destructure, names);
|
|
1753
|
+
} else {
|
|
1754
|
+
names.push(p.name);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
return names;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// Helper to recursively collect names from destructuring patterns
|
|
1761
|
+
_collectDestructureNames(pattern, names) {
|
|
1762
|
+
if (pattern.type === 'ObjectPattern') {
|
|
1763
|
+
for (const prop of pattern.properties) {
|
|
1764
|
+
if (prop.value && prop.value.type === 'Identifier') {
|
|
1765
|
+
names.push(prop.value.name);
|
|
1766
|
+
} else if (prop.value) {
|
|
1767
|
+
this._collectDestructureNames(prop.value, names);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
} else if (pattern.type === 'ArrayPattern') {
|
|
1771
|
+
for (const elem of pattern.elements) {
|
|
1772
|
+
if (elem === null) continue; // skip holes
|
|
1773
|
+
if (elem.type === 'Identifier') {
|
|
1774
|
+
names.push(elem.name);
|
|
1775
|
+
} else if (elem.type === 'RestElement' && elem.argument.type === 'Identifier') {
|
|
1776
|
+
names.push(elem.argument.name);
|
|
1777
|
+
} else {
|
|
1778
|
+
this._collectDestructureNames(elem, names);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
} else if (pattern.type === 'TuplePattern') {
|
|
1782
|
+
for (const elem of pattern.elements) {
|
|
1783
|
+
if (elem === null) continue;
|
|
1784
|
+
if (elem.type === 'Identifier') {
|
|
1785
|
+
names.push(elem.name);
|
|
1786
|
+
} else {
|
|
1787
|
+
this._collectDestructureNames(elem, names);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1725
1793
|
// ─── Compile-time devirtualization for Result/Option ──────────
|
|
1726
1794
|
// When the codegen sees Ok(val).method(), Err(val).method(), Some(val).method(),
|
|
1727
1795
|
// or None.method(), it knows the exact type at compile time. Instead of allocating
|
|
@@ -2421,11 +2489,27 @@ export class BaseCodegen {
|
|
|
2421
2489
|
const hasPropagate = this._containsPropagate(node.body);
|
|
2422
2490
|
const asyncPrefix = node.isAsync ? 'async ' : '';
|
|
2423
2491
|
|
|
2492
|
+
// Track which parameter names shadow active substitutions
|
|
2493
|
+
const paramNames = this._collectParamNames(node.params);
|
|
2494
|
+
const shadowedNames = [];
|
|
2495
|
+
if (this._paramSubstitutions) {
|
|
2496
|
+
for (const name of paramNames) {
|
|
2497
|
+
if (this._paramSubstitutions.has(name)) {
|
|
2498
|
+
shadowedNames.push(name);
|
|
2499
|
+
this._substitutionShadowed.add(name);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2424
2504
|
if (node.body.type === 'BlockStatement') {
|
|
2425
2505
|
this.pushScope();
|
|
2426
2506
|
for (const p of node.params) { if (p.destructure) this._declareDestructureVars(p.destructure); else this.declareVar(p.name); }
|
|
2427
2507
|
const body = this.genBlockBody(node.body);
|
|
2428
2508
|
this.popScope();
|
|
2509
|
+
// Restore shadowing state
|
|
2510
|
+
for (const name of shadowedNames) {
|
|
2511
|
+
this._substitutionShadowed.delete(name);
|
|
2512
|
+
}
|
|
2429
2513
|
if (hasPropagate) {
|
|
2430
2514
|
const p = [];
|
|
2431
2515
|
p.push(`${asyncPrefix}(${params}) => {`);
|
|
@@ -2449,13 +2533,25 @@ export class BaseCodegen {
|
|
|
2449
2533
|
const stmt = this.generateStatement(node.body);
|
|
2450
2534
|
this.indent--;
|
|
2451
2535
|
this.popScope();
|
|
2536
|
+
// Restore shadowing state
|
|
2537
|
+
for (const name of shadowedNames) {
|
|
2538
|
+
this._substitutionShadowed.delete(name);
|
|
2539
|
+
}
|
|
2452
2540
|
return `${asyncPrefix}(${params}) => { ${stmt.trim()} }`;
|
|
2453
2541
|
}
|
|
2454
2542
|
|
|
2543
|
+
// Expression body
|
|
2544
|
+
let bodyCode;
|
|
2455
2545
|
if (hasPropagate) {
|
|
2456
|
-
|
|
2546
|
+
bodyCode = `${asyncPrefix}(${params}) => { try { return ${this.genExpression(node.body)}; } catch (__e) { if (__e && __e.__tova_propagate) return __e.value; throw __e; } }`;
|
|
2547
|
+
} else {
|
|
2548
|
+
bodyCode = `${asyncPrefix}(${params}) => ${this.genExpression(node.body)}`;
|
|
2549
|
+
}
|
|
2550
|
+
// Restore shadowing state
|
|
2551
|
+
for (const name of shadowedNames) {
|
|
2552
|
+
this._substitutionShadowed.delete(name);
|
|
2457
2553
|
}
|
|
2458
|
-
return
|
|
2554
|
+
return bodyCode;
|
|
2459
2555
|
}
|
|
2460
2556
|
|
|
2461
2557
|
// Check if a match can be emitted as a ternary chain instead of IIFE
|
|
@@ -48,7 +48,7 @@ export class BrowserCodegen extends BaseCodegen {
|
|
|
48
48
|
if (node.type === 'ReturnStatement') return this._containsRPC(node.value);
|
|
49
49
|
if (node.type === 'IfStatement') {
|
|
50
50
|
return this._containsRPC(node.condition) || this._containsRPC(node.consequent) ||
|
|
51
|
-
node.alternates.some(a => this._containsRPC(a.body)) ||
|
|
51
|
+
(node.alternates && node.alternates.some(a => this._containsRPC(a.body))) ||
|
|
52
52
|
this._containsRPC(node.elseBody);
|
|
53
53
|
}
|
|
54
54
|
if (node.type === 'IfExpression') {
|
|
@@ -223,7 +223,7 @@ export class BrowserCodegen extends BaseCodegen {
|
|
|
223
223
|
// Runtime imports
|
|
224
224
|
lines.push(`import { createSignal, createEffect, createComputed, mount, hydrate, tova_el, tova_fragment, tova_keyed, tova_transition, tova_inject_css, batch, onMount, onUnmount, onCleanup, onBeforeUpdate, createRef, createContext, provide, inject, createErrorBoundary, ErrorBoundary, ErrorInfo, createRoot, watch, untrack, Dynamic, Portal, lazy, Suspense, Head, createResource, __tova_action, TransitionGroup, createForm, configureCSP, __tova_load_font } from './runtime/reactivity.js';`);
|
|
225
225
|
lines.push(`import { rpc, configureRPC, addRPCInterceptor, setCSRFToken } from './runtime/rpc.js';`);
|
|
226
|
-
lines.push(`import { createRouter,
|
|
226
|
+
lines.push(`import { createRouter, resetRouter, navigate, getCurrentRoute, getParams, getPath, getQuery, getMeta, defineRoutes, onRouteChange, beforeNavigate, afterNavigate, getRouter, Router, Outlet, Link, Redirect } from './runtime/router.js';`);
|
|
227
227
|
|
|
228
228
|
// Hoist import lines from shared code to the top of the module
|
|
229
229
|
let sharedRest = sharedCode;
|
|
@@ -185,7 +185,7 @@ export class EdgeCodegen extends BaseCodegen {
|
|
|
185
185
|
lines.push(' const checks = {};');
|
|
186
186
|
lines.push(' let status = "healthy";');
|
|
187
187
|
if (config.healthChecks.includes('check_memory')) {
|
|
188
|
-
lines.push(' const mem = process.memoryUsage ? process.memoryUsage() : { heapUsed: 0, heapTotal: 1 };');
|
|
188
|
+
lines.push(' const mem = (typeof process !== "undefined" && process.memoryUsage) ? process.memoryUsage() : { heapUsed: 0, heapTotal: 1 };');
|
|
189
189
|
lines.push(' const heapPct = mem.heapUsed / mem.heapTotal;');
|
|
190
190
|
lines.push(' checks.memory = { status: heapPct > 0.9 ? "degraded" : "healthy", heapUsed: mem.heapUsed, heapTotal: mem.heapTotal };');
|
|
191
191
|
lines.push(' if (heapPct > 0.9) status = "degraded";');
|
|
@@ -264,15 +264,17 @@ export class EdgeCodegen extends BaseCodegen {
|
|
|
264
264
|
const fallbackHeaders = hasCors
|
|
265
265
|
? `{ "Content-Type": "application/json", ...__getCorsHeaders(${reqVar}) }`
|
|
266
266
|
: '{ "Content-Type": "application/json" }';
|
|
267
|
-
lines.push(`${indent}
|
|
267
|
+
lines.push(`${indent} console.error("[tova:edge]", e);`);
|
|
268
|
+
lines.push(`${indent} return { statusCode: 500, headers: ${fallbackHeaders}, body: JSON.stringify({ error: "Internal Server Error" }) };`);
|
|
268
269
|
} else {
|
|
270
|
+
lines.push(`${indent} console.error("[tova:edge]", e);`);
|
|
269
271
|
if (hasCors) {
|
|
270
|
-
lines.push(`${indent} return new Response(JSON.stringify({ error:
|
|
272
|
+
lines.push(`${indent} return new Response(JSON.stringify({ error: "Internal Server Error" }), {`);
|
|
271
273
|
lines.push(`${indent} status: 500,`);
|
|
272
274
|
lines.push(`${indent} headers: { "Content-Type": "application/json", ...__getCorsHeaders(${reqVar}) }`);
|
|
273
275
|
lines.push(`${indent} });`);
|
|
274
276
|
} else {
|
|
275
|
-
lines.push(`${indent} return new Response(JSON.stringify({ error:
|
|
277
|
+
lines.push(`${indent} return new Response(JSON.stringify({ error: "Internal Server Error" }), {`);
|
|
276
278
|
lines.push(`${indent} status: 500,`);
|
|
277
279
|
lines.push(`${indent} headers: { "Content-Type": "application/json" }`);
|
|
278
280
|
lines.push(`${indent} });`);
|
|
@@ -39,13 +39,13 @@ export function generateValidatorFn(fieldName, validators, genExpression, indent
|
|
|
39
39
|
|
|
40
40
|
case 'min': {
|
|
41
41
|
const threshold = v.args.length >= 2 ? genExpression(v.args[0]) : '0';
|
|
42
|
-
lines.push(`${indent} if (
|
|
42
|
+
lines.push(`${indent} if (v !== "" && v !== null && v !== undefined && Number(v) < ${threshold}) return ${msg};`);
|
|
43
43
|
break;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
case 'max': {
|
|
47
47
|
const threshold = v.args.length >= 2 ? genExpression(v.args[0]) : 'Infinity';
|
|
48
|
-
lines.push(`${indent} if (
|
|
48
|
+
lines.push(`${indent} if (v !== "" && v !== null && v !== undefined && Number(v) > ${threshold}) return ${msg};`);
|
|
49
49
|
break;
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -196,13 +196,13 @@ export function generateGuardedValidatorFn(fieldName, validators, genExpression,
|
|
|
196
196
|
|
|
197
197
|
case 'min': {
|
|
198
198
|
const threshold = v.args.length >= 2 ? genExpression(v.args[0]) : '0';
|
|
199
|
-
lines.push(`${indent} if (
|
|
199
|
+
lines.push(`${indent} if (v !== "" && v !== null && v !== undefined && Number(v) < ${threshold}) return ${msg};`);
|
|
200
200
|
break;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
case 'max': {
|
|
204
204
|
const threshold = v.args.length >= 2 ? genExpression(v.args[0]) : 'Infinity';
|
|
205
|
-
lines.push(`${indent} if (
|
|
205
|
+
lines.push(`${indent} if (v !== "" && v !== null && v !== undefined && Number(v) > ${threshold}) return ${msg};`);
|
|
206
206
|
break;
|
|
207
207
|
}
|
|
208
208
|
|
|
@@ -262,14 +262,15 @@ export class SecurityCodegen extends BaseCodegen {
|
|
|
262
262
|
lines.push(`function ${fnName}(obj, user) {`);
|
|
263
263
|
lines.push(' if (!obj) return obj;');
|
|
264
264
|
lines.push(' const result = { ...obj };');
|
|
265
|
-
for (
|
|
265
|
+
for (let fi = 0; fi < fields.length; fi++) {
|
|
266
|
+
const field = fields[fi];
|
|
266
267
|
if (field.config.never_expose) {
|
|
267
268
|
lines.push(` delete result.${field.fieldName};`);
|
|
268
269
|
} else if (field.config.visible_to) {
|
|
269
270
|
const visibleExpr = this.genExpression(field.config.visible_to);
|
|
270
|
-
lines.push(` const __visibleTo = ${visibleExpr};`);
|
|
271
|
-
lines.push(`
|
|
272
|
-
lines.push(`
|
|
271
|
+
lines.push(` { const __visibleTo = ${visibleExpr};`);
|
|
272
|
+
lines.push(` const __canSee = __visibleTo.some(v => v === "self" ? (user && __isSameIdentity(user, obj)) : __hasRole(user, v));`);
|
|
273
|
+
lines.push(` if (!__canSee) delete result.${field.fieldName}; }`);
|
|
273
274
|
}
|
|
274
275
|
}
|
|
275
276
|
lines.push(' return result;');
|
|
@@ -3217,6 +3217,7 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
3217
3217
|
lines.push(' let __pathname = __qIdx === -1 ? __rawUrl.slice(__pStart) : __rawUrl.slice(__pStart, __qIdx);');
|
|
3218
3218
|
lines.push(' try { __pathname = decodeURIComponent(__pathname); } catch {}');
|
|
3219
3219
|
lines.push(' __pathname = __pathname.replace(/\\/\\/+/g, "/");');
|
|
3220
|
+
lines.push(' if (__pathname.includes("..")) { const __parts = __pathname.split("/"); const __resolved = []; for (const __seg of __parts) { if (__seg === "..") { __resolved.pop(); } else if (__seg !== ".") { __resolved.push(__seg); } } __pathname = __resolved.join("/") || "/"; }');
|
|
3220
3221
|
lines.push(' if (__pathname.length > 1 && __pathname.endsWith("/")) __pathname = __pathname.slice(0, -1);');
|
|
3221
3222
|
lines.push(' const __method = req.method;');
|
|
3222
3223
|
|
package/src/lexer/lexer.js
CHANGED
|
@@ -518,6 +518,14 @@ export class Lexer {
|
|
|
518
518
|
} else if (quote === '"' && this.peek() === '}' && strDepth > 0) {
|
|
519
519
|
strDepth--;
|
|
520
520
|
exprParts.push(this.advance());
|
|
521
|
+
} else if (quote === '`' && this.peek() === '$' && this.pos + 1 < this.length && this.source[this.pos + 1] === '{') {
|
|
522
|
+
// Handle JS template literal interpolation ${...}
|
|
523
|
+
exprParts.push(this.advance()); // $
|
|
524
|
+
exprParts.push(this.advance()); // {
|
|
525
|
+
strDepth++;
|
|
526
|
+
} else if (quote === '`' && this.peek() === '}' && strDepth > 0) {
|
|
527
|
+
strDepth--;
|
|
528
|
+
exprParts.push(this.advance());
|
|
521
529
|
} else if (this.peek() === quote && strDepth === 0) {
|
|
522
530
|
break;
|
|
523
531
|
} else {
|
package/src/runtime/embedded.js
CHANGED
|
@@ -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\n// Reusable array for flush cycle — avoids allocation on every flush\nlet _flushBuf = [];\n\n// Reset module-level mutable state for test isolation.\n// Bun runs all test files in a single process, so Maps like __tovaStyleRefs\n// accumulate entries across files. Call this between tests to prevent pollution.\nexport function __resetForTesting() {\n if (typeof __tovaStyleRefs !== 'undefined') __tovaStyleRefs.clear();\n if (typeof __tovaFontRefs !== 'undefined') __tovaFontRefs.clear();\n __cspNonce = null;\n __tovaHeadTags.length = 0;\n pendingEffects.clear();\n currentEffect = null;\n currentOwner = null;\n effectStack.length = 0;\n ownerStack.length = 0;\n batchDepth = 0;\n flushing = false;\n _flushBuf.length = 0;\n __devtools_hooks = null;\n __errorBoundaryIdCounter = 0;\n currentErrorHandler = null;\n}\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\n // Invoke onBeforeUpdate callbacks for owners that have pending effects\n const ownersNotified = new Set();\n for (const effect of pendingEffects) {\n const owner = effect._owner;\n if (owner && owner._beforeUpdate && !ownersNotified.has(owner)) {\n ownersNotified.add(owner);\n for (const cb of owner._beforeUpdate) {\n try { cb(); } catch (e) { console.error('Tova: onBeforeUpdate error:', e); }\n }\n }\n }\n\n const toRun = pendingEffects;\n pendingEffects = new Set();\n // Sort by depth (parents first) to avoid redundant child re-runs\n // Reuse buffer to reduce GC pressure\n if (toRun.size > 1) {\n _flushBuf.length = 0;\n for (const effect of toRun) _flushBuf.push(effect);\n _flushBuf.sort((a, b) => (a._depth || 0) - (b._depth || 0));\n for (let i = 0; i < _flushBuf.length; i++) {\n if (!_flushBuf[i]._disposed) {\n _flushBuf[i]();\n }\n }\n _flushBuf.length = 0;\n } else {\n for (const effect of toRun) {\n if (!effect._disposed) {\n effect();\n }\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 (skip already-disposed)\n for (let i = root._children.length - 1; i >= 0; i--) {\n const child = root._children[i];\n if (!child._disposed && 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 // Compute depth for priority scheduling (parents flush before children)\n effect._depth = currentOwner ? (currentOwner._depth || 0) + 1 : 0;\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 };\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 notify._dirty = true;\n for (const sub of subscribers) {\n if (sub._isComputed) {\n if (!sub._dirty) sub(); // skip already-dirty computeds\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 };\n\n function recompute() {\n cleanupDeps(notify);\n\n effectStack.push(notify);\n currentEffect = notify;\n try {\n value = fn();\n dirty = false;\n notify._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\nexport function onBeforeUpdate(fn) {\n if (currentOwner && !currentOwner._disposed) {\n if (!currentOwner._beforeUpdate) currentOwner._beforeUpdate = [];\n currentOwner._beforeUpdate.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\nlet __errorBoundaryIdCounter = 0;\n\nexport function ErrorBoundary({ fallback, children, onError, onReset, onErrorCleared, retry = 0 }) {\n const [error, setError] = createSignal(null);\n const [retryCount, setRetryCount] = createSignal(0);\n const boundaryId = ++__errorBoundaryIdCounter;\n let lastErrorId = 0;\n\n function handleError(e) {\n const stack = buildComponentStack();\n const errorId = `EB${boundaryId}-${++lastErrorId}`;\n\n if (e && typeof e === 'object') {\n e.__tovaComponentStack = stack;\n e.__tovaErrorId = errorId;\n }\n\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, errorId, retryCount: retryCount() });\n }\n\n function resetBoundary() {\n setRetryCount(0);\n setError(null);\n if (onReset) onReset();\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 const errorId = err && typeof err === 'object' ? err.__tovaErrorId : null;\n return typeof fallback === 'function'\n ? fallback({\n error: err,\n errorId,\n retryCount: retryCount(),\n componentStack: err && typeof err === 'object' ? err.__tovaComponentStack : [],\n reset: resetBoundary,\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 // Children rendered successfully — fire onErrorCleared if we recovered from an error\n if (onErrorCleared && lastErrorId > 0 && retryCount() === 0) {\n queueMicrotask(() => onErrorCleared());\n }\n return childContent;\n },\n };\n\n return vnode;\n}\n\n// Built-in ErrorInfo component — renders a formatted error display\n// Usage: <ErrorBoundary fallback={fn(props) ErrorInfo(props)} />\nexport function ErrorInfo({ error, errorId, componentStack, reset, retryCount }) {\n const message = error instanceof Error ? error.message : String(error);\n const stackTrace = error instanceof Error && error.stack ? error.stack : '';\n const compStack = (componentStack || []).join(' > ');\n\n const children = [\n tova_el('h3', { style: { margin: '0 0 8px 0', color: '#e53e3e' } }, ['Something went wrong']),\n tova_el('p', { style: { margin: '4px 0', fontFamily: 'monospace', fontSize: '14px' } }, [message]),\n ];\n\n if (compStack) {\n children.push(\n tova_el('p', { style: { margin: '4px 0', fontSize: '12px', color: '#718096' } }, [\n 'Component: ', compStack\n ])\n );\n }\n\n if (errorId) {\n children.push(\n tova_el('p', { style: { margin: '4px 0', fontSize: '11px', color: '#a0aec0' } }, [\n 'Error ID: ', errorId\n ])\n );\n }\n\n if (stackTrace) {\n children.push(\n tova_el('details', { style: { marginTop: '8px', fontSize: '12px' } }, [\n tova_el('summary', { style: { cursor: 'pointer', color: '#4a5568' } }, ['Stack trace']),\n tova_el('pre', { style: { margin: '4px 0', padding: '8px', background: '#1a202c', color: '#e2e8f0', borderRadius: '4px', overflow: 'auto', fontSize: '11px', maxHeight: '200px' } }, [stackTrace]),\n ])\n );\n }\n\n if (reset) {\n children.push(\n tova_el('button', {\n style: { marginTop: '8px', padding: '6px 16px', background: '#3182ce', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '13px' },\n onClick: reset,\n }, [retryCount > 0 ? 'Retry again' : 'Try again'])\n );\n }\n\n return tova_el('div', {\n style: { padding: '16px', border: '1px solid #fed7d7', borderRadius: '8px', background: '#fff5f5', color: '#2d3748', fontFamily: 'system-ui, -apple-system, sans-serif' },\n role: 'alert',\n }, children);\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// Cleans up children from the target when the component unmounts.\n\nexport function Portal({ target, children }) {\n return {\n __tova: true,\n tag: '__portal',\n props: { target },\n children: children || [],\n _portalCleanup: true, // Signal to render() to register cleanup\n };\n}\n\n// ─── Suspense ────────────────────────────────────────────\n// Renders fallback while any child lazy() component is loading.\n// Usage: Suspense({ fallback: loadingEl, children: [LazyComp(props)] })\n\nconst SuspenseContext = createContext(null);\n\nexport function Suspense({ fallback, children }) {\n const [pending, setPending] = createSignal(0);\n const childContent = children && children.length === 1 ? children[0] : tova_fragment(children || []);\n\n const boundary = {\n register() {\n setPending(p => p + 1);\n },\n resolve() {\n setPending(p => Math.max(0, p - 1));\n },\n };\n\n return {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n compute: () => {\n provide(SuspenseContext, boundary);\n if (pending() > 0) {\n return typeof fallback === 'function' ? fallback() : fallback;\n }\n return childContent;\n },\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 // Signal is shared across all renders of this lazy component (not per-call)\n const [tick, setTick] = createSignal(0);\n\n return function LazyWrapper(props) {\n if (resolved) {\n return resolved(props);\n }\n\n // Check for Suspense boundary\n const suspense = inject(SuspenseContext);\n\n if (!promise) {\n if (suspense) suspense.register();\n promise = loader()\n .then(mod => {\n resolved = mod.default || mod;\n if (suspense) suspense.resolve();\n setTick(1);\n })\n .catch(e => {\n loadError = e;\n if (suspense) suspense.resolve();\n setTick(1);\n });\n }\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 (individual or Suspense-level)\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// ─── Head Component ──────────────────────────────────────\n// Declarative document head management.\n// Usage: Head({ children: [tova_el('title', {}, ['My Page']), tova_el('meta', {name: 'description', content: '...'})] })\n// Components can render <Head> to set title, meta, link, and script tags in <head>.\n// When the component unmounts, its head contributions are removed.\n\nconst __tovaHeadTags = [];\n\nexport function Head({ children }) {\n if (typeof document === 'undefined') return null;\n\n const addedElements = [];\n const childList = Array.isArray(children) ? children : [children];\n\n for (const child of childList) {\n if (!child || !child.__tova) continue;\n const tag = child.tag;\n const props = child.props || {};\n const text = child.children && child.children.length > 0 ? child.children.join('') : null;\n\n if (tag === 'title') {\n // Special case: update document.title directly\n const prevTitle = document.title;\n document.title = text || '';\n addedElements.push({ type: 'title', prev: prevTitle });\n } else {\n const el = document.createElement(tag);\n for (const [key, val] of Object.entries(props)) {\n if (key.startsWith('on') || key === 'key' || key === 'ref') continue;\n const attrName = key === 'className' ? 'class' : key;\n const attrVal = typeof val === 'function' ? val() : val;\n if (attrVal !== false && attrVal != null) {\n el.setAttribute(attrName, String(attrVal));\n }\n }\n if (text) el.textContent = text;\n document.head.appendChild(el);\n addedElements.push({ type: 'element', el });\n }\n }\n\n // Register cleanup: remove added elements when component unmounts\n if (currentOwner) {\n const cleanup = () => {\n for (const item of addedElements) {\n if (item.type === 'element') {\n if (typeof item.el.remove === 'function') {\n item.el.remove();\n } else if (item.el.parentNode && typeof item.el.parentNode.removeChild === 'function') {\n item.el.parentNode.removeChild(item.el);\n }\n } else if (item.type === 'title') {\n document.title = item.prev;\n }\n }\n };\n if (!currentOwner._cleanups) currentOwner._cleanups = [];\n currentOwner._cleanups.push(cleanup);\n }\n\n return null; // Head renders nothing in the component tree\n}\n\n// ─── createResource ──────────────────────────────────────\n// Async data fetching primitive integrated with signals.\n// Usage: const [data, { loading, error, refetch }] = createResource(fetcher)\n// Usage with source: const [data, { loading, error, refetch }] = createResource(sourceSignal, fetcher)\n// When source changes, fetcher is re-invoked automatically.\n\nexport function createResource(sourceOrFetcher, maybeFetcher) {\n let source, fetcher;\n if (typeof maybeFetcher === 'function') {\n source = sourceOrFetcher;\n fetcher = maybeFetcher;\n } else {\n source = null;\n fetcher = sourceOrFetcher;\n }\n\n const [data, setData] = createSignal(undefined);\n const [loading, setLoading] = createSignal(false);\n const [error, setError] = createSignal(undefined);\n let version = 0; // Guards against stale responses\n\n function doFetch(sourceVal) {\n const currentVersion = ++version;\n setLoading(true);\n setError(undefined);\n try {\n const result = source ? fetcher(sourceVal) : fetcher();\n if (result && typeof result.then === 'function') {\n result.then(\n (val) => {\n if (currentVersion === version) {\n setData(() => val);\n setLoading(false);\n }\n },\n (err) => {\n if (currentVersion === version) {\n setError(() => err);\n setLoading(false);\n }\n },\n );\n } else {\n // Synchronous fetcher\n if (currentVersion === version) {\n setData(() => result);\n setLoading(false);\n }\n }\n } catch (err) {\n if (currentVersion === version) {\n setError(() => err);\n setLoading(false);\n }\n }\n }\n\n function refetch() {\n const sourceVal = source ? (typeof source === 'function' ? source() : source) : undefined;\n doFetch(sourceVal);\n }\n\n // If source is provided, track it reactively\n if (source) {\n createEffect(() => {\n const sourceVal = typeof source === 'function' ? source() : source;\n if (sourceVal !== undefined && sourceVal !== null && sourceVal !== false) {\n doFetch(sourceVal);\n }\n });\n } else {\n // Fetch immediately\n doFetch();\n }\n\n return [data, { loading, error, refetch, mutate: setData }];\n}\n\n// ─── DOM Rendering ────────────────────────────────────────\n\n// CSP nonce — set via configureCSP({ nonce: '...' }) or auto-detected from\n// <meta name=\"csp-nonce\" content=\"...\">. Used for style tags to comply with\n// Content-Security-Policy headers.\nlet __cspNonce = null;\n\nexport function configureCSP(options) {\n if (options && options.nonce) __cspNonce = options.nonce;\n}\n\nfunction getCSPNonce() {\n if (__cspNonce) return __cspNonce;\n if (typeof document !== 'undefined' && typeof document.querySelector === 'function') {\n const meta = document.querySelector('meta[name=\"csp-nonce\"]');\n if (meta) {\n __cspNonce = meta.getAttribute('content');\n return __cspNonce;\n }\n }\n return null;\n}\n\n// Inject scoped CSS into the page with reference counting.\n// Style tags are created on first use and removed when no component instances reference them.\n// Supports CSP nonce for Content-Security-Policy compliance.\nconst __tovaStyleRefs = new Map(); // id → { el, count }\nexport function tova_inject_css(id, css) {\n const ref = __tovaStyleRefs.get(id);\n if (ref) {\n ref.count++;\n } else {\n const style = document.createElement('style');\n style.setAttribute('data-tova-style', id);\n const nonce = getCSPNonce();\n if (nonce) style.setAttribute('nonce', nonce);\n style.textContent = css;\n document.head.appendChild(style);\n __tovaStyleRefs.set(id, { el: style, count: 1 });\n }\n // Register cleanup on the current owner so unmount decrements the ref count\n if (currentOwner) {\n let cleaned = false;\n const cleanup = () => {\n if (cleaned) return;\n cleaned = true;\n const r = __tovaStyleRefs.get(id);\n if (r) {\n r.count--;\n if (r.count <= 0) {\n if (typeof r.el.remove === 'function') {\n r.el.remove();\n } else if (r.el.parentNode && typeof r.el.parentNode.removeChild === 'function') {\n r.el.parentNode.removeChild(r.el);\n }\n __tovaStyleRefs.delete(id);\n }\n }\n };\n if (!currentOwner._cleanups) currentOwner._cleanups = [];\n currentOwner._cleanups.push(cleanup);\n }\n}\n\n// Load a remote font stylesheet with reference counting.\n// When the last component using a font unmounts, the <link> is removed.\nconst __tovaFontRefs = new Map(); // id → { el, count }\nexport function __tova_load_font(id, url) {\n const ref = __tovaFontRefs.get(id);\n if (ref) {\n ref.count++;\n } else {\n const link = document.createElement('link');\n link.rel = 'stylesheet';\n link.href = url;\n link.dataset.tovaFont = id;\n document.head.appendChild(link);\n __tovaFontRefs.set(id, { el: link, count: 1 });\n }\n // Register cleanup on the current owner so unmount decrements the ref count\n if (currentOwner) {\n let cleaned = false;\n const cleanup = () => {\n if (cleaned) return;\n cleaned = true;\n const r = __tovaFontRefs.get(id);\n if (r) {\n r.count--;\n if (r.count <= 0) {\n if (typeof r.el.remove === 'function') {\n r.el.remove();\n } else if (r.el.parentNode && typeof r.el.parentNode.removeChild === 'function') {\n r.el.parentNode.removeChild(r.el);\n }\n __tovaFontRefs.delete(id);\n }\n }\n };\n if (!currentOwner._cleanups) currentOwner._cleanups = [];\n currentOwner._cleanups.push(cleanup);\n }\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// ─── Transitions ──────────────────────────────────────────\n// CSS transition directives for mount/unmount animations.\n// Usage: tova_transition(vnode, \"fade\", { duration: 300 })\n\nconst TRANSITION_DEFAULTS = {\n fade: { duration: 200, easing: 'ease' },\n slide: { duration: 300, easing: 'ease-out', axis: 'y' },\n scale: { duration: 200, easing: 'ease' },\n fly: { duration: 300, easing: 'ease-out', x: 0, y: -20 },\n};\n\nfunction getTransitionCSS(name, config, phase) {\n const opts = { ...TRANSITION_DEFAULTS[name], ...config };\n const dur = opts.duration + 'ms';\n const ease = opts.easing;\n\n switch (name) {\n case 'fade':\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { opacity: '0', transition: `opacity ${dur} ${ease}` };\n }\n return { opacity: '1', transition: `opacity ${dur} ${ease}` };\n\n case 'slide': {\n const axis = opts.axis || 'y';\n const prop = axis === 'x' ? 'translateX' : 'translateY';\n const dist = (opts.distance || 20) + 'px';\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { transform: `${prop}(${dist})`, opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n return { transform: `${prop}(0)`, opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n\n case 'scale':\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { transform: 'scale(0)', opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n return { transform: 'scale(1)', opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n\n case 'fly': {\n const x = (opts.x || 0) + 'px';\n const y = (opts.y || -20) + 'px';\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { transform: `translate(${x}, ${y})`, opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n return { transform: 'translate(0, 0)', opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n\n default:\n return {};\n }\n}\n\nexport function tova_transition(vnode, nameOrConfig, config = {}) {\n if (!vnode || !vnode.__tova) return vnode;\n\n // Directional transitions: tova_transition(vnode, { in: {...}, out: {...} })\n if (typeof nameOrConfig === 'object' && nameOrConfig !== null && !nameOrConfig.__tova && (nameOrConfig.in || nameOrConfig.out)) {\n vnode._transition = { directional: true, in: nameOrConfig.in, out: nameOrConfig.out };\n return vnode;\n }\n\n // Custom transition function: tova_transition(vnode, myTransitionFn, config)\n if (typeof nameOrConfig === 'function') {\n vnode._transition = { custom: nameOrConfig, config };\n return vnode;\n }\n\n // Built-in transition: tova_transition(vnode, \"fade\", config)\n vnode._transition = { name: nameOrConfig, config };\n return vnode;\n}\n\n// ─── TransitionGroup ──────────────────────────────────────\n// Animates enter, leave, and move for keyed list items.\n// Usage: TransitionGroup({ name: \"fade\", tag: \"ul\", children: items.map(i => ...) })\n// Each child MUST have a `key` prop.\n// Supports FLIP-based move animations when items reorder.\n\nexport function TransitionGroup({ name = 'fade', tag = 'div', config = {}, children, ...rest }) {\n const transName = name;\n const transConfig = config;\n const childList = Array.isArray(children) ? children : (children ? [children] : []);\n\n // Annotate each child vnode with the transition\n const annotated = childList.map(child => {\n if (child && child.__tova && !child._transition) {\n child._transition = { name: transName, config: transConfig };\n }\n return child;\n });\n\n // Wrap in a container element (default <div>)\n const wrapper = tova_el(tag, { ...rest, 'data-tova-transition-group': '' }, annotated);\n wrapper._transitionGroup = { name: transName, config: transConfig };\n return wrapper;\n}\n\n// ─── Actions ──────────────────────────────────────────────\n// use: directive support. Calls actionFn(el, param) after render.\n// Returns the wrapped vnode. The action lifecycle (update/destroy) is managed.\n\nexport function __tova_action(vnode, actionFn, param) {\n if (!vnode || !vnode.__tova) return vnode;\n if (!vnode._actions) vnode._actions = [];\n vnode._actions.push({ fn: actionFn, param });\n return vnode;\n}\n\n// Apply enter transition to a DOM element after render\nfunction applyEnterTransition(el, trans) {\n if (!trans) return;\n\n // Custom transition function\n if (trans.custom) {\n const result = trans.custom(el, trans.config || {}, 'enter');\n if (result && typeof result === 'object' && !result.then) {\n Object.assign(el.style, result);\n }\n return;\n }\n\n // Directional: use 'in' config for enter\n const name = trans.directional ? (trans.in ? trans.in.name : null) : trans.name;\n const config = trans.directional ? (trans.in ? trans.in.config : {}) : trans.config;\n if (!name) return;\n\n const fromStyles = getTransitionCSS(name, config, 'enter-from');\n const toStyles = getTransitionCSS(name, config, 'enter-to');\n\n // Set initial state\n Object.assign(el.style, fromStyles);\n\n // Force reflow, then apply target state\n void el.offsetHeight;\n Object.assign(el.style, toStyles);\n}\n\n// Apply leave transition and return a Promise that resolves when done\nfunction applyLeaveTransition(el, trans) {\n if (!trans) return Promise.resolve();\n\n // Custom transition function\n if (trans.custom) {\n const result = trans.custom(el, trans.config || {}, 'leave');\n if (result && typeof result.then === 'function') {\n // Race with timeout to prevent leaked promises from custom transitions\n const dur = (trans.config && trans.config.duration) || 5000;\n return Promise.race([result, new Promise(r => setTimeout(r, dur + 100))]);\n }\n if (result && typeof result === 'object') {\n Object.assign(el.style, result);\n }\n const dur = (trans.config && trans.config.duration) || 200;\n return new Promise(resolve => setTimeout(resolve, dur));\n }\n\n // Directional: use 'out' config for leave\n const name = trans.directional ? (trans.out ? trans.out.name : null) : trans.name;\n const config = trans.directional ? (trans.out ? trans.out.config : {}) : trans.config;\n if (!name) return Promise.resolve();\n\n const duration = (config && config.duration) || TRANSITION_DEFAULTS[name]?.duration || 200;\n const toStyles = getTransitionCSS(name, config, 'leave-to');\n Object.assign(el.style, toStyles);\n\n return new Promise(resolve => {\n const handler = () => {\n el.removeEventListener('transitionend', handler);\n resolve();\n };\n el.addEventListener('transitionend', handler);\n // Fallback timeout in case transitionend doesn't fire\n setTimeout(resolve, duration + 50);\n });\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 // If element has a leave transition, animate out before removing\n if (node.__tovaTransition && node.nodeType === 1) {\n const el = node;\n applyLeaveTransition(el, el.__tovaTransition).then(() => {\n disposeNode(el);\n if (el.parentNode) el.parentNode.removeChild(el);\n }).catch(() => {\n disposeNode(el);\n if (el.parentNode) el.parentNode.removeChild(el);\n });\n } else {\n disposeNode(node);\n if (node.parentNode) node.parentNode.removeChild(node);\n }\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 const portalNodes = [];\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 const rendered = render(child);\n targetEl.appendChild(rendered);\n portalNodes.push(rendered);\n }\n }\n });\n // Register cleanup: remove portal children when component unmounts\n if (currentOwner && !currentOwner._disposed) {\n if (!currentOwner._cleanups) currentOwner._cleanups = [];\n currentOwner._cleanups.push(() => {\n for (const node of portalNodes) {\n disposeNode(node);\n if (node.parentNode) node.parentNode.removeChild(node);\n }\n portalNodes.length = 0;\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 // Apply enter transition if present\n if (vnode._transition) {\n el.__tovaTransition = vnode._transition;\n applyEnterTransition(el, vnode._transition);\n }\n\n // Apply use: actions if present\n if (vnode._actions && vnode._actions.length > 0) {\n for (const action of vnode._actions) {\n const paramValue = typeof action.param === 'function' ? action.param() : action.param;\n const result = action.fn(el, paramValue);\n if (result) {\n // If param is reactive, set up effect for updates\n if (typeof action.param === 'function') {\n createEffect(() => {\n const newVal = action.param();\n if (result.update) result.update(newVal);\n });\n }\n // Register destroy on cleanup\n if (result.destroy) {\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._cleanups.push(result.destroy);\n }\n }\n }\n }\n }\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 if (typeof value === 'object' && value !== null && value.handler) {\n el.addEventListener(eventName, value.handler, value.options);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value.handler;\n el.__handlerOptions = el.__handlerOptions || {};\n el.__handlerOptions[eventName] = value.options;\n } else {\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n }\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 === 'dangerouslySetInnerHTML') {\n // Explicit unsafe HTML injection — requires {__html: \"...\"} format\n const html = typeof val === 'object' && val !== null ? val.__html || '' : '';\n if (__DEV__ && html) {\n console.warn('Tova: dangerouslySetInnerHTML bypasses XSS protection. Ensure content is sanitized.');\n }\n if (el.innerHTML !== html) el.innerHTML = html;\n } else if (key === 'innerHTML') {\n // Blocked: use dangerouslySetInnerHTML instead\n if (__DEV__) {\n console.error('Tova: innerHTML is not allowed. Use dangerouslySetInnerHTML={{__html: value}} to acknowledge XSS risk.');\n }\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 // Delta update: only remove properties that were in previous style but not in new\n if (el.__prevStyle) {\n for (const prop of Object.keys(el.__prevStyle)) {\n if (!(prop in val)) el.style.removeProperty(prop);\n }\n }\n el.__prevStyle = { ...val };\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 if (typeof value === 'object' && value !== null && value.handler) {\n const oldHandler = el.__handlers && el.__handlers[eventName];\n if (oldHandler !== value.handler) {\n const oldOpts = el.__handlerOptions && el.__handlerOptions[eventName];\n if (oldHandler) el.removeEventListener(eventName, oldHandler, oldOpts);\n el.addEventListener(eventName, value.handler, value.options);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value.handler;\n el.__handlerOptions = el.__handlerOptions || {};\n el.__handlerOptions[eventName] = value.options;\n }\n } else {\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 }\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// ─── Longest Increasing Subsequence (O(n log n)) ────────\n// Used by keyed reconciliation to minimize DOM moves.\n\nfunction longestIncreasingSubsequence(arr) {\n const n = arr.length;\n if (n === 0) return [];\n\n // tails[i] = index in arr of smallest tail element for IS of length i+1\n const tails = [];\n // parent[i] = index in arr of predecessor of arr[i] in the LIS\n const parent = new Array(n).fill(-1);\n // indices[i] = index in arr of tails[i]\n const indices = [];\n\n for (let i = 0; i < n; i++) {\n const val = arr[i];\n if (val < 0) continue; // skip removed items (marker -1)\n\n // Binary search for the insertion point\n let lo = 0, hi = tails.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (tails[mid] < val) lo = mid + 1;\n else hi = mid;\n }\n\n tails[lo] = val;\n indices[lo] = i;\n\n if (lo > 0) {\n parent[i] = indices[lo - 1];\n }\n }\n\n // Reconstruct\n const result = new Array(tails.length);\n let k = indices[tails.length - 1];\n for (let i = tails.length - 1; i >= 0; i--) {\n result[i] = k;\n k = parent[k];\n }\n\n return result;\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 // LIS-based reorder: compute old positions, find LIS, only move non-LIS nodes\n const oldPosMap = new Map();\n for (let i = 0; i < oldNodes.length; i++) {\n oldPosMap.set(oldNodes[i], i);\n }\n const positions = newNodes.map(n => oldPosMap.has(n) ? oldPosMap.get(n) : -1);\n const lisIndices = new Set(longestIncreasingSubsequence(positions));\n\n // Insert nodes: only move nodes not in the LIS\n let cursor = marker.nextSibling;\n for (let i = 0; i < newNodes.length; i++) {\n const node = newNodes[i];\n if (lisIndices.has(i) && 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 (skip identical vnodes)\n const patchCount = Math.min(oldCount, newCount);\n for (let i = 0; i < patchCount; i++) {\n if (oldNodes[i] === newChildren[i]) continue;\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 // LIS-based reorder for element children\n const logicalAfterRemove = getLogicalChildren(parent);\n const oldPosMap = new Map();\n for (let i = 0; i < logicalAfterRemove.length; i++) {\n oldPosMap.set(logicalAfterRemove[i], i);\n }\n const positions = newNodes.map(n => oldPosMap.has(n) ? oldPosMap.get(n) : -1);\n const lisIndices = new Set(longestIncreasingSubsequence(positions));\n\n for (let i = 0; i < newNodes.length; i++) {\n const expected = newNodes[i];\n if (!lisIndices.has(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\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 if (typeof value === 'object' && value !== null && value.handler) {\n el.addEventListener(eventName, value.handler, value.options);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value.handler;\n el.__handlerOptions = el.__handlerOptions || {};\n el.__handlerOptions[eventName] = value.options;\n } else {\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n }\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 if (typeof container.replaceChildren === 'function') {\n container.replaceChildren();\n } else {\n while (container.firstChild) container.removeChild(container.firstChild);\n }\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\n// ─── Form Handling ──────────────────────────────────────────\n// Reactive form primitives with field-level validation.\n// Usage:\n// const form = createForm({\n// fields: { email: { initial: '', validate: (v) => v.includes('@') ? null : 'Invalid email' } },\n// onSubmit: async (values) => { await server.register(values); }\n// });\n// <input bind:value={form.field('email').value} />\n// {form.field('email').error()}\n// <button on:click={form.submit} disabled={form.submitting()}>Submit</button>\n\nexport function createForm({ fields = {}, onSubmit, validateOnChange = true, validateOnBlur = true }) {\n const fieldSignals = {};\n const errorSignals = {};\n const touchedSignals = {};\n const [submitting, setSubmitting] = createSignal(false);\n const [submitError, setSubmitError] = createSignal(null);\n const [submitCount, setSubmitCount] = createSignal(0);\n\n // Initialize field signals\n for (const [name, config] of Object.entries(fields)) {\n const initial = config.initial !== undefined ? config.initial : '';\n const [value, setValue] = createSignal(initial);\n const [error, setError] = createSignal(null);\n const [touched, setTouched] = createSignal(false);\n fieldSignals[name] = { value, setValue, validate: config.validate || null, initial };\n errorSignals[name] = { error, setError };\n touchedSignals[name] = { touched, setTouched };\n }\n\n function validateField(name) {\n const f = fieldSignals[name];\n const e = errorSignals[name];\n if (!f || !e || !f.validate) return null;\n const err = f.validate(f.value());\n e.setError(err);\n return err;\n }\n\n function validateAll() {\n let hasErrors = false;\n for (const name of Object.keys(fieldSignals)) {\n const err = validateField(name);\n if (err) hasErrors = true;\n }\n return !hasErrors;\n }\n\n function field(name) {\n const f = fieldSignals[name];\n const e = errorSignals[name];\n const t = touchedSignals[name];\n if (!f) throw new Error(`Tova form: unknown field \"${name}\"`);\n return {\n value: f.value,\n error: e.error,\n touched: t.touched,\n set(val) {\n f.setValue(val);\n if (validateOnChange && t.touched()) validateField(name);\n },\n blur() {\n t.setTouched(true);\n if (validateOnBlur) validateField(name);\n },\n validate() { return validateField(name); },\n };\n }\n\n function values() {\n const result = {};\n for (const [name, f] of Object.entries(fieldSignals)) {\n result[name] = f.value();\n }\n return result;\n }\n\n function reset() {\n for (const [name, f] of Object.entries(fieldSignals)) {\n f.setValue(f.initial);\n errorSignals[name].setError(null);\n touchedSignals[name].setTouched(false);\n }\n setSubmitError(null);\n }\n\n async function submit(e) {\n if (e && typeof e.preventDefault === 'function') e.preventDefault();\n // Touch all fields\n for (const name of Object.keys(touchedSignals)) {\n touchedSignals[name].setTouched(true);\n }\n if (!validateAll()) return;\n if (!onSubmit) return;\n setSubmitting(true);\n setSubmitError(null);\n setSubmitCount(c => c + 1);\n try {\n await onSubmit(values());\n } catch (err) {\n setSubmitError(err);\n } finally {\n setSubmitting(false);\n }\n }\n\n const isValid = createComputed(() => {\n for (const name of Object.keys(errorSignals)) {\n if (errorSignals[name].error()) return false;\n }\n return true;\n });\n\n const isDirty = createComputed(() => {\n for (const [name, f] of Object.entries(fieldSignals)) {\n if (f.value() !== f.initial) return true;\n }\n return false;\n });\n\n return {\n field,\n values,\n reset,\n submit,\n submitting,\n submitError,\n submitCount,\n isValid,\n isDirty,\n validate: validateAll,\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\n// Reusable array for flush cycle — avoids allocation on every flush\nlet _flushBuf = [];\n\n// Reset module-level mutable state for test isolation.\n// Bun runs all test files in a single process, so Maps like __tovaStyleRefs\n// accumulate entries across files. Call this between tests to prevent pollution.\nexport function __resetForTesting() {\n if (typeof __tovaStyleRefs !== 'undefined') __tovaStyleRefs.clear();\n if (typeof __tovaFontRefs !== 'undefined') __tovaFontRefs.clear();\n __cspNonce = null;\n __tovaHeadTags.length = 0;\n pendingEffects.clear();\n currentEffect = null;\n currentOwner = null;\n effectStack.length = 0;\n ownerStack.length = 0;\n batchDepth = 0;\n flushing = false;\n _flushBuf.length = 0;\n __devtools_hooks = null;\n __errorBoundaryIdCounter = 0;\n currentErrorHandler = null;\n}\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\n // Invoke onBeforeUpdate callbacks for owners that have pending effects\n const ownersNotified = new Set();\n for (const effect of pendingEffects) {\n const owner = effect._owner;\n if (owner && owner._beforeUpdate && !ownersNotified.has(owner)) {\n ownersNotified.add(owner);\n for (const cb of owner._beforeUpdate) {\n try { cb(); } catch (e) { console.error('Tova: onBeforeUpdate error:', e); }\n }\n }\n }\n\n const toRun = pendingEffects;\n pendingEffects = new Set();\n // Sort by depth (parents first) to avoid redundant child re-runs\n // Reuse buffer to reduce GC pressure\n if (toRun.size > 1) {\n _flushBuf.length = 0;\n for (const effect of toRun) _flushBuf.push(effect);\n _flushBuf.sort((a, b) => (a._depth || 0) - (b._depth || 0));\n for (let i = 0; i < _flushBuf.length; i++) {\n if (!_flushBuf[i]._disposed) {\n _flushBuf[i]();\n }\n }\n _flushBuf.length = 0;\n } else {\n for (const effect of toRun) {\n if (!effect._disposed) {\n effect();\n }\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 (skip already-disposed)\n for (let i = root._children.length - 1; i >= 0; i--) {\n const child = root._children[i];\n if (!child._disposed && 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 // Compute depth for priority scheduling (parents flush before children)\n effect._depth = currentOwner ? (currentOwner._depth || 0) + 1 : 0;\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 };\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 notify._dirty = true;\n for (const sub of subscribers) {\n if (sub._isComputed) {\n if (!sub._dirty) sub(); // skip already-dirty computeds\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 };\n\n function recompute() {\n cleanupDeps(notify);\n\n effectStack.push(notify);\n currentEffect = notify;\n try {\n value = fn();\n dirty = false;\n notify._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 if (owner && owner._disposed) return;\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\nexport function onBeforeUpdate(fn) {\n if (currentOwner && !currentOwner._disposed) {\n if (!currentOwner._beforeUpdate) currentOwner._beforeUpdate = [];\n currentOwner._beforeUpdate.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\nlet __errorBoundaryIdCounter = 0;\n\nexport function ErrorBoundary({ fallback, children, onError, onReset, onErrorCleared, retry = 0 }) {\n const [error, setError] = createSignal(null);\n const [retryCount, setRetryCount] = createSignal(0);\n const boundaryId = ++__errorBoundaryIdCounter;\n let lastErrorId = 0;\n\n function handleError(e) {\n const stack = buildComponentStack();\n const errorId = `EB${boundaryId}-${++lastErrorId}`;\n\n if (e && typeof e === 'object') {\n e.__tovaComponentStack = stack;\n e.__tovaErrorId = errorId;\n }\n\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, errorId, retryCount: retryCount() });\n }\n\n function resetBoundary() {\n setRetryCount(0);\n setError(null);\n if (onReset) onReset();\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 const errorId = err && typeof err === 'object' ? err.__tovaErrorId : null;\n return typeof fallback === 'function'\n ? fallback({\n error: err,\n errorId,\n retryCount: retryCount(),\n componentStack: err && typeof err === 'object' ? err.__tovaComponentStack : [],\n reset: resetBoundary,\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 // Children rendered successfully — fire onErrorCleared if we recovered from an error\n if (onErrorCleared && lastErrorId > 0 && retryCount() === 0) {\n queueMicrotask(() => onErrorCleared());\n }\n return childContent;\n },\n };\n\n return vnode;\n}\n\n// Built-in ErrorInfo component — renders a formatted error display\n// Usage: <ErrorBoundary fallback={fn(props) ErrorInfo(props)} />\nexport function ErrorInfo({ error, errorId, componentStack, reset, retryCount }) {\n const message = error instanceof Error ? error.message : String(error);\n const stackTrace = error instanceof Error && error.stack ? error.stack : '';\n const compStack = (componentStack || []).join(' > ');\n\n const children = [\n tova_el('h3', { style: { margin: '0 0 8px 0', color: '#e53e3e' } }, ['Something went wrong']),\n tova_el('p', { style: { margin: '4px 0', fontFamily: 'monospace', fontSize: '14px' } }, [message]),\n ];\n\n if (compStack) {\n children.push(\n tova_el('p', { style: { margin: '4px 0', fontSize: '12px', color: '#718096' } }, [\n 'Component: ', compStack\n ])\n );\n }\n\n if (errorId) {\n children.push(\n tova_el('p', { style: { margin: '4px 0', fontSize: '11px', color: '#a0aec0' } }, [\n 'Error ID: ', errorId\n ])\n );\n }\n\n if (stackTrace) {\n children.push(\n tova_el('details', { style: { marginTop: '8px', fontSize: '12px' } }, [\n tova_el('summary', { style: { cursor: 'pointer', color: '#4a5568' } }, ['Stack trace']),\n tova_el('pre', { style: { margin: '4px 0', padding: '8px', background: '#1a202c', color: '#e2e8f0', borderRadius: '4px', overflow: 'auto', fontSize: '11px', maxHeight: '200px' } }, [stackTrace]),\n ])\n );\n }\n\n if (reset) {\n children.push(\n tova_el('button', {\n style: { marginTop: '8px', padding: '6px 16px', background: '#3182ce', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '13px' },\n onClick: reset,\n }, [retryCount > 0 ? 'Retry again' : 'Try again'])\n );\n }\n\n return tova_el('div', {\n style: { padding: '16px', border: '1px solid #fed7d7', borderRadius: '8px', background: '#fff5f5', color: '#2d3748', fontFamily: 'system-ui, -apple-system, sans-serif' },\n role: 'alert',\n }, children);\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// Cleans up children from the target when the component unmounts.\n\nexport function Portal({ target, children }) {\n return {\n __tova: true,\n tag: '__portal',\n props: { target },\n children: children || [],\n _portalCleanup: true, // Signal to render() to register cleanup\n };\n}\n\n// ─── Suspense ────────────────────────────────────────────\n// Renders fallback while any child lazy() component is loading.\n// Usage: Suspense({ fallback: loadingEl, children: [LazyComp(props)] })\n\nconst SuspenseContext = createContext(null);\n\nexport function Suspense({ fallback, children }) {\n const [pending, setPending] = createSignal(0);\n const childContent = children && children.length === 1 ? children[0] : tova_fragment(children || []);\n\n const boundary = {\n register() {\n setPending(p => p + 1);\n },\n resolve() {\n setPending(p => Math.max(0, p - 1));\n },\n };\n\n return {\n __tova: true,\n tag: '__dynamic',\n props: {},\n children: [],\n compute: () => {\n provide(SuspenseContext, boundary);\n if (pending() > 0) {\n return typeof fallback === 'function' ? fallback() : fallback;\n }\n return childContent;\n },\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 // Signal is shared across all renders of this lazy component (not per-call)\n const [tick, setTick] = createSignal(0);\n\n return function LazyWrapper(props) {\n if (resolved) {\n return resolved(props);\n }\n\n // Check for Suspense boundary\n const suspense = inject(SuspenseContext);\n\n if (!promise) {\n if (suspense) suspense.register();\n promise = loader()\n .then(mod => {\n resolved = mod.default || mod;\n if (suspense) suspense.resolve();\n setTick(1);\n })\n .catch(e => {\n loadError = e;\n if (suspense) suspense.resolve();\n setTick(1);\n });\n }\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 (individual or Suspense-level)\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// ─── Head Component ──────────────────────────────────────\n// Declarative document head management.\n// Usage: Head({ children: [tova_el('title', {}, ['My Page']), tova_el('meta', {name: 'description', content: '...'})] })\n// Components can render <Head> to set title, meta, link, and script tags in <head>.\n// When the component unmounts, its head contributions are removed.\n\nconst __tovaHeadTags = [];\n\nexport function Head({ children }) {\n if (typeof document === 'undefined') return null;\n\n const addedElements = [];\n const childList = Array.isArray(children) ? children : [children];\n\n for (const child of childList) {\n if (!child || !child.__tova) continue;\n const tag = child.tag;\n const props = child.props || {};\n const text = child.children && child.children.length > 0 ? child.children.join('') : null;\n\n if (tag === 'title') {\n // Special case: update document.title directly\n const prevTitle = document.title;\n document.title = text || '';\n addedElements.push({ type: 'title', prev: prevTitle });\n } else {\n const el = document.createElement(tag);\n for (const [key, val] of Object.entries(props)) {\n if (key.startsWith('on') || key === 'key' || key === 'ref') continue;\n const attrName = key === 'className' ? 'class' : key;\n const attrVal = typeof val === 'function' ? val() : val;\n if (attrVal !== false && attrVal != null) {\n el.setAttribute(attrName, String(attrVal));\n }\n }\n if (text) el.textContent = text;\n document.head.appendChild(el);\n addedElements.push({ type: 'element', el });\n }\n }\n\n // Register cleanup: remove added elements when component unmounts\n if (currentOwner) {\n const cleanup = () => {\n for (const item of addedElements) {\n if (item.type === 'element') {\n if (typeof item.el.remove === 'function') {\n item.el.remove();\n } else if (item.el.parentNode && typeof item.el.parentNode.removeChild === 'function') {\n item.el.parentNode.removeChild(item.el);\n }\n } else if (item.type === 'title') {\n document.title = item.prev;\n }\n }\n };\n if (!currentOwner._cleanups) currentOwner._cleanups = [];\n currentOwner._cleanups.push(cleanup);\n }\n\n return null; // Head renders nothing in the component tree\n}\n\n// ─── createResource ──────────────────────────────────────\n// Async data fetching primitive integrated with signals.\n// Usage: const [data, { loading, error, refetch }] = createResource(fetcher)\n// Usage with source: const [data, { loading, error, refetch }] = createResource(sourceSignal, fetcher)\n// When source changes, fetcher is re-invoked automatically.\n\nexport function createResource(sourceOrFetcher, maybeFetcher) {\n let source, fetcher;\n if (typeof maybeFetcher === 'function') {\n source = sourceOrFetcher;\n fetcher = maybeFetcher;\n } else {\n source = null;\n fetcher = sourceOrFetcher;\n }\n\n const [data, setData] = createSignal(undefined);\n const [loading, setLoading] = createSignal(false);\n const [error, setError] = createSignal(undefined);\n let version = 0; // Guards against stale responses\n\n function doFetch(sourceVal) {\n const currentVersion = ++version;\n setLoading(true);\n setError(undefined);\n try {\n const result = source ? fetcher(sourceVal) : fetcher();\n if (result && typeof result.then === 'function') {\n result.then(\n (val) => {\n if (currentVersion === version) {\n setData(() => val);\n setLoading(false);\n }\n },\n (err) => {\n if (currentVersion === version) {\n setError(() => err);\n setLoading(false);\n }\n },\n );\n } else {\n // Synchronous fetcher\n if (currentVersion === version) {\n setData(() => result);\n setLoading(false);\n }\n }\n } catch (err) {\n if (currentVersion === version) {\n setError(() => err);\n setLoading(false);\n }\n }\n }\n\n function refetch() {\n const sourceVal = source ? (typeof source === 'function' ? source() : source) : undefined;\n doFetch(sourceVal);\n }\n\n // If source is provided, track it reactively\n if (source) {\n createEffect(() => {\n const sourceVal = typeof source === 'function' ? source() : source;\n if (sourceVal !== undefined && sourceVal !== null && sourceVal !== false) {\n doFetch(sourceVal);\n }\n });\n } else {\n // Fetch immediately\n doFetch();\n }\n\n return [data, { loading, error, refetch, mutate: setData }];\n}\n\n// ─── DOM Rendering ────────────────────────────────────────\n\n// CSP nonce — set via configureCSP({ nonce: '...' }) or auto-detected from\n// <meta name=\"csp-nonce\" content=\"...\">. Used for style tags to comply with\n// Content-Security-Policy headers.\nlet __cspNonce = null;\n\nexport function configureCSP(options) {\n if (options && options.nonce) __cspNonce = options.nonce;\n}\n\nfunction getCSPNonce() {\n if (__cspNonce) return __cspNonce;\n if (typeof document !== 'undefined' && typeof document.querySelector === 'function') {\n const meta = document.querySelector('meta[name=\"csp-nonce\"]');\n if (meta) {\n __cspNonce = meta.getAttribute('content');\n return __cspNonce;\n }\n }\n return null;\n}\n\n// Inject scoped CSS into the page with reference counting.\n// Style tags are created on first use and removed when no component instances reference them.\n// Supports CSP nonce for Content-Security-Policy compliance.\nconst __tovaStyleRefs = new Map(); // id → { el, count }\nexport function tova_inject_css(id, css) {\n const ref = __tovaStyleRefs.get(id);\n if (ref) {\n ref.count++;\n } else {\n const style = document.createElement('style');\n style.setAttribute('data-tova-style', id);\n const nonce = getCSPNonce();\n if (nonce) style.setAttribute('nonce', nonce);\n style.textContent = css;\n document.head.appendChild(style);\n __tovaStyleRefs.set(id, { el: style, count: 1 });\n }\n // Register cleanup on the current owner so unmount decrements the ref count\n if (currentOwner) {\n let cleaned = false;\n const cleanup = () => {\n if (cleaned) return;\n cleaned = true;\n const r = __tovaStyleRefs.get(id);\n if (r) {\n r.count--;\n if (r.count <= 0) {\n if (typeof r.el.remove === 'function') {\n r.el.remove();\n } else if (r.el.parentNode && typeof r.el.parentNode.removeChild === 'function') {\n r.el.parentNode.removeChild(r.el);\n }\n __tovaStyleRefs.delete(id);\n }\n }\n };\n if (!currentOwner._cleanups) currentOwner._cleanups = [];\n currentOwner._cleanups.push(cleanup);\n }\n}\n\n// Load a remote font stylesheet with reference counting.\n// When the last component using a font unmounts, the <link> is removed.\nconst __tovaFontRefs = new Map(); // id → { el, count }\nexport function __tova_load_font(id, url) {\n const ref = __tovaFontRefs.get(id);\n if (ref) {\n ref.count++;\n } else {\n const link = document.createElement('link');\n link.rel = 'stylesheet';\n link.href = url;\n link.dataset.tovaFont = id;\n document.head.appendChild(link);\n __tovaFontRefs.set(id, { el: link, count: 1 });\n }\n // Register cleanup on the current owner so unmount decrements the ref count\n if (currentOwner) {\n let cleaned = false;\n const cleanup = () => {\n if (cleaned) return;\n cleaned = true;\n const r = __tovaFontRefs.get(id);\n if (r) {\n r.count--;\n if (r.count <= 0) {\n if (typeof r.el.remove === 'function') {\n r.el.remove();\n } else if (r.el.parentNode && typeof r.el.parentNode.removeChild === 'function') {\n r.el.parentNode.removeChild(r.el);\n }\n __tovaFontRefs.delete(id);\n }\n }\n };\n if (!currentOwner._cleanups) currentOwner._cleanups = [];\n currentOwner._cleanups.push(cleanup);\n }\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// ─── Transitions ──────────────────────────────────────────\n// CSS transition directives for mount/unmount animations.\n// Usage: tova_transition(vnode, \"fade\", { duration: 300 })\n\nconst TRANSITION_DEFAULTS = {\n fade: { duration: 200, easing: 'ease' },\n slide: { duration: 300, easing: 'ease-out', axis: 'y' },\n scale: { duration: 200, easing: 'ease' },\n fly: { duration: 300, easing: 'ease-out', x: 0, y: -20 },\n};\n\nfunction getTransitionCSS(name, config, phase) {\n const opts = { ...TRANSITION_DEFAULTS[name], ...config };\n const dur = opts.duration + 'ms';\n const ease = opts.easing;\n\n switch (name) {\n case 'fade':\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { opacity: '0', transition: `opacity ${dur} ${ease}` };\n }\n return { opacity: '1', transition: `opacity ${dur} ${ease}` };\n\n case 'slide': {\n const axis = opts.axis || 'y';\n const prop = axis === 'x' ? 'translateX' : 'translateY';\n const dist = (opts.distance || 20) + 'px';\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { transform: `${prop}(${dist})`, opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n return { transform: `${prop}(0)`, opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n\n case 'scale':\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { transform: 'scale(0)', opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n return { transform: 'scale(1)', opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n\n case 'fly': {\n const x = (opts.x || 0) + 'px';\n const y = (opts.y || -20) + 'px';\n if (phase === 'enter-from' || phase === 'leave-to') {\n return { transform: `translate(${x}, ${y})`, opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n return { transform: 'translate(0, 0)', opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };\n }\n\n default:\n return {};\n }\n}\n\nexport function tova_transition(vnode, nameOrConfig, config = {}) {\n if (!vnode || !vnode.__tova) return vnode;\n\n // Directional transitions: tova_transition(vnode, { in: {...}, out: {...} })\n if (typeof nameOrConfig === 'object' && nameOrConfig !== null && !nameOrConfig.__tova && (nameOrConfig.in || nameOrConfig.out)) {\n vnode._transition = { directional: true, in: nameOrConfig.in, out: nameOrConfig.out };\n return vnode;\n }\n\n // Custom transition function: tova_transition(vnode, myTransitionFn, config)\n if (typeof nameOrConfig === 'function') {\n vnode._transition = { custom: nameOrConfig, config };\n return vnode;\n }\n\n // Built-in transition: tova_transition(vnode, \"fade\", config)\n vnode._transition = { name: nameOrConfig, config };\n return vnode;\n}\n\n// ─── TransitionGroup ──────────────────────────────────────\n// Animates enter, leave, and move for keyed list items.\n// Usage: TransitionGroup({ name: \"fade\", tag: \"ul\", children: items.map(i => ...) })\n// Each child MUST have a `key` prop.\n// Supports FLIP-based move animations when items reorder.\n\nexport function TransitionGroup({ name = 'fade', tag = 'div', config = {}, children, ...rest }) {\n const transName = name;\n const transConfig = config;\n const childList = Array.isArray(children) ? children : (children ? [children] : []);\n\n // Annotate each child vnode with the transition\n const annotated = childList.map(child => {\n if (child && child.__tova && !child._transition) {\n child._transition = { name: transName, config: transConfig };\n }\n return child;\n });\n\n // Wrap in a container element (default <div>)\n const wrapper = tova_el(tag, { ...rest, 'data-tova-transition-group': '' }, annotated);\n wrapper._transitionGroup = { name: transName, config: transConfig };\n return wrapper;\n}\n\n// ─── Actions ──────────────────────────────────────────────\n// use: directive support. Calls actionFn(el, param) after render.\n// Returns the wrapped vnode. The action lifecycle (update/destroy) is managed.\n\nexport function __tova_action(vnode, actionFn, param) {\n if (!vnode || !vnode.__tova) return vnode;\n if (!vnode._actions) vnode._actions = [];\n vnode._actions.push({ fn: actionFn, param });\n return vnode;\n}\n\n// Apply enter transition to a DOM element after render\nfunction applyEnterTransition(el, trans) {\n if (!trans) return;\n\n // Custom transition function\n if (trans.custom) {\n const result = trans.custom(el, trans.config || {}, 'enter');\n if (result && typeof result === 'object' && !result.then) {\n Object.assign(el.style, result);\n }\n return;\n }\n\n // Directional: use 'in' config for enter\n const name = trans.directional ? (trans.in ? trans.in.name : null) : trans.name;\n const config = trans.directional ? (trans.in ? trans.in.config : {}) : trans.config;\n if (!name) return;\n\n const fromStyles = getTransitionCSS(name, config, 'enter-from');\n const toStyles = getTransitionCSS(name, config, 'enter-to');\n\n // Set initial state\n Object.assign(el.style, fromStyles);\n\n // Force reflow, then apply target state\n void el.offsetHeight;\n Object.assign(el.style, toStyles);\n}\n\n// Apply leave transition and return a Promise that resolves when done\nfunction applyLeaveTransition(el, trans) {\n if (!trans) return Promise.resolve();\n\n // Custom transition function\n if (trans.custom) {\n const result = trans.custom(el, trans.config || {}, 'leave');\n if (result && typeof result.then === 'function') {\n // Race with timeout to prevent leaked promises from custom transitions\n const dur = (trans.config && trans.config.duration) || 5000;\n return Promise.race([result, new Promise(r => setTimeout(r, dur + 100))]);\n }\n if (result && typeof result === 'object') {\n Object.assign(el.style, result);\n }\n const dur = (trans.config && trans.config.duration) || 200;\n return new Promise(resolve => setTimeout(resolve, dur));\n }\n\n // Directional: use 'out' config for leave\n const name = trans.directional ? (trans.out ? trans.out.name : null) : trans.name;\n const config = trans.directional ? (trans.out ? trans.out.config : {}) : trans.config;\n if (!name) return Promise.resolve();\n\n const duration = (config && config.duration) || TRANSITION_DEFAULTS[name]?.duration || 200;\n const toStyles = getTransitionCSS(name, config, 'leave-to');\n Object.assign(el.style, toStyles);\n\n return new Promise(resolve => {\n const handler = () => {\n el.removeEventListener('transitionend', handler);\n resolve();\n };\n el.addEventListener('transitionend', handler);\n // Fallback timeout in case transitionend doesn't fire\n setTimeout(resolve, duration + 50);\n });\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 // If element has a leave transition, animate out before removing\n if (node.__tovaTransition && node.nodeType === 1) {\n const el = node;\n applyLeaveTransition(el, el.__tovaTransition).then(() => {\n disposeNode(el);\n if (el.parentNode) el.parentNode.removeChild(el);\n }).catch(() => {\n disposeNode(el);\n if (el.parentNode) el.parentNode.removeChild(el);\n });\n } else {\n disposeNode(node);\n if (node.parentNode) node.parentNode.removeChild(node);\n }\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 const portalNodes = [];\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 const rendered = render(child);\n targetEl.appendChild(rendered);\n portalNodes.push(rendered);\n }\n }\n });\n // Register cleanup: remove portal children when component unmounts\n if (currentOwner && !currentOwner._disposed) {\n if (!currentOwner._cleanups) currentOwner._cleanups = [];\n currentOwner._cleanups.push(() => {\n for (const node of portalNodes) {\n disposeNode(node);\n if (node.parentNode) node.parentNode.removeChild(node);\n }\n portalNodes.length = 0;\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 // Apply enter transition if present\n if (vnode._transition) {\n el.__tovaTransition = vnode._transition;\n applyEnterTransition(el, vnode._transition);\n }\n\n // Apply use: actions if present\n if (vnode._actions && vnode._actions.length > 0) {\n for (const action of vnode._actions) {\n const paramValue = typeof action.param === 'function' ? action.param() : action.param;\n const result = action.fn(el, paramValue);\n if (result) {\n // If param is reactive, set up effect for updates\n if (typeof action.param === 'function') {\n createEffect(() => {\n const newVal = action.param();\n if (result.update) result.update(newVal);\n });\n }\n // Register destroy on cleanup\n if (result.destroy) {\n if (currentOwner && !currentOwner._disposed) {\n currentOwner._cleanups.push(result.destroy);\n }\n }\n }\n }\n }\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 if (typeof value === 'object' && value !== null && value.handler) {\n el.addEventListener(eventName, value.handler, value.options);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value.handler;\n el.__handlerOptions = el.__handlerOptions || {};\n el.__handlerOptions[eventName] = value.options;\n } else {\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n }\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 === 'dangerouslySetInnerHTML') {\n // Explicit unsafe HTML injection — requires {__html: \"...\"} format\n const html = typeof val === 'object' && val !== null ? val.__html || '' : '';\n if (__DEV__ && html) {\n console.warn('Tova: dangerouslySetInnerHTML bypasses XSS protection. Ensure content is sanitized.');\n }\n if (el.innerHTML !== html) el.innerHTML = html;\n } else if (key === 'innerHTML') {\n // Blocked: use dangerouslySetInnerHTML instead\n if (__DEV__) {\n console.error('Tova: innerHTML is not allowed. Use dangerouslySetInnerHTML={{__html: value}} to acknowledge XSS risk.');\n }\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 // Delta update: only remove properties that were in previous style but not in new\n if (el.__prevStyle) {\n for (const prop of Object.keys(el.__prevStyle)) {\n if (!(prop in val)) el.style.removeProperty(prop);\n }\n }\n el.__prevStyle = { ...val };\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 if (typeof value === 'object' && value !== null && value.handler) {\n const oldHandler = el.__handlers && el.__handlers[eventName];\n if (oldHandler !== value.handler) {\n const oldOpts = el.__handlerOptions && el.__handlerOptions[eventName];\n if (oldHandler) el.removeEventListener(eventName, oldHandler, oldOpts);\n el.addEventListener(eventName, value.handler, value.options);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value.handler;\n el.__handlerOptions = el.__handlerOptions || {};\n el.__handlerOptions[eventName] = value.options;\n }\n } else {\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 }\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// ─── Longest Increasing Subsequence (O(n log n)) ────────\n// Used by keyed reconciliation to minimize DOM moves.\n\nfunction longestIncreasingSubsequence(arr) {\n const n = arr.length;\n if (n === 0) return [];\n\n // tails[i] = index in arr of smallest tail element for IS of length i+1\n const tails = [];\n // parent[i] = index in arr of predecessor of arr[i] in the LIS\n const parent = new Array(n).fill(-1);\n // indices[i] = index in arr of tails[i]\n const indices = [];\n\n for (let i = 0; i < n; i++) {\n const val = arr[i];\n if (val < 0) continue; // skip removed items (marker -1)\n\n // Binary search for the insertion point\n let lo = 0, hi = tails.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (tails[mid] < val) lo = mid + 1;\n else hi = mid;\n }\n\n tails[lo] = val;\n indices[lo] = i;\n\n if (lo > 0) {\n parent[i] = indices[lo - 1];\n }\n }\n\n // Reconstruct\n const result = new Array(tails.length);\n let k = indices[tails.length - 1];\n for (let i = tails.length - 1; i >= 0; i--) {\n result[i] = k;\n k = parent[k];\n }\n\n return result;\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 // LIS-based reorder: compute old positions, find LIS, only move non-LIS nodes\n const oldPosMap = new Map();\n for (let i = 0; i < oldNodes.length; i++) {\n oldPosMap.set(oldNodes[i], i);\n }\n const positions = newNodes.map(n => oldPosMap.has(n) ? oldPosMap.get(n) : -1);\n const lisIndices = new Set(longestIncreasingSubsequence(positions));\n\n // Insert nodes: only move nodes not in the LIS\n let cursor = marker.nextSibling;\n for (let i = 0; i < newNodes.length; i++) {\n const node = newNodes[i];\n if (lisIndices.has(i) && 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 (skip identical vnodes)\n const patchCount = Math.min(oldCount, newCount);\n for (let i = 0; i < patchCount; i++) {\n if (oldNodes[i] === newChildren[i]) continue;\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 // LIS-based reorder for element children\n const logicalAfterRemove = getLogicalChildren(parent);\n const oldPosMap = new Map();\n for (let i = 0; i < logicalAfterRemove.length; i++) {\n oldPosMap.set(logicalAfterRemove[i], i);\n }\n const positions = newNodes.map(n => oldPosMap.has(n) ? oldPosMap.get(n) : -1);\n const lisIndices = new Set(longestIncreasingSubsequence(positions));\n\n for (let i = 0; i < newNodes.length; i++) {\n const expected = newNodes[i];\n if (!lisIndices.has(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\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 if (typeof value === 'object' && value !== null && value.handler) {\n el.addEventListener(eventName, value.handler, value.options);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value.handler;\n el.__handlerOptions = el.__handlerOptions || {};\n el.__handlerOptions[eventName] = value.options;\n } else {\n el.addEventListener(eventName, value);\n if (!el.__handlers) el.__handlers = {};\n el.__handlers[eventName] = value;\n }\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 if (typeof container.replaceChildren === 'function') {\n container.replaceChildren();\n } else {\n while (container.firstChild) container.removeChild(container.firstChild);\n }\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\n// ─── Form Handling ──────────────────────────────────────────\n// Reactive form primitives with field-level validation.\n// Usage:\n// const form = createForm({\n// fields: { email: { initial: '', validate: (v) => v.includes('@') ? null : 'Invalid email' } },\n// onSubmit: async (values) => { await server.register(values); }\n// });\n// <input bind:value={form.field('email').value} />\n// {form.field('email').error()}\n// <button on:click={form.submit} disabled={form.submitting()}>Submit</button>\n\nexport function createForm({ fields = {}, onSubmit, validateOnChange = true, validateOnBlur = true }) {\n const fieldSignals = {};\n const errorSignals = {};\n const touchedSignals = {};\n const [submitting, setSubmitting] = createSignal(false);\n const [submitError, setSubmitError] = createSignal(null);\n const [submitCount, setSubmitCount] = createSignal(0);\n\n // Initialize field signals\n for (const [name, config] of Object.entries(fields)) {\n const initial = config.initial !== undefined ? config.initial : '';\n const [value, setValue] = createSignal(initial);\n const [error, setError] = createSignal(null);\n const [touched, setTouched] = createSignal(false);\n fieldSignals[name] = { value, setValue, validate: config.validate || null, initial };\n errorSignals[name] = { error, setError };\n touchedSignals[name] = { touched, setTouched };\n }\n\n function validateField(name) {\n const f = fieldSignals[name];\n const e = errorSignals[name];\n if (!f || !e || !f.validate) return null;\n const err = f.validate(f.value());\n e.setError(err);\n return err;\n }\n\n function validateAll() {\n let hasErrors = false;\n for (const name of Object.keys(fieldSignals)) {\n const err = validateField(name);\n if (err) hasErrors = true;\n }\n return !hasErrors;\n }\n\n function field(name) {\n const f = fieldSignals[name];\n const e = errorSignals[name];\n const t = touchedSignals[name];\n if (!f) throw new Error(`Tova form: unknown field \"${name}\"`);\n return {\n value: f.value,\n error: e.error,\n touched: t.touched,\n set(val) {\n f.setValue(val);\n if (validateOnChange && t.touched()) validateField(name);\n },\n blur() {\n t.setTouched(true);\n if (validateOnBlur) validateField(name);\n },\n validate() { return validateField(name); },\n };\n }\n\n function values() {\n const result = {};\n for (const [name, f] of Object.entries(fieldSignals)) {\n result[name] = f.value();\n }\n return result;\n }\n\n function reset() {\n for (const [name, f] of Object.entries(fieldSignals)) {\n f.setValue(f.initial);\n errorSignals[name].setError(null);\n touchedSignals[name].setTouched(false);\n }\n setSubmitError(null);\n }\n\n async function submit(e) {\n if (e && typeof e.preventDefault === 'function') e.preventDefault();\n // Touch all fields\n for (const name of Object.keys(touchedSignals)) {\n touchedSignals[name].setTouched(true);\n }\n if (!validateAll()) return;\n if (!onSubmit) return;\n setSubmitting(true);\n setSubmitError(null);\n setSubmitCount(c => c + 1);\n try {\n await onSubmit(values());\n } catch (err) {\n setSubmitError(err);\n } finally {\n setSubmitting(false);\n }\n }\n\n const isValid = createComputed(() => {\n for (const name of Object.keys(errorSignals)) {\n if (errorSignals[name].error()) return false;\n }\n return true;\n });\n\n const isDirty = createComputed(() => {\n for (const [name, f] of Object.entries(fieldSignals)) {\n if (f.value() !== f.initial) return true;\n }\n return false;\n });\n\n return {\n field,\n values,\n reset,\n submit,\n submitting,\n submitError,\n submitCount,\n isValid,\n isDirty,\n validate: validateAll,\n };\n}\n";
|
|
4
4
|
|
|
5
5
|
export const RPC_SOURCE = "// RPC bridge — client calls to server functions are auto-routed via HTTP\n// Includes CSRF protection, request timeouts, and interceptor middleware.\n\n// ─── Configuration ────────────────────────────────────────\n\nconst _config = {\n base: typeof window !== 'undefined' ? (window.__TOVA_RPC_BASE || '') : 'http://localhost:3000',\n timeout: 30000, // 30s default timeout\n csrfHeader: 'X-Tova-CSRF',\n csrfToken: null, // auto-detected from meta tag or set manually\n credentials: 'same-origin', // fetch credentials mode\n};\n\n// Interceptor chains — each is { request?: fn, response?: fn, error?: fn }\nconst _interceptors = [];\n\n// ─── CSRF Token Management ────────────────────────────────\n\nfunction getCSRFToken() {\n if (_config.csrfToken) return _config.csrfToken;\n // Auto-detect from <meta name=\"csrf-token\" content=\"...\"> (server-rendered)\n if (typeof document !== 'undefined') {\n const meta = document.querySelector('meta[name=\"csrf-token\"]');\n if (meta) {\n _config.csrfToken = meta.getAttribute('content');\n return _config.csrfToken;\n }\n }\n return null;\n}\n\n// ─── Core RPC Function ────────────────────────────────────\n\nexport async function rpc(functionName, args = []) {\n const url = `${_config.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 body = { __args: args };\n } else {\n body = {};\n }\n\n // Build headers\n const headers = { 'Content-Type': 'application/json' };\n let csrf = getCSRFToken();\n if (!csrf && _csrfReadyPromise) {\n await _csrfReadyPromise;\n csrf = getCSRFToken();\n }\n if (csrf) {\n headers[_config.csrfHeader] = csrf;\n }\n\n // Build request options\n let requestOptions = {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n credentials: _config.credentials,\n };\n\n // Run request interceptors\n for (const interceptor of _interceptors) {\n if (interceptor.request) {\n const result = interceptor.request({ url, functionName, args, options: requestOptions });\n if (result && typeof result === 'object') {\n requestOptions = { ...requestOptions, ...result };\n }\n }\n }\n\n // AbortController for timeout\n const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;\n if (controller) {\n requestOptions.signal = controller.signal;\n }\n const timeoutId = controller && _config.timeout > 0\n ? setTimeout(() => controller.abort(), _config.timeout)\n : null;\n\n try {\n const response = await fetch(url, requestOptions);\n\n if (timeoutId) clearTimeout(timeoutId);\n\n if (!response.ok) {\n const errorText = await response.text();\n const err = new Error(`RPC call to '${functionName}' failed: ${response.status} ${errorText}`);\n err.status = response.status;\n err.functionName = functionName;\n\n // Run error interceptors\n for (const interceptor of _interceptors) {\n if (interceptor.error) {\n const handled = interceptor.error(err, { url, functionName, args, response });\n if (handled === false) return undefined; // Interceptor suppressed the error\n }\n }\n\n throw err;\n }\n\n let data = await response.json();\n\n // Run response interceptors\n for (const interceptor of _interceptors) {\n if (interceptor.response) {\n const transformed = interceptor.response(data, { url, functionName, args, response });\n if (transformed !== undefined) data = transformed;\n }\n }\n\n return data.result;\n } catch (error) {\n if (timeoutId) clearTimeout(timeoutId);\n\n // Wrap AbortError as timeout\n if (error.name === 'AbortError') {\n const err = new Error(`RPC call to '${functionName}' timed out after ${_config.timeout}ms`);\n err.code = 'TIMEOUT';\n err.functionName = functionName;\n\n for (const interceptor of _interceptors) {\n if (interceptor.error) {\n const handled = interceptor.error(err, { url, functionName, args });\n if (handled === false) return undefined;\n }\n }\n\n throw err;\n }\n\n if (error.message && error.message.includes('RPC call')) throw error;\n throw new Error(`RPC call to '${functionName}' failed: ${error.message}`);\n }\n}\n\n// ─── Configuration API ────────────────────────────────────\n\nexport function configureRPC(options) {\n if (typeof options === 'string') {\n // Backward compat: configureRPC('http://...')\n _config.base = options;\n if (typeof window !== 'undefined') window.__TOVA_RPC_BASE = options;\n return;\n }\n if (options.baseUrl !== undefined) {\n _config.base = options.baseUrl;\n if (typeof window !== 'undefined') window.__TOVA_RPC_BASE = options.baseUrl;\n }\n if (options.timeout !== undefined) _config.timeout = options.timeout;\n if (options.csrfToken !== undefined) _config.csrfToken = options.csrfToken;\n if (options.csrfHeader !== undefined) _config.csrfHeader = options.csrfHeader;\n if (options.credentials !== undefined) _config.credentials = options.credentials;\n}\n\n// ─── Interceptor API ──────────────────────────────────────\n// Usage:\n// const unsub = addRPCInterceptor({\n// request({ url, functionName, args, options }) {\n// options.headers['Authorization'] = 'Bearer ' + token;\n// return options;\n// },\n// response(data, { functionName }) { ... },\n// error(err, { functionName }) { ... },\n// });\n// unsub(); // remove interceptor\n\nexport function addRPCInterceptor(interceptor) {\n _interceptors.push(interceptor);\n return () => {\n const idx = _interceptors.indexOf(interceptor);\n if (idx !== -1) _interceptors.splice(idx, 1);\n };\n}\n\n// ─── Set CSRF Token ───────────────────────────────────────\n\nexport function setCSRFToken(token) {\n _config.csrfToken = token;\n}\n\n// ─── CSRF Ready Promise ──────────────────────────────────\n// When auth is enabled, the CSRF token fetch is async. Store the promise\n// so rpc() can await it before the first request if no token is available yet.\n\nlet _csrfReadyPromise = null;\n\nexport function setCsrfReady(promise) {\n _csrfReadyPromise = promise;\n}\n";
|
|
6
6
|
|
|
@@ -365,6 +365,7 @@ export function createComputed(fn) {
|
|
|
365
365
|
export function onMount(fn) {
|
|
366
366
|
const owner = currentOwner;
|
|
367
367
|
queueMicrotask(() => {
|
|
368
|
+
if (owner && owner._disposed) return;
|
|
368
369
|
const result = fn();
|
|
369
370
|
if (typeof result === 'function' && owner && !owner._disposed) {
|
|
370
371
|
owner._cleanups.push(result);
|
package/src/stdlib/core.js
CHANGED
|
@@ -82,11 +82,21 @@ export function flat_map(arr, fn) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
export function min(arr) {
|
|
85
|
-
|
|
85
|
+
if (!arr || arr.length === 0) return null;
|
|
86
|
+
let m = arr[0];
|
|
87
|
+
for (let i = 1; i < arr.length; i++) {
|
|
88
|
+
if (arr[i] < m) m = arr[i];
|
|
89
|
+
}
|
|
90
|
+
return m;
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
export function max(arr) {
|
|
89
|
-
|
|
94
|
+
if (!arr || arr.length === 0) return null;
|
|
95
|
+
let m = arr[0];
|
|
96
|
+
for (let i = 1; i < arr.length; i++) {
|
|
97
|
+
if (arr[i] > m) m = arr[i];
|
|
98
|
+
}
|
|
99
|
+
return m;
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
export function any(arr, fn) {
|
package/src/stdlib/inline.js
CHANGED
|
@@ -113,7 +113,7 @@ export const BUILTIN_FUNCTIONS = {
|
|
|
113
113
|
unique: `function unique(arr) { return [...new Set(arr)]; }`,
|
|
114
114
|
group_by: `function group_by(arr, fn) { const r = {}; for (const v of arr) { const k = fn(v); if (!r[k]) r[k] = []; r[k].push(v); } return r; }`,
|
|
115
115
|
chunk: `function chunk(arr, n) { const r = []; for (let i = 0; i < arr.length; i += n) r.push(arr.slice(i, i + n)); return r; }`,
|
|
116
|
-
flatten: `function flatten(arr) { return arr.flat(); }`,
|
|
116
|
+
flatten: `function flatten(arr) { return arr.flat(Infinity); }`,
|
|
117
117
|
take: `function take(arr, n) { return arr.slice(0, n); }`,
|
|
118
118
|
drop: `function drop(arr, n) { return arr.slice(n); }`,
|
|
119
119
|
first: `function first(arr) { return arr.length > 0 ? arr[0] : null; }`,
|
|
@@ -520,7 +520,7 @@ Table.prototype = { get rows() { return this._rows.length; }, get columns() { re
|
|
|
520
520
|
compose: `function compose(...fns) { return (x) => fns.reduceRight((v, fn) => fn(v), x); }`,
|
|
521
521
|
pipe_fn: `function pipe_fn(...fns) { return (x) => fns.reduce((v, fn) => fn(v), x); }`,
|
|
522
522
|
identity: `function identity(x) { return x; }`,
|
|
523
|
-
memoize: `function memoize(fn) { const cache = new Map(); return function(...args) { const key = JSON.stringify(args); if (cache.has(key)) return cache.get(key); const result = fn.apply(this, args); cache.set(key, result); return result; }; }`,
|
|
523
|
+
memoize: `function memoize(fn) { const cache = new Map(); const keys = []; const maxSize = 1000; return function(...args) { const key = JSON.stringify(args); if (cache.has(key)) return cache.get(key); const result = fn.apply(this, args); if (cache.size >= maxSize) { const oldest = keys.shift(); cache.delete(oldest); } cache.set(key, result); keys.push(key); return result; }; }`,
|
|
524
524
|
debounce: `function debounce(fn, ms) { let timer; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), ms); }; }`,
|
|
525
525
|
throttle: `function throttle(fn, ms) { let last = 0; return function(...args) { const now = Date.now(); if (now - last >= ms) { last = now; return fn.apply(this, args); } }; }`,
|
|
526
526
|
once: `function once(fn) { let called = false, result; return function(...args) { if (!called) { called = true; result = fn.apply(this, args); } return result; }; }`,
|
|
@@ -561,7 +561,7 @@ Table.prototype = { get rows() { return this._rows.length; }, get columns() { re
|
|
|
561
561
|
|
|
562
562
|
// ── Regex (with compiled regex cache) ─────────────────
|
|
563
563
|
__regex_cache: `const __reCache = new Map(); function __re(p, f) { const k = p + '\\0' + (f || ''); let r = __reCache.get(k); if (!r) { r = new RegExp(p, f); __reCache.set(k, r); if (__reCache.size > 1000) { const first = __reCache.keys().next().value; __reCache.delete(first); } } return r; }`,
|
|
564
|
-
regex_test: `function regex_test(s, pattern, flags) {
|
|
564
|
+
regex_test: `function regex_test(s, pattern, flags) { const r = __re(pattern, flags); r.lastIndex = 0; return r.test(s); }`,
|
|
565
565
|
regex_match: `function regex_match(s, pattern, flags) { const m = s.match(__re(pattern, flags)); if (!m) return Err('No match'); return Ok({ match: m[0], index: m.index, groups: m.slice(1) }); }`,
|
|
566
566
|
regex_find_all: `function regex_find_all(s, pattern, flags) { const re = __re(pattern, (flags || '') + (flags && flags.includes('g') ? '' : 'g')); const results = []; let m; re.lastIndex = 0; while ((m = re.exec(s)) !== null) { results.push({ match: m[0], index: m.index, groups: m.slice(1) }); } return results; }`,
|
|
567
567
|
regex_replace: `function regex_replace(s, pattern, replacement, flags) { return s.replace(__re(pattern, flags || 'g'), replacement); }`,
|
package/src/stdlib/math.js
CHANGED
|
@@ -36,7 +36,7 @@ export function factorial(n) { if (n < 0) return null; if (n <= 1) return 1; let
|
|
|
36
36
|
|
|
37
37
|
export function hypot(a, b) { return Math.hypot(a, b); }
|
|
38
38
|
export function lerp(a, b, t) { return a + (b - a) * t; }
|
|
39
|
-
export function divmod(a, b) {
|
|
39
|
+
export function divmod(a, b) { const q = Math.floor(a / b); return [q, a - q * b]; }
|
|
40
40
|
export function avg(arr) { return arr.length === 0 ? 0 : arr.reduce((a, b) => a + b, 0) / arr.length; }
|
|
41
41
|
|
|
42
42
|
// ── Statistics ────────────────────────────────────────────
|
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.9.
|
|
2
|
+
export const VERSION = "0.9.13";
|