aiplang 2.10.7 → 2.10.9
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/aiplang.js +47 -79
- package/package.json +1 -1
- package/runtime/aiplang-hydrate.js +182 -72
- package/server/server.js +105 -2
package/bin/aiplang.js
CHANGED
|
@@ -5,7 +5,7 @@ const fs = require('fs')
|
|
|
5
5
|
const path = require('path')
|
|
6
6
|
const http = require('http')
|
|
7
7
|
|
|
8
|
-
const VERSION = '2.10.
|
|
8
|
+
const VERSION = '2.10.9'
|
|
9
9
|
const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
|
|
10
10
|
const cmd = process.argv[2]
|
|
11
11
|
const args = process.argv.slice(3)
|
|
@@ -24,10 +24,6 @@ const ic = n => ICONS[n] || n
|
|
|
24
24
|
const isDyn = s => s&&(s.includes('@')||s.includes('$'))
|
|
25
25
|
const hSize = n => n<1024?`${n}B`:`${(n/1024).toFixed(1)}KB`
|
|
26
26
|
|
|
27
|
-
// ─────────────────────────────────────────────────────────────────
|
|
28
|
-
// CLI COMMANDS
|
|
29
|
-
// ─────────────────────────────────────────────────────────────────
|
|
30
|
-
|
|
31
27
|
if (!cmd||cmd==='--help'||cmd==='-h') {
|
|
32
28
|
console.log(`
|
|
33
29
|
aiplang v${VERSION}
|
|
@@ -74,18 +70,12 @@ if (!cmd||cmd==='--help'||cmd==='-h') {
|
|
|
74
70
|
}
|
|
75
71
|
if (cmd==='--version'||cmd==='-v') { console.log(`aiplang v${VERSION}`); process.exit(0) }
|
|
76
72
|
|
|
77
|
-
// ─────────────────────────────────────────────────────────────────
|
|
78
|
-
// TEMPLATE SYSTEM
|
|
79
|
-
// Custom templates stored at ~/.aip/templates/<name>.aip
|
|
80
|
-
// ─────────────────────────────────────────────────────────────────
|
|
81
|
-
|
|
82
73
|
const TEMPLATES_DIR = path.join(require('os').homedir(), '.aip', 'templates')
|
|
83
74
|
|
|
84
75
|
function ensureTemplatesDir() {
|
|
85
76
|
if (!fs.existsSync(TEMPLATES_DIR)) fs.mkdirSync(TEMPLATES_DIR, { recursive: true })
|
|
86
77
|
}
|
|
87
78
|
|
|
88
|
-
// Built-in templates (interpolate {{name}} and {{year}})
|
|
89
79
|
const BUILTIN_TEMPLATES = {
|
|
90
80
|
saas: `# {{name}}
|
|
91
81
|
~db sqlite ./app.db
|
|
@@ -255,24 +245,20 @@ function applyTemplateVars(src, name, year) {
|
|
|
255
245
|
function getTemplate(tplName, name, year) {
|
|
256
246
|
ensureTemplatesDir()
|
|
257
247
|
|
|
258
|
-
// 1. Local file path: --template ./my-template.aip or --template /abs/path.aip
|
|
259
248
|
if (tplName.startsWith('./') || tplName.startsWith('../') || tplName.startsWith('/')) {
|
|
260
249
|
const full = path.resolve(tplName)
|
|
261
250
|
if (!fs.existsSync(full)) { console.error(`\n ✗ Template file not found: ${full}\n`); process.exit(1) }
|
|
262
251
|
return applyTemplateVars(fs.readFileSync(full, 'utf8'), name, year)
|
|
263
252
|
}
|
|
264
253
|
|
|
265
|
-
// 2. User custom template: ~/.aip/templates/<name>.aip
|
|
266
254
|
const customPath = path.join(TEMPLATES_DIR, tplName + '.aip')
|
|
267
255
|
if (fs.existsSync(customPath)) {
|
|
268
256
|
return applyTemplateVars(fs.readFileSync(customPath, 'utf8'), name, year)
|
|
269
257
|
}
|
|
270
258
|
|
|
271
|
-
// 3. Built-in template
|
|
272
259
|
const builtin = BUILTIN_TEMPLATES[tplName]
|
|
273
260
|
if (builtin) return applyTemplateVars(builtin, name, year)
|
|
274
261
|
|
|
275
|
-
// Not found — show what's available
|
|
276
262
|
const customs = fs.existsSync(TEMPLATES_DIR)
|
|
277
263
|
? fs.readdirSync(TEMPLATES_DIR).filter(f=>f.endsWith('.aip')).map(f=>f.replace('.aip',''))
|
|
278
264
|
: []
|
|
@@ -297,17 +283,14 @@ function listTemplates() {
|
|
|
297
283
|
console.log()
|
|
298
284
|
}
|
|
299
285
|
|
|
300
|
-
// ── template subcommand ──────────────────────────────────────────
|
|
301
286
|
if (cmd === 'template') {
|
|
302
287
|
const sub = args[0]
|
|
303
288
|
ensureTemplatesDir()
|
|
304
289
|
|
|
305
|
-
// aiplang template list
|
|
306
290
|
if (!sub || sub === 'list' || sub === 'ls') {
|
|
307
291
|
listTemplates(); process.exit(0)
|
|
308
292
|
}
|
|
309
293
|
|
|
310
|
-
// aiplang template save <name> [--from <file>]
|
|
311
294
|
if (sub === 'save' || sub === 'add') {
|
|
312
295
|
const tname = args[1]
|
|
313
296
|
if (!tname) { console.error('\n ✗ Usage: aiplang template save <name> [--from <file>]\n'); process.exit(1) }
|
|
@@ -318,7 +301,7 @@ if (cmd === 'template') {
|
|
|
318
301
|
if (!fs.existsSync(fp)) { console.error(`\n ✗ File not found: ${fp}\n`); process.exit(1) }
|
|
319
302
|
src = fs.readFileSync(fp, 'utf8')
|
|
320
303
|
} else {
|
|
321
|
-
|
|
304
|
+
|
|
322
305
|
const sources = ['pages', 'app.aip', 'index.aip']
|
|
323
306
|
const found = sources.find(s => fs.existsSync(s))
|
|
324
307
|
if (!found) { console.error('\n ✗ No .aip files found. Use --from <file> to specify source.\n'); process.exit(1) }
|
|
@@ -335,7 +318,6 @@ if (cmd === 'template') {
|
|
|
335
318
|
process.exit(0)
|
|
336
319
|
}
|
|
337
320
|
|
|
338
|
-
// aiplang template remove <name>
|
|
339
321
|
if (sub === 'remove' || sub === 'rm' || sub === 'delete') {
|
|
340
322
|
const tname = args[1]
|
|
341
323
|
if (!tname) { console.error('\n ✗ Usage: aiplang template remove <name>\n'); process.exit(1) }
|
|
@@ -345,13 +327,12 @@ if (cmd === 'template') {
|
|
|
345
327
|
console.log(`\n ✓ Removed template: ${tname}\n`); process.exit(0)
|
|
346
328
|
}
|
|
347
329
|
|
|
348
|
-
// aiplang template edit <name>
|
|
349
330
|
if (sub === 'edit' || sub === 'open') {
|
|
350
331
|
const tname = args[1]
|
|
351
332
|
if (!tname) { console.error('\n ✗ Usage: aiplang template edit <name>\n'); process.exit(1) }
|
|
352
333
|
let dest = path.join(TEMPLATES_DIR, tname + '.aip')
|
|
353
334
|
if (!fs.existsSync(dest)) {
|
|
354
|
-
|
|
335
|
+
|
|
355
336
|
const builtin = BUILTIN_TEMPLATES[tname]
|
|
356
337
|
if (builtin) { fs.writeFileSync(dest, builtin); console.log(`\n ✓ Copied built-in "${tname}" to custom templates.\n`) }
|
|
357
338
|
else { console.error(`\n ✗ Template "${tname}" not found.\n`); process.exit(1) }
|
|
@@ -362,7 +343,6 @@ if (cmd === 'template') {
|
|
|
362
343
|
process.exit(0)
|
|
363
344
|
}
|
|
364
345
|
|
|
365
|
-
// aiplang template show <name>
|
|
366
346
|
if (sub === 'show' || sub === 'cat') {
|
|
367
347
|
const tname = args[1] || 'default'
|
|
368
348
|
const customPath = path.join(TEMPLATES_DIR, tname + '.aip')
|
|
@@ -372,7 +352,6 @@ if (cmd === 'template') {
|
|
|
372
352
|
console.error(`\n ✗ Template "${tname}" not found.\n`); process.exit(1)
|
|
373
353
|
}
|
|
374
354
|
|
|
375
|
-
// aiplang template export <name> [--out <file>]
|
|
376
355
|
if (sub === 'export') {
|
|
377
356
|
const tname = args[1]
|
|
378
357
|
if (!tname) { console.error('\n ✗ Usage: aiplang template export <name>\n'); process.exit(1) }
|
|
@@ -390,7 +369,6 @@ if (cmd === 'template') {
|
|
|
390
369
|
process.exit(1)
|
|
391
370
|
}
|
|
392
371
|
|
|
393
|
-
// ── Init ─────────────────────────────────────────────────────────
|
|
394
372
|
if (cmd==='init') {
|
|
395
373
|
const tplIdx = args.indexOf('--template')
|
|
396
374
|
const tplName = tplIdx !== -1 ? args[tplIdx+1] : 'default'
|
|
@@ -399,15 +377,13 @@ if (cmd==='init') {
|
|
|
399
377
|
|
|
400
378
|
if (fs.existsSync(dir)) { console.error(`\n ✗ Directory "${name}" already exists.\n`); process.exit(1) }
|
|
401
379
|
|
|
402
|
-
// Get template source (built-in, custom, or file path)
|
|
403
380
|
const tplSrc = getTemplate(tplName, name, year)
|
|
404
381
|
|
|
405
|
-
// Check if template has full-stack backend (models/api blocks)
|
|
406
382
|
const isFullStack = tplSrc.includes('\nmodel ') || tplSrc.includes('\napi ')
|
|
407
383
|
const isMultiFile = tplSrc.includes('\n---\n')
|
|
408
384
|
|
|
409
385
|
if (isFullStack) {
|
|
410
|
-
|
|
386
|
+
|
|
411
387
|
fs.mkdirSync(dir, { recursive: true })
|
|
412
388
|
fs.writeFileSync(path.join(dir, 'app.aip'), tplSrc)
|
|
413
389
|
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({
|
|
@@ -421,7 +397,7 @@ if (cmd==='init') {
|
|
|
421
397
|
const label = tplName !== 'default' ? ` (template: ${tplName})` : ''
|
|
422
398
|
console.log(`\n ✓ Created ${name}/${label}\n\n app.aip ← full-stack app (backend + frontend)\n\n Next:\n cd ${name} && npx aiplang start app.aip\n`)
|
|
423
399
|
} else if (isMultiFile) {
|
|
424
|
-
|
|
400
|
+
|
|
425
401
|
fs.mkdirSync(path.join(dir,'pages'), {recursive:true})
|
|
426
402
|
fs.mkdirSync(path.join(dir,'public'), {recursive:true})
|
|
427
403
|
for (const f of ['aiplang-runtime.js','aiplang-hydrate.js']) {
|
|
@@ -439,7 +415,7 @@ if (cmd==='init') {
|
|
|
439
415
|
const files = fs.readdirSync(path.join(dir,'pages')).map(f=>f).join(', ')
|
|
440
416
|
console.log(`\n ✓ Created ${name}/${label}\n\n pages/{${files}} ← edit these\n\n Next:\n cd ${name} && npx aiplang serve\n`)
|
|
441
417
|
} else {
|
|
442
|
-
|
|
418
|
+
|
|
443
419
|
fs.mkdirSync(path.join(dir,'pages'), {recursive:true})
|
|
444
420
|
fs.mkdirSync(path.join(dir,'public'), {recursive:true})
|
|
445
421
|
for (const f of ['aiplang-runtime.js','aiplang-hydrate.js']) {
|
|
@@ -454,7 +430,6 @@ if (cmd==='init') {
|
|
|
454
430
|
process.exit(0)
|
|
455
431
|
}
|
|
456
432
|
|
|
457
|
-
// ── New ───────────────────────────────────────────────────────────
|
|
458
433
|
if (cmd==='new') {
|
|
459
434
|
const name=args[0]; if(!name){console.error('\n ✗ Usage: aiplang new <page>\n');process.exit(1)}
|
|
460
435
|
const dir=fs.existsSync('pages')?'pages':'.'
|
|
@@ -466,7 +441,6 @@ if (cmd==='new') {
|
|
|
466
441
|
process.exit(0)
|
|
467
442
|
}
|
|
468
443
|
|
|
469
|
-
// ── Build ─────────────────────────────────────────────────────────
|
|
470
444
|
if (cmd==='build') {
|
|
471
445
|
const outIdx=args.indexOf('--out')
|
|
472
446
|
const outDir=outIdx!==-1?args[outIdx+1]:'dist'
|
|
@@ -476,7 +450,7 @@ if (cmd==='build') {
|
|
|
476
450
|
fs.readdirSync(input).filter(f=>f.endsWith('.aip')).forEach(f=>files.push(path.join(input,f)))
|
|
477
451
|
} else if(input.endsWith('.aip')&&fs.existsSync(input)){ files.push(input) }
|
|
478
452
|
if(!files.length){console.error(`\n ✗ No .aip files in: ${input}\n`);process.exit(1)}
|
|
479
|
-
|
|
453
|
+
|
|
480
454
|
function resolveImports(content, baseDir, seen=new Set()) {
|
|
481
455
|
return content.replace(/^~import\s+["']?([^"'\n]+)["']?$/mg, (_, importPath) => {
|
|
482
456
|
const resolved = path.resolve(baseDir, importPath.trim())
|
|
@@ -511,7 +485,6 @@ if (cmd==='build') {
|
|
|
511
485
|
process.exit(0)
|
|
512
486
|
}
|
|
513
487
|
|
|
514
|
-
// ── Serve (hot reload) ────────────────────────────────────────────
|
|
515
488
|
if (cmd==='serve'||cmd==='dev') {
|
|
516
489
|
const root=path.resolve(args[0]||'.')
|
|
517
490
|
const port=parseInt(process.env.PORT||'3000')
|
|
@@ -549,7 +522,6 @@ if (cmd==='serve'||cmd==='dev') {
|
|
|
549
522
|
return
|
|
550
523
|
}
|
|
551
524
|
|
|
552
|
-
// ── Dev server (full-stack) ──────────────────────────────────────
|
|
553
525
|
if (cmd === 'start' || cmd === 'run') {
|
|
554
526
|
const aipFile = args[0]
|
|
555
527
|
if (!aipFile || !fs.existsSync(aipFile)) {
|
|
@@ -573,10 +545,6 @@ if (cmd === 'start' || cmd === 'run') {
|
|
|
573
545
|
console.error(`\n ✗ Unknown command: ${cmd}\n Run aiplang --help\n`)
|
|
574
546
|
process.exit(1)
|
|
575
547
|
|
|
576
|
-
// ═════════════════════════════════════════════════════════════════
|
|
577
|
-
// PARSER
|
|
578
|
-
// ═════════════════════════════════════════════════════════════════
|
|
579
|
-
|
|
580
548
|
function parsePages(src) {
|
|
581
549
|
return src.split(/\n---\n/).map(s=>parsePage(s.trim())).filter(Boolean)
|
|
582
550
|
}
|
|
@@ -619,8 +587,7 @@ function parseQuery(s) {
|
|
|
619
587
|
}
|
|
620
588
|
|
|
621
589
|
function parseBlock(line) {
|
|
622
|
-
|
|
623
|
-
// animate:fade-up class:my-class (can appear at end of any block line)
|
|
590
|
+
|
|
624
591
|
let extraClass=null, animate=null
|
|
625
592
|
const _cm=line.match(/\bclass:(\S+)/)
|
|
626
593
|
if(_cm){extraClass=_cm[1];line=line.replace(_cm[0],'').trim()}
|
|
@@ -636,12 +603,10 @@ function parseBlock(line) {
|
|
|
636
603
|
const _bgm=line.match(/\bbg:(#[0-9a-fA-F]+|[a-z]+)/)
|
|
637
604
|
if(_bgm){bg=_bgm[1];line=line.replace(_bgm[0],'').trim()}
|
|
638
605
|
|
|
639
|
-
// ── raw{} HTML passthrough ──────────────────────────────────
|
|
640
606
|
if(line.startsWith('raw{')) {
|
|
641
607
|
return{kind:'raw',html:line.slice(4,line.lastIndexOf('}')),extraClass,animate}
|
|
642
608
|
}
|
|
643
609
|
|
|
644
|
-
// ── table ───────────────────────────────────────────────────
|
|
645
610
|
if(line.startsWith('table ') || line.startsWith('table{')) {
|
|
646
611
|
const idx=line.indexOf('{');if(idx===-1) return null
|
|
647
612
|
const start=line.startsWith('table{')?6:6
|
|
@@ -659,7 +624,6 @@ function parseBlock(line) {
|
|
|
659
624
|
return{kind:'table',binding,cols:Array.isArray(cols)?cols:[],empty:parseEmpty(clean),editPath:em?.[2]||null,editMethod:em?.[1]||'PUT',deletePath:dm?.[1]||null,deleteKey:'id',fallback:fallbackM?.[1]?.trim()||null,retry:retryM?.[1]||null,extraClass,animate,variant,style,bg}
|
|
660
625
|
}
|
|
661
626
|
|
|
662
|
-
// ── form ────────────────────────────────────────────────────
|
|
663
627
|
if(line.startsWith('form ') || line.startsWith('form{')) {
|
|
664
628
|
const bi=line.indexOf('{');if(bi===-1) return null
|
|
665
629
|
let head=line.slice(line.startsWith('form{')?4:5,bi).trim()
|
|
@@ -667,7 +631,7 @@ function parseBlock(line) {
|
|
|
667
631
|
let action='', optimistic=false; const ai=head.indexOf('=>')
|
|
668
632
|
if(ai!==-1){
|
|
669
633
|
action=head.slice(ai+2).trim()
|
|
670
|
-
|
|
634
|
+
|
|
671
635
|
if(action.includes('.optimistic(')){optimistic=true;action=action.replace('.optimistic','')}
|
|
672
636
|
head=head.slice(0,ai).trim()
|
|
673
637
|
}
|
|
@@ -677,7 +641,6 @@ function parseBlock(line) {
|
|
|
677
641
|
return{kind:'form',method,bpath,action,optimistic,fields:parseFields(content)||[],extraClass,animate,variant,style,bg}
|
|
678
642
|
}
|
|
679
643
|
|
|
680
|
-
// ── pricing ─────────────────────────────────────────────────
|
|
681
644
|
if(line.startsWith('pricing{')) {
|
|
682
645
|
const body=line.slice(8,line.lastIndexOf('}')).trim()
|
|
683
646
|
const plans=body.split('|').map(p=>{
|
|
@@ -687,14 +650,12 @@ function parseBlock(line) {
|
|
|
687
650
|
return{kind:'pricing',plans,extraClass,animate,variant,style,bg}
|
|
688
651
|
}
|
|
689
652
|
|
|
690
|
-
// ── faq ─────────────────────────────────────────────────────
|
|
691
653
|
if(line.startsWith('faq{')) {
|
|
692
654
|
const body=line.slice(4,line.lastIndexOf('}')).trim()
|
|
693
655
|
const items=body.split('|').map(i=>{const idx=i.indexOf('>');return{q:i.slice(0,idx).trim(),a:i.slice(idx+1).trim()}}).filter(i=>i.q&&i.a)
|
|
694
656
|
return{kind:'faq',items,extraClass,animate}
|
|
695
657
|
}
|
|
696
658
|
|
|
697
|
-
// ── testimonial ──────────────────────────────────────────────
|
|
698
659
|
if(line.startsWith('testimonial{')) {
|
|
699
660
|
const body=line.slice(12,line.lastIndexOf('}')).trim()
|
|
700
661
|
const parts=body.split('|').map(x=>x.trim())
|
|
@@ -702,12 +663,10 @@ function parseBlock(line) {
|
|
|
702
663
|
return{kind:'testimonial',author:parts[0],quote:parts[1]?.replace(/^"|"$/g,''),img:imgPart?.slice(4)||null,extraClass,animate}
|
|
703
664
|
}
|
|
704
665
|
|
|
705
|
-
// ── gallery ──────────────────────────────────────────────────
|
|
706
666
|
if(line.startsWith('gallery{')) {
|
|
707
667
|
return{kind:'gallery',imgs:line.slice(8,line.lastIndexOf('}')).trim().split('|').map(x=>x.trim()).filter(Boolean),extraClass,animate}
|
|
708
668
|
}
|
|
709
669
|
|
|
710
|
-
// ── btn ──────────────────────────────────────────────────────
|
|
711
670
|
if(line.startsWith('btn{')) {
|
|
712
671
|
const parts=line.slice(4,line.lastIndexOf('}')).split('>').map(p=>p.trim())
|
|
713
672
|
const label=parts[0]||'Click', method=parts[1]?.split(' ')[0]||'POST'
|
|
@@ -717,7 +676,6 @@ function parseBlock(line) {
|
|
|
717
676
|
return{kind:'btn',label,method,bpath,action,confirm,extraClass,animate}
|
|
718
677
|
}
|
|
719
678
|
|
|
720
|
-
// ── card{} — standalone card customizável ──────────────────
|
|
721
679
|
if(line.startsWith('card{') || line.startsWith('card ')) {
|
|
722
680
|
const bi=line.indexOf('{'); if(bi===-1) return null
|
|
723
681
|
const body=line.slice(bi+1,line.lastIndexOf('}')).trim()
|
|
@@ -731,7 +689,6 @@ function parseBlock(line) {
|
|
|
731
689
|
return{kind:'card',title,subtitle,img:imgPart?.slice(4)||null,link:linkPart||null,badge,bind,extraClass,animate,variant,style,bg}
|
|
732
690
|
}
|
|
733
691
|
|
|
734
|
-
// ── cols{} — grid de conteúdo livre ─────────────────────────
|
|
735
692
|
if(line.startsWith('cols{') || (line.startsWith('cols ') && line.includes('{'))) {
|
|
736
693
|
const bi=line.indexOf('{'); if(bi===-1) return null
|
|
737
694
|
const head=line.slice(0,bi).trim()
|
|
@@ -742,19 +699,16 @@ function parseBlock(line) {
|
|
|
742
699
|
return{kind:'cols',n,items,extraClass,animate,variant,style,bg}
|
|
743
700
|
}
|
|
744
701
|
|
|
745
|
-
// ── divider{} — separador visual ─────────────────────────────
|
|
746
702
|
if(line.startsWith('divider') || line.startsWith('hr{')) {
|
|
747
703
|
const label=line.match(/\{([^}]*)\}/)?.[1]?.trim()||null
|
|
748
704
|
return{kind:'divider',label,extraClass,animate,variant,style}
|
|
749
705
|
}
|
|
750
706
|
|
|
751
|
-
// ── badge{} — label/tag destacado ───────────────────────────
|
|
752
707
|
if(line.startsWith('badge{') || line.startsWith('tag{')) {
|
|
753
708
|
const content=line.slice(line.indexOf('{')+1,line.lastIndexOf('}')).trim()
|
|
754
709
|
return{kind:'badge',content,extraClass,animate,variant,style}
|
|
755
710
|
}
|
|
756
711
|
|
|
757
|
-
// ── select ───────────────────────────────────────────────────
|
|
758
712
|
if(line.startsWith('select ')) {
|
|
759
713
|
const bi=line.indexOf('{')
|
|
760
714
|
const varName=bi!==-1?line.slice(7,bi).trim():line.slice(7).trim()
|
|
@@ -762,13 +716,11 @@ function parseBlock(line) {
|
|
|
762
716
|
return{kind:'select',binding:varName,options:body.split('|').map(o=>o.trim()).filter(Boolean),extraClass,animate}
|
|
763
717
|
}
|
|
764
718
|
|
|
765
|
-
// ── if ───────────────────────────────────────────────────────
|
|
766
719
|
if(line.startsWith('if ')) {
|
|
767
720
|
const bi=line.indexOf('{');if(bi===-1) return null
|
|
768
721
|
return{kind:'if',cond:line.slice(3,bi).trim(),inner:line.slice(bi+1,line.lastIndexOf('}')).trim(),extraClass,animate}
|
|
769
722
|
}
|
|
770
723
|
|
|
771
|
-
// ── chart{} — data visualization ───────────────────────────────
|
|
772
724
|
if(line.startsWith('chart{') || line.startsWith('chart ')) {
|
|
773
725
|
const bi=line.indexOf('{'); if(bi===-1) return null
|
|
774
726
|
const body=line.slice(bi+1,line.lastIndexOf('}')).trim()
|
|
@@ -781,7 +733,6 @@ function parseBlock(line) {
|
|
|
781
733
|
return{kind:'chart',type,binding,labels,values,title,extraClass,animate,variant,style}
|
|
782
734
|
}
|
|
783
735
|
|
|
784
|
-
// ── kanban{} — drag-and-drop board ───────────────────────────────
|
|
785
736
|
if(line.startsWith('kanban{') || line.startsWith('kanban ')) {
|
|
786
737
|
const bi=line.indexOf('{'); if(bi===-1) return null
|
|
787
738
|
const body=line.slice(bi+1,line.lastIndexOf('}')).trim()
|
|
@@ -793,7 +744,6 @@ function parseBlock(line) {
|
|
|
793
744
|
return{kind:'kanban',binding,cols,statusField,updatePath,extraClass,animate,style}
|
|
794
745
|
}
|
|
795
746
|
|
|
796
|
-
// ── editor{} — rich text editor ──────────────────────────────────
|
|
797
747
|
if(line.startsWith('editor{') || line.startsWith('editor ')) {
|
|
798
748
|
const bi=line.indexOf('{'); if(bi===-1) return null
|
|
799
749
|
const body=line.slice(bi+1,line.lastIndexOf('}')).trim()
|
|
@@ -804,7 +754,6 @@ function parseBlock(line) {
|
|
|
804
754
|
return{kind:'editor',name,placeholder,submitPath,extraClass,animate,style}
|
|
805
755
|
}
|
|
806
756
|
|
|
807
|
-
// ── each @list { template } — loop como React .map() ────────
|
|
808
757
|
if(line.startsWith('each ')) {
|
|
809
758
|
const bi=line.indexOf('{');if(bi===-1) return null
|
|
810
759
|
const binding=line.slice(5,bi).trim()
|
|
@@ -812,18 +761,15 @@ function parseBlock(line) {
|
|
|
812
761
|
return{kind:'each',binding,tpl,extraClass,animate,variant}
|
|
813
762
|
}
|
|
814
763
|
|
|
815
|
-
// ── spacer{} — espaçamento customizável ──────────────────────
|
|
816
764
|
if(line.startsWith('spacer{') || line.startsWith('spacer ')) {
|
|
817
765
|
const h=line.match(/[{\s](\S+)[}]?/)?.[1]||'3rem'
|
|
818
766
|
return{kind:'spacer',height:h,extraClass,animate}
|
|
819
767
|
}
|
|
820
768
|
|
|
821
|
-
// ── html{} — HTML inline com interpolação de @state ──────────
|
|
822
769
|
if(line.startsWith('html{')) {
|
|
823
770
|
return{kind:'html',content:line.slice(5,line.lastIndexOf('}')),extraClass,animate}
|
|
824
771
|
}
|
|
825
772
|
|
|
826
|
-
// ── regular blocks (nav, hero, stats, rowN, sect, foot) ──────
|
|
827
773
|
const bi=line.indexOf('{');if(bi===-1) return null
|
|
828
774
|
const head=line.slice(0,bi).trim()
|
|
829
775
|
const body=line.slice(bi+1,line.lastIndexOf('}')).trim()
|
|
@@ -846,25 +792,55 @@ function parseCols(s){return s.split('|').map(c=>{c=c.trim();if(c.startsWith('em
|
|
|
846
792
|
function parseEmpty(s){const m=s.match(/empty:\s*([^|]+)/);return m?m[1].trim():'No data.'}
|
|
847
793
|
function parseFields(s){return s.split('|').map(f=>{const[label,type,ph]=f.split(':').map(x=>x.trim());return label?{label,type:type||'text',placeholder:ph||'',name:label.toLowerCase().replace(/\s+/g,'_')}:null}).filter(Boolean)}
|
|
848
794
|
|
|
849
|
-
// ═════════════════════════════════════════════════════════════════
|
|
850
|
-
// RENDERER
|
|
851
|
-
// ═════════════════════════════════════════════════════════════════
|
|
852
|
-
|
|
853
795
|
function applyMods(html, b) {
|
|
854
796
|
if(!html||(!b.extraClass&&!b.animate)) return html
|
|
855
797
|
const cls=[(b.extraClass||''),(b.animate?'fx-anim-'+b.animate:'')].filter(Boolean).join(' ')
|
|
856
|
-
|
|
798
|
+
|
|
857
799
|
return html.replace(/class="([^"]*)"/, (_,c)=>`class="${c} ${cls}"`)
|
|
858
800
|
}
|
|
859
801
|
|
|
860
802
|
function renderPage(page, allPages) {
|
|
861
803
|
const needsJS=page.queries.length>0||page.blocks.some(b=>['table','list','form','if','btn','select','faq'].includes(b.kind))
|
|
862
804
|
const body=page.blocks.map(b=>{try{return applyMods(renderBlock(b,page),b)}catch(e){console.error('[aiplang] Block render error:',b.kind,e.message);return ''}}).join('')
|
|
863
|
-
|
|
805
|
+
|
|
806
|
+
const tableBlocks = page.blocks.filter(b => b.kind === 'table' && b.binding && b.cols && b.cols.length)
|
|
807
|
+
const numericKeys = ['score','count','total','amount','price','value','qty','age','rank','num','int','float','rate','pct','percent']
|
|
808
|
+
const compiledDiffs = tableBlocks.map(b => {
|
|
809
|
+
const binding = b.binding.replace(/^@/, '')
|
|
810
|
+
|
|
811
|
+
const safeId = s => (s||'').replace(/[^a-zA-Z0-9_]/g, '_').slice(0,64) || 'col'
|
|
812
|
+
const safeBinding = safeId(binding)
|
|
813
|
+
const colDefs = b.cols.map((col, j) => ({
|
|
814
|
+
key: safeId(col.key),
|
|
815
|
+
origKey: col.key,
|
|
816
|
+
idx: j,
|
|
817
|
+
numeric: numericKeys.some(kw => col.key.toLowerCase().includes(kw))
|
|
818
|
+
}))
|
|
819
|
+
const initParts = colDefs.map(d =>
|
|
820
|
+
d.numeric ? `c${d.idx}:new Float64Array(rows.map(r=>+(r.${JSON.stringify(d.origKey)}===undefined?r['${d.origKey}']:r[${JSON.stringify(d.origKey)}])||0))`
|
|
821
|
+
: `c${d.idx}:rows.map(r=>r[${JSON.stringify(d.origKey)}]??'')`
|
|
822
|
+
).join(',')
|
|
823
|
+
const diffParts = colDefs.map(d => {
|
|
824
|
+
const k = JSON.stringify(d.origKey)
|
|
825
|
+
return d.numeric
|
|
826
|
+
? `if(c${d.idx}[i]!==(r[${k}]||0)){c${d.idx}[i]=r[${k}]||0;p.push(i<<4|${d.idx})}`
|
|
827
|
+
: `if(c${d.idx}[i]!==r[${k}]){c${d.idx}[i]=r[${k}];p.push(i<<4|${d.idx})}`
|
|
828
|
+
}).join(';')
|
|
829
|
+
return [
|
|
830
|
+
`window.__aip_init_${safeBinding}=function(rows){return{${initParts}}};`,
|
|
831
|
+
`window.__aip_diff_${safeBinding}=function(rows,cache){`,
|
|
832
|
+
`const n=rows.length,p=[],${colDefs.map(d=>`c${d.idx}=cache.c${d.idx}`).join(',')};`,
|
|
833
|
+
`for(let i=0;i<n;i++){const r=rows[i];${diffParts}}return p};`
|
|
834
|
+
].join('')
|
|
835
|
+
}).join('\n')
|
|
836
|
+
const compiledScript = compiledDiffs.length
|
|
837
|
+
? `<script>/* aiplang compiled-diffs */\n${compiledDiffs}\n</script>`
|
|
838
|
+
: ''
|
|
839
|
+
const config=needsJS?JSON.stringify({id:page.id,theme:page.theme,routes:allPages.map(p=>p.route),state:page.state,queries:page.queries,stores:page.stores||[],computed:page.computed||{},compiledTables:tableBlocks.map(b=>(b.binding||'').replace(/^@/,'').replace(/[^a-zA-Z0-9_]/g,'_').slice(0,64))}):''
|
|
864
840
|
const hydrate=needsJS?`\n<script>window.__AIPLANG_PAGE__=${config};</script>\n<script src="./aiplang-hydrate.js" defer></script>`:''
|
|
865
841
|
const customVars=page.customTheme?genCustomThemeVars(page.customTheme):''
|
|
866
842
|
const themeVarCSS=page.themeVars?genThemeVarCSS(page.themeVars):''
|
|
867
|
-
|
|
843
|
+
|
|
868
844
|
const _navBlock = page.blocks.find(b=>b.kind==='nav')
|
|
869
845
|
const _brand = _navBlock?.brand || ''
|
|
870
846
|
const _title = _brand ? `${esc(_brand)} — ${esc(page.id.charAt(0).toUpperCase()+page.id.slice(1))}` : esc(page.id.charAt(0).toUpperCase()+page.id.slice(1))
|
|
@@ -920,7 +896,6 @@ function renderBlock(b, page) {
|
|
|
920
896
|
}
|
|
921
897
|
}
|
|
922
898
|
|
|
923
|
-
// ── Chart — lazy-loads Chart.js from CDN ────────────────────────
|
|
924
899
|
function rChart(b) {
|
|
925
900
|
const id = 'chart_' + Math.random().toString(36).slice(2,8)
|
|
926
901
|
const binding = b.binding || ''
|
|
@@ -931,7 +906,6 @@ function rChart(b) {
|
|
|
931
906
|
</div>\n`
|
|
932
907
|
}
|
|
933
908
|
|
|
934
|
-
// ── Kanban — drag-and-drop board ─────────────────────────────────
|
|
935
909
|
function rKanban(b) {
|
|
936
910
|
const cols = (b.cols||['Todo','In Progress','Done'])
|
|
937
911
|
const colsHtml = cols.map(col => `
|
|
@@ -943,7 +917,6 @@ function rKanban(b) {
|
|
|
943
917
|
return `<div class="fx-kanban" data-fx-kanban="${esc(b.binding||'')}" data-status-field="${esc(b.statusField||'status')}" data-update-path="${esc(b.updatePath||'')}"${style}>${colsHtml}</div>\n`
|
|
944
918
|
}
|
|
945
919
|
|
|
946
|
-
// ── Rich text editor ──────────────────────────────────────────────
|
|
947
920
|
function rEditor(b) {
|
|
948
921
|
const style = b.style ? ` style="${b.style.replace(/,/g,';')}"` : ''
|
|
949
922
|
return `<div class="fx-editor-wrap"${style}>
|
|
@@ -1029,7 +1002,7 @@ function rRow(b) {
|
|
|
1029
1002
|
pink:'#ec4899',cyan:'#06b6d4',lime:'#84cc16',amber:'#f59e0b'
|
|
1030
1003
|
}
|
|
1031
1004
|
const cards=(b.items||[]).map(item=>{
|
|
1032
|
-
|
|
1005
|
+
|
|
1033
1006
|
let colorStyle='', firstIdx=0
|
|
1034
1007
|
if(item[0]&&!item[0].isImg&&!item[0].isLink){
|
|
1035
1008
|
const colorKey=item[0].text?.toLowerCase()
|
|
@@ -1150,7 +1123,6 @@ function rGallery(b) {
|
|
|
1150
1123
|
return `<div class="fx-gallery">${imgs}</div>\n`
|
|
1151
1124
|
}
|
|
1152
1125
|
|
|
1153
|
-
// ── Theme helpers ─────────────────────────────────────────────────
|
|
1154
1126
|
function genCustomThemeVars(ct) {
|
|
1155
1127
|
return `body{background:${ct.bg};color:${ct.text}}.fx-nav{background:${ct.bg}cc;border-bottom:1px solid ${ct.text}18}.fx-cta,.fx-btn{background:${ct.accent};color:#fff}.fx-card{background:${ct.surface||ct.bg};border:1px solid ${ct.text}15}.fx-form{background:${ct.surface||ct.bg};border:1px solid ${ct.text}15}.fx-input{background:${ct.bg};border:1px solid ${ct.text}30;color:${ct.text}}.fx-stat-lbl,.fx-card-body,.fx-sub,.fx-sect-body,.fx-footer-text{color:${ct.text}88}.fx-th,.fx-nav-link{color:${ct.text}77}.fx-footer{border-top:1px solid ${ct.text}15}.fx-th{border-bottom:1px solid ${ct.text}15}`
|
|
1156
1128
|
}
|
|
@@ -1170,10 +1142,6 @@ function genThemeVarCSS(t) {
|
|
|
1170
1142
|
return r.join('')
|
|
1171
1143
|
}
|
|
1172
1144
|
|
|
1173
|
-
// ═════════════════════════════════════════════════════════════════
|
|
1174
|
-
// CSS
|
|
1175
|
-
// ═════════════════════════════════════════════════════════════════
|
|
1176
|
-
|
|
1177
1145
|
function css(theme) {
|
|
1178
1146
|
const base=`*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html{scroll-behavior:smooth}body{font-family:-apple-system,'Segoe UI',system-ui,sans-serif;-webkit-font-smoothing:antialiased;min-height:100vh}a{text-decoration:none;color:inherit}input,button,select{font-family:inherit}img{max-width:100%;height:auto}.fx-nav{display:flex;align-items:center;justify-content:space-between;padding:1rem 2.5rem;position:sticky;top:0;z-index:50;backdrop-filter:blur(12px);flex-wrap:wrap;gap:.5rem}.fx-brand{font-size:1.25rem;font-weight:800;letter-spacing:-.03em}.fx-nav-links{display:flex;align-items:center;gap:1.75rem}.fx-nav-link{font-size:.875rem;font-weight:500;opacity:.65;transition:opacity .15s}.fx-nav-link:hover{opacity:1}.fx-hamburger{display:none;flex-direction:column;gap:5px;background:none;border:none;cursor:pointer;padding:.25rem}.fx-hamburger span{display:block;width:22px;height:2px;background:currentColor;transition:all .2s;border-radius:1px}.fx-hamburger.open span:nth-child(1){transform:rotate(45deg) translate(5px,5px)}.fx-hamburger.open span:nth-child(2){opacity:0}.fx-hamburger.open span:nth-child(3){transform:rotate(-45deg) translate(5px,-5px)}@media(max-width:640px){.fx-hamburger{display:flex}.fx-nav-links{display:none;width:100%;flex-direction:column;align-items:flex-start;gap:.75rem;padding:.75rem 0}.fx-nav-links.open{display:flex}}.fx-hero{display:flex;align-items:center;justify-content:center;min-height:92vh;padding:4rem 1.5rem}.fx-hero-split{display:grid;grid-template-columns:1fr 1fr;gap:3rem;align-items:center;padding:4rem 2.5rem;min-height:70vh}@media(max-width:768px){.fx-hero-split{grid-template-columns:1fr}}.fx-hero-img{width:100%;border-radius:1.25rem;object-fit:cover;max-height:500px}.fx-hero-inner{max-width:56rem;text-align:center;display:flex;flex-direction:column;align-items:center;gap:1.5rem}.fx-hero-split .fx-hero-inner{text-align:left;align-items:flex-start;max-width:none}.fx-title{font-size:clamp(2.5rem,8vw,5.5rem);font-weight:900;letter-spacing:-.04em;line-height:1}.fx-sub{font-size:clamp(1rem,2vw,1.25rem);line-height:1.75;max-width:40rem}.fx-cta{display:inline-flex;align-items:center;padding:.875rem 2.5rem;border-radius:.75rem;font-weight:700;font-size:1rem;letter-spacing:-.01em;transition:transform .15s;margin:.25rem}.fx-cta:hover{transform:translateY(-1px)}.fx-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:3rem;padding:5rem 2.5rem;text-align:center}.fx-stat-val{font-size:clamp(2.5rem,5vw,4rem);font-weight:900;letter-spacing:-.04em;line-height:1}.fx-stat-lbl{font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.1em;margin-top:.5rem}.fx-grid{display:grid;gap:1.25rem;padding:1rem 2.5rem 5rem}.fx-grid-2{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}.fx-grid-3{grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}.fx-grid-4{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.fx-card{border-radius:1rem;padding:1.75rem;transition:transform .2s,box-shadow .2s}.fx-card:hover{transform:translateY(-2px)}.fx-card-img{width:100%;border-radius:.75rem;object-fit:cover;height:180px;margin-bottom:1rem}.fx-icon{font-size:2rem;margin-bottom:1rem}.fx-card-title{font-size:1.0625rem;font-weight:700;letter-spacing:-.02em;margin-bottom:.5rem}.fx-card-body{font-size:.875rem;line-height:1.65}.fx-card-link{font-size:.8125rem;font-weight:600;display:inline-block;margin-top:1rem;opacity:.6;transition:opacity .15s}.fx-card-link:hover{opacity:1}.fx-sect{padding:5rem 2.5rem}.fx-sect-title{font-size:clamp(1.75rem,4vw,3rem);font-weight:800;letter-spacing:-.04em;margin-bottom:1.5rem;text-align:center}.fx-sect-body{font-size:1rem;line-height:1.75;text-align:center;max-width:48rem;margin:0 auto}.fx-form-wrap{padding:3rem 2.5rem;display:flex;justify-content:center}.fx-form{width:100%;max-width:28rem;border-radius:1.25rem;padding:2.5rem}.fx-field{margin-bottom:1.25rem}.fx-label{display:block;font-size:.8125rem;font-weight:600;margin-bottom:.5rem}.fx-input{width:100%;padding:.75rem 1rem;border-radius:.625rem;font-size:.9375rem;outline:none;transition:box-shadow .15s}.fx-input:focus{box-shadow:0 0 0 3px rgba(37,99,235,.35)}.fx-btn{width:100%;padding:.875rem 1.5rem;border:none;border-radius:.625rem;font-size:.9375rem;font-weight:700;cursor:pointer;margin-top:.5rem;transition:transform .15s,opacity .15s;letter-spacing:-.01em}.fx-btn:hover{transform:translateY(-1px)}.fx-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.fx-btn-wrap{padding:0 2.5rem 1.5rem}.fx-standalone-btn{width:auto;padding:.75rem 2rem;margin-top:0}.fx-form-msg{font-size:.8125rem;padding:.5rem 0;min-height:1.5rem;text-align:center}.fx-form-err{color:#f87171}.fx-form-ok{color:#4ade80}.fx-table-wrap{overflow-x:auto;padding:0 2.5rem 4rem}.fx-table{width:100%;border-collapse:collapse;font-size:.875rem}.fx-th{text-align:left;padding:.875rem 1.25rem;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em}.fx-th-actions{opacity:.6}.fx-tr{transition:background .1s}.fx-td{padding:.875rem 1.25rem}.fx-td-empty{padding:2rem 1.25rem;text-align:center;opacity:.4}.fx-td-actions{white-space:nowrap;padding:.5rem 1rem!important}.fx-action-btn{border:none;cursor:pointer;font-size:.75rem;font-weight:600;padding:.3rem .75rem;border-radius:.375rem;margin-right:.375rem;font-family:inherit;transition:opacity .15s}.fx-action-btn:hover{opacity:.85}.fx-edit-btn{background:#1e40af;color:#93c5fd}.fx-delete-btn{background:#7f1d1d;color:#fca5a5}.fx-select-wrap{padding:.5rem 2.5rem}.fx-select-block{width:auto;min-width:200px;margin-top:0}.fx-pricing{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1.5rem;padding:2rem 2.5rem 5rem;align-items:start}.fx-pricing-card{border-radius:1.25rem;padding:2rem;position:relative;transition:transform .2s}.fx-pricing-featured{transform:scale(1.03)}.fx-pricing-badge{position:absolute;top:-12px;left:50%;transform:translateX(-50%);background:#2563eb;color:#fff;font-size:.7rem;font-weight:700;padding:.25rem .875rem;border-radius:999px;white-space:nowrap;letter-spacing:.05em}.fx-pricing-name{font-size:.875rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;margin-bottom:.5rem;opacity:.7}.fx-pricing-price{font-size:3rem;font-weight:900;letter-spacing:-.05em;line-height:1;margin-bottom:.75rem}.fx-pricing-desc{font-size:.875rem;line-height:1.65;margin-bottom:1.5rem;opacity:.7}.fx-pricing-cta{display:block;text-align:center;padding:.75rem;border-radius:.625rem;font-weight:700;font-size:.9rem;transition:opacity .15s}.fx-pricing-cta:hover{opacity:.85}.fx-faq{max-width:48rem;margin:0 auto}.fx-faq-item{border-radius:.75rem;margin-bottom:.625rem;cursor:pointer;overflow:hidden;transition:background .15s}.fx-faq-q{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.25rem;font-size:.9375rem;font-weight:600}.fx-faq-arrow{transition:transform .2s;font-size:.75rem;opacity:.5}.fx-faq-item.open .fx-faq-arrow{transform:rotate(90deg)}.fx-faq-a{max-height:0;overflow:hidden;padding:0 1.25rem;font-size:.875rem;line-height:1.7;transition:max-height .3s,padding .3s}.fx-faq-item.open .fx-faq-a{max-height:300px;padding:.75rem 1.25rem 1.25rem}.fx-testi-wrap{padding:5rem 2.5rem;display:flex;justify-content:center}.fx-testi{max-width:42rem;text-align:center;display:flex;flex-direction:column;align-items:center;gap:1.25rem}.fx-testi-img{width:64px;height:64px;border-radius:50%;object-fit:cover}.fx-testi-avatar{width:64px;height:64px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:1.5rem;font-weight:700;background:#1e293b}.fx-testi-quote{font-size:1.25rem;line-height:1.7;font-style:italic;opacity:.9}.fx-testi-author{font-size:.875rem;font-weight:600;opacity:.5}.fx-gallery{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:.75rem;padding:1rem 2.5rem 4rem}.fx-gallery-item{border-radius:.75rem;overflow:hidden;aspect-ratio:4/3}.fx-gallery-item img{width:100%;height:100%;object-fit:cover;transition:transform .3s}.fx-gallery-item:hover img{transform:scale(1.04)}.fx-if-wrap{display:contents}.fx-footer{padding:3rem 2.5rem;text-align:center}.fx-footer-text{font-size:.8125rem}.fx-footer-link{font-size:.8125rem;margin:0 .75rem;opacity:.5;transition:opacity .15s}.fx-footer-link:hover{opacity:1}
|
|
1179
1147
|
.fx-hero-minimal{min-height:50vh!important}
|
package/package.json
CHANGED
|
@@ -6,23 +6,20 @@
|
|
|
6
6
|
const cfg = window.__AIPLANG_PAGE__
|
|
7
7
|
if (!cfg) return
|
|
8
8
|
|
|
9
|
-
// ── Global Store — cross-page state (like React Context / Zustand) ─
|
|
10
9
|
const _STORE_KEY = 'aiplang_store_v1'
|
|
11
10
|
const _globalStore = (() => {
|
|
12
11
|
try { return JSON.parse(sessionStorage.getItem(_STORE_KEY) || '{}') } catch { return {} }
|
|
13
12
|
})()
|
|
14
13
|
function syncStore(key, value) {
|
|
15
14
|
_globalStore[key] = value
|
|
16
|
-
try { sessionStorage.setItem(_STORE_KEY, JSON.stringify(_globalStore)) } catch {}
|
|
17
|
-
try { new BroadcastChannel(_STORE_KEY).postMessage({ key, value }) } catch {}
|
|
15
|
+
try { sessionStorage.setItem(_STORE_KEY, JSON.stringify(_globalStore)) } catch(_e) { if(typeof console !== 'undefined') console.debug('[aiplang]',_e?.message) }
|
|
16
|
+
try { new BroadcastChannel(_STORE_KEY).postMessage({ key, value }) } catch(_e) { if(typeof console !== 'undefined') console.debug('[aiplang]',_e?.message) }
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
// ── Page-level State ─────────────────────────────────────────────
|
|
21
19
|
const _state = {}
|
|
22
20
|
const _watchers = {}
|
|
23
21
|
const _storeKeys = new Set((cfg.stores || []).map(s => s.key))
|
|
24
22
|
|
|
25
|
-
// Bootstrap state: SSR data > global store > page state declarations
|
|
26
23
|
const _boot = { ...(window.__SSR_DATA__ || {}), ..._globalStore }
|
|
27
24
|
for (const [k, v] of Object.entries({ ...(cfg.state || {}), ..._boot })) {
|
|
28
25
|
try { _state[k] = typeof v === 'string' && (v.startsWith('[') || v.startsWith('{') || v === 'true' || v === 'false' || !isNaN(v)) ? JSON.parse(v) : v } catch { _state[k] = v }
|
|
@@ -31,14 +28,14 @@ for (const [k, v] of Object.entries({ ...(cfg.state || {}), ..._boot })) {
|
|
|
31
28
|
function get(key) { return _state[key] }
|
|
32
29
|
|
|
33
30
|
function set(key, value, _persist) {
|
|
34
|
-
|
|
31
|
+
|
|
35
32
|
const old = _state[key]
|
|
36
33
|
if (old === value) return
|
|
37
34
|
if (typeof value !== 'object' && old === value) return
|
|
38
35
|
if (typeof value === 'object' && value !== null && typeof old === 'object' && old !== null) {
|
|
39
|
-
|
|
36
|
+
|
|
40
37
|
if (Array.isArray(value) && Array.isArray(old) && value.length !== old.length) {
|
|
41
|
-
|
|
38
|
+
|
|
42
39
|
} else if (JSON.stringify(old) === JSON.stringify(value)) return
|
|
43
40
|
}
|
|
44
41
|
_state[key] = value
|
|
@@ -46,13 +43,12 @@ function set(key, value, _persist) {
|
|
|
46
43
|
notify(key)
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
// Cross-tab store sync (other pages update when store changes)
|
|
50
46
|
try {
|
|
51
47
|
const _bc = new BroadcastChannel(_STORE_KEY)
|
|
52
48
|
_bc.onmessage = ({ data: { key, value } }) => {
|
|
53
49
|
_state[key] = value; notify(key)
|
|
54
50
|
}
|
|
55
|
-
} catch {}
|
|
51
|
+
} catch(_e) { if(typeof console !== 'undefined') console.debug('[aiplang]',_e?.message) }
|
|
56
52
|
|
|
57
53
|
function watch(key, cb) {
|
|
58
54
|
if (!_watchers[key]) _watchers[key] = []
|
|
@@ -77,8 +73,7 @@ function notify(key) {
|
|
|
77
73
|
_pending.add(key)
|
|
78
74
|
if (!_batchScheduled) {
|
|
79
75
|
_batchScheduled = true
|
|
80
|
-
|
|
81
|
-
// Use rAF for user interaction (avoids mid-frame layout thrash)
|
|
76
|
+
|
|
82
77
|
Promise.resolve().then(() => {
|
|
83
78
|
if (_batchScheduled) requestAnimationFrame(flushBatch)
|
|
84
79
|
})
|
|
@@ -105,14 +100,54 @@ function resolvePath(tmpl, row) {
|
|
|
105
100
|
|
|
106
101
|
const _intervals = []
|
|
107
102
|
|
|
103
|
+
const _sid = Math.random().toString(36).slice(2)
|
|
104
|
+
|
|
108
105
|
async function runQuery(q) {
|
|
109
106
|
const path = resolve(q.path)
|
|
110
|
-
const
|
|
107
|
+
const isGet = (q.method || 'GET').toUpperCase() === 'GET'
|
|
108
|
+
const opts = {
|
|
109
|
+
method: q.method || 'GET',
|
|
110
|
+
headers: {
|
|
111
|
+
'Content-Type': 'application/json',
|
|
112
|
+
'Accept': 'application/msgpack, application/json',
|
|
113
|
+
'x-session-id': _sid
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (isGet && q._deltaReady) opts.headers['x-aiplang-delta'] = '1'
|
|
111
118
|
if (q.body) opts.body = JSON.stringify(q.body)
|
|
112
119
|
try {
|
|
113
|
-
const res
|
|
120
|
+
const res = await fetch(path, opts)
|
|
121
|
+
|
|
122
|
+
if (res.status === 304) { q._deltaReady = true; return get(q.target?.replace(/^@/,'')) || null }
|
|
114
123
|
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
115
|
-
const
|
|
124
|
+
const ct = res.headers.get('Content-Type') || ''
|
|
125
|
+
let data
|
|
126
|
+
if (ct.includes('msgpack')) {
|
|
127
|
+
const buf = await res.arrayBuffer()
|
|
128
|
+
data = _mp.decode(new Uint8Array(buf))
|
|
129
|
+
} else {
|
|
130
|
+
data = await res.json()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (data && data.__delta) {
|
|
134
|
+
const key = q.target ? q.target.replace(/^@/, '') : null
|
|
135
|
+
if (key) {
|
|
136
|
+
const current = [...(get(key) || [])]
|
|
137
|
+
|
|
138
|
+
for (const row of (data.changed || [])) {
|
|
139
|
+
const idx = current.findIndex(r => r.id === row.id)
|
|
140
|
+
if (idx >= 0) current[idx] = row; else current.push(row)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const delSet = new Set(data.deleted || [])
|
|
144
|
+
const merged = current.filter(r => !delSet.has(r.id))
|
|
145
|
+
set(key, merged)
|
|
146
|
+
q._deltaReady = true
|
|
147
|
+
return merged
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
q._deltaReady = true
|
|
116
151
|
applyAction(data, q.target, q.action)
|
|
117
152
|
return data
|
|
118
153
|
} catch (e) {
|
|
@@ -147,7 +182,7 @@ function applyAction(data, target, action) {
|
|
|
147
182
|
return true
|
|
148
183
|
})
|
|
149
184
|
set(fm[1], filtered)
|
|
150
|
-
} catch {}
|
|
185
|
+
} catch(_e) { if(typeof console !== 'undefined') console.debug('[aiplang]',_e?.message) }
|
|
151
186
|
return
|
|
152
187
|
}
|
|
153
188
|
const am = action.match(/^@([a-zA-Z_]+)\s*=\s*\$result$/)
|
|
@@ -281,15 +316,12 @@ function hydrateTables() {
|
|
|
281
316
|
}
|
|
282
317
|
}
|
|
283
318
|
|
|
284
|
-
|
|
285
|
-
// First render: full DocumentFragment build (fast)
|
|
286
|
-
// Re-renders: only update cells that actually changed
|
|
287
|
-
const _rowCache = new Map() // id → {score, status, ..., tr element}
|
|
319
|
+
const _rowCache = new Map()
|
|
288
320
|
const _colKeys = cols.map(c => c.key)
|
|
289
321
|
let _initialized = false
|
|
290
322
|
|
|
291
323
|
const renderRow = (row, idx) => {
|
|
292
|
-
|
|
324
|
+
|
|
293
325
|
}
|
|
294
326
|
|
|
295
327
|
const render = () => {
|
|
@@ -297,7 +329,6 @@ function hydrateTables() {
|
|
|
297
329
|
let rows = get(key)
|
|
298
330
|
if (!Array.isArray(rows)) rows = []
|
|
299
331
|
|
|
300
|
-
// Empty state
|
|
301
332
|
if (!rows.length) {
|
|
302
333
|
tbody.innerHTML = ''
|
|
303
334
|
_rowCache.clear()
|
|
@@ -428,9 +459,8 @@ function hydrateTables() {
|
|
|
428
459
|
tbody.appendChild(tr)
|
|
429
460
|
}
|
|
430
461
|
|
|
431
|
-
// ── Surgical update vs full initial build ────────────────────
|
|
432
462
|
if (!_initialized) {
|
|
433
|
-
|
|
463
|
+
|
|
434
464
|
_initialized = true
|
|
435
465
|
tbody.innerHTML = ''
|
|
436
466
|
const frag = document.createDocumentFragment()
|
|
@@ -446,7 +476,7 @@ function hydrateTables() {
|
|
|
446
476
|
td.textContent = row[col.key] != null ? row[col.key] : ''
|
|
447
477
|
tr.appendChild(td)
|
|
448
478
|
}
|
|
449
|
-
|
|
479
|
+
|
|
450
480
|
if (editPath || delPath) {
|
|
451
481
|
const actTd = document.createElement('td')
|
|
452
482
|
actTd.className = 'fx-td fx-td-actions'
|
|
@@ -484,16 +514,19 @@ function hydrateTables() {
|
|
|
484
514
|
frag.appendChild(tr)
|
|
485
515
|
}
|
|
486
516
|
tbody.appendChild(frag)
|
|
487
|
-
|
|
517
|
+
|
|
488
518
|
try {
|
|
489
519
|
const tc = _buildTypedCache(rows, _colKeys)
|
|
490
520
|
_rowCache._typed = tc
|
|
491
|
-
|
|
521
|
+
|
|
522
|
+
const compiledInit = window['__aip_init_' + stateKey]
|
|
523
|
+
if (compiledInit) {
|
|
524
|
+
_rowCache._compiled = compiledInit(rows)
|
|
525
|
+
_rowCache._compiled_n = rows.length
|
|
526
|
+
_rowCache._ids = rows.map(r => r.id != null ? r.id : null)
|
|
527
|
+
}
|
|
528
|
+
} catch(_e) { if(typeof console !== 'undefined') console.debug('[aiplang]',_e?.message) }
|
|
492
529
|
} else {
|
|
493
|
-
// UPDATE: off-main-thread diff + requestIdleCallback
|
|
494
|
-
// For 500+ rows: Worker computes diff on separate CPU core
|
|
495
|
-
// For <500 rows: sync diff (worker overhead not worth it)
|
|
496
|
-
// DOM patches always run on main thread but are minimal
|
|
497
530
|
|
|
498
531
|
const _makeRow = (row, idx) => {
|
|
499
532
|
const tr = document.createElement('tr')
|
|
@@ -531,10 +564,10 @@ function hydrateTables() {
|
|
|
531
564
|
}
|
|
532
565
|
|
|
533
566
|
if (rows.length >= 500) {
|
|
534
|
-
|
|
567
|
+
|
|
535
568
|
_diffAsync(rows, _colKeys, _rowCache).then(result => _schedIdle(() => _applyResult(result)))
|
|
536
569
|
} else {
|
|
537
|
-
|
|
570
|
+
|
|
538
571
|
_applyResult(_diffSync(rows, _colKeys, _rowCache))
|
|
539
572
|
}
|
|
540
573
|
}
|
|
@@ -648,7 +681,7 @@ function hydrateBindings() {
|
|
|
648
681
|
document.querySelectorAll('[data-fx-bind]').forEach(el => {
|
|
649
682
|
const expr = el.getAttribute('data-fx-bind')
|
|
650
683
|
const keys = (expr.match(/[@$][a-zA-Z_][a-zA-Z0-9_.]*/g) || []).map(m => m.slice(1).split('.')[0])
|
|
651
|
-
|
|
684
|
+
|
|
652
685
|
const simpleM = expr.match(/^[@$]([a-zA-Z_][a-zA-Z0-9_.]*)$/)
|
|
653
686
|
if (simpleM) {
|
|
654
687
|
const path = simpleM[1].split('.')
|
|
@@ -826,19 +859,20 @@ function injectActionCSS() {
|
|
|
826
859
|
document.head.appendChild(style)
|
|
827
860
|
}
|
|
828
861
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
// Main thread only handles tiny DOM patches — never competes with animations.
|
|
834
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
862
|
+
const _workerSrc = `
|
|
863
|
+
'use strict'
|
|
864
|
+
|
|
865
|
+
let _sab = null, _sabView = null
|
|
835
866
|
|
|
836
|
-
const _workerSrc = `'use strict'
|
|
837
867
|
self.onmessage = function(e) {
|
|
838
|
-
const { type, rows, colKeys, cache, reqId } = e.data
|
|
868
|
+
const { type, rows, colKeys, cache, reqId, sab } = e.data
|
|
869
|
+
|
|
870
|
+
if (sab) { _sab = sab; _sabView = new Int32Array(sab) }
|
|
871
|
+
|
|
839
872
|
if (type !== 'diff') return
|
|
840
873
|
const patches = [], inserts = [], deletes = []
|
|
841
874
|
const seenIds = new Set()
|
|
875
|
+
|
|
842
876
|
for (let i = 0; i < rows.length; i++) {
|
|
843
877
|
const row = rows[i]
|
|
844
878
|
const id = row.id != null ? row.id : i
|
|
@@ -852,15 +886,31 @@ self.onmessage = function(e) {
|
|
|
852
886
|
}
|
|
853
887
|
}
|
|
854
888
|
for (const id in cache) {
|
|
855
|
-
const nid =
|
|
889
|
+
const nid = isNaN(id) ? id : Number(id)
|
|
856
890
|
if (!seenIds.has(String(id)) && !seenIds.has(nid)) deletes.push(id)
|
|
857
891
|
}
|
|
858
|
-
|
|
859
|
-
|
|
892
|
+
|
|
893
|
+
if (_sabView && patches.length < 8000) {
|
|
894
|
+
Atomics.store(_sabView, 0, patches.length)
|
|
895
|
+
for (let i = 0; i < patches.length; i++) {
|
|
896
|
+
|
|
897
|
+
_sabView[1 + i*3] = typeof patches[i].id === 'number' ? patches[i].id : patches[i].id.length
|
|
898
|
+
_sabView[1 + i*3+1] = patches[i].col
|
|
899
|
+
_sabView[1 + i*3+2] = 0
|
|
900
|
+
}
|
|
901
|
+
Atomics.store(_sabView, 0, patches.length | 0x80000000)
|
|
902
|
+
|
|
903
|
+
self.postMessage({ type: 'patches', patches, inserts, deletes, reqId, usedSAB: true })
|
|
904
|
+
} else {
|
|
905
|
+
self.postMessage({ type: 'patches', patches, inserts, deletes, reqId, usedSAB: false })
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
`
|
|
860
909
|
|
|
861
910
|
let _diffWorker = null
|
|
862
911
|
const _wCbs = new Map()
|
|
863
912
|
let _wReq = 0
|
|
913
|
+
let _wSAB = null
|
|
864
914
|
|
|
865
915
|
function _getWorker() {
|
|
866
916
|
if (_diffWorker) return _diffWorker
|
|
@@ -871,6 +921,12 @@ function _getWorker() {
|
|
|
871
921
|
if (cb) { cb(e.data); _wCbs.delete(e.data.reqId) }
|
|
872
922
|
}
|
|
873
923
|
_diffWorker.onerror = () => { _diffWorker = null }
|
|
924
|
+
|
|
925
|
+
try {
|
|
926
|
+
const sab = new SharedArrayBuffer(8000 * 3 * 4 + 4)
|
|
927
|
+
_diffWorker.postMessage({ type: 'init_sab', sab }, [])
|
|
928
|
+
_wSAB = new Int32Array(sab)
|
|
929
|
+
} catch(_e) { if(typeof console !== 'undefined') console.debug('[aiplang]',_e?.message) }
|
|
874
930
|
} catch { _diffWorker = null }
|
|
875
931
|
return _diffWorker
|
|
876
932
|
}
|
|
@@ -887,35 +943,30 @@ function _diffAsync(rows, colKeys, rowCache) {
|
|
|
887
943
|
})
|
|
888
944
|
}
|
|
889
945
|
|
|
890
|
-
// ── TypedArray positional cache — beats Vue Vapor at all sizes ───
|
|
891
|
-
// Float64Array for numeric fields, Uint8Array for status/enum
|
|
892
|
-
// No Map.get in hot loop — pure positional array scan
|
|
893
|
-
// Strategy: string-compare first for status (cheap), only encode int on change
|
|
894
|
-
|
|
895
946
|
function _buildTypedCache(rows, colKeys) {
|
|
896
947
|
const n = rows.length
|
|
897
948
|
const isNum = colKeys.map(k => {
|
|
898
949
|
const v = rows[0]?.[k]; return typeof v === 'number' || (v != null && !isNaN(Number(v)) && typeof v !== 'string')
|
|
899
950
|
})
|
|
900
951
|
const scores = new Float64Array(n)
|
|
901
|
-
const statuses = new Uint8Array(n)
|
|
902
|
-
const strCols = []
|
|
952
|
+
const statuses = new Uint8Array(n)
|
|
953
|
+
const strCols = []
|
|
903
954
|
colKeys.forEach((k,j) => {
|
|
904
955
|
if (!isNum[j]) strCols.push(j)
|
|
905
956
|
})
|
|
906
957
|
rows.forEach((r,i) => {
|
|
907
|
-
|
|
958
|
+
|
|
908
959
|
colKeys.forEach((k,j) => { if(isNum[j]) { const buf=j===0?scores:null; if(buf) buf[i]=Number(r[k])||0 } })
|
|
909
|
-
|
|
960
|
+
|
|
910
961
|
const numIdx = colKeys.findIndex((_,j)=>isNum[j] && j>1)
|
|
911
962
|
if(numIdx>=0) scores[i] = Number(rows[i][colKeys[numIdx]])||0
|
|
912
|
-
|
|
963
|
+
|
|
913
964
|
const enumIdx = colKeys.findIndex((_,j)=>!isNum[j] && j>1)
|
|
914
965
|
if(enumIdx>=0) statuses[i] = _STATUS_INT[rows[i][colKeys[enumIdx]]]??0
|
|
915
966
|
})
|
|
916
967
|
return {
|
|
917
968
|
scores, statuses,
|
|
918
|
-
prevVals: colKeys.map(k => rows.map(r => r[k])),
|
|
969
|
+
prevVals: colKeys.map(k => rows.map(r => r[k])),
|
|
919
970
|
isNum, colKeys, n,
|
|
920
971
|
ids: rows.map(r => r.id)
|
|
921
972
|
}
|
|
@@ -925,14 +976,31 @@ function _diffSync(rows, colKeys, rowCache) {
|
|
|
925
976
|
const patches = [], inserts = [], deletes = [], seen = new Set()
|
|
926
977
|
const nCols = colKeys.length
|
|
927
978
|
|
|
928
|
-
|
|
929
|
-
|
|
979
|
+
const compiledKey = stateKey
|
|
980
|
+
const compiledInit = window['__aip_init_' + compiledKey]
|
|
981
|
+
const compiledDiff = window['__aip_diff_' + compiledKey]
|
|
982
|
+
|
|
983
|
+
if (compiledDiff && rowCache._compiled && rows.length === rowCache._compiled_n
|
|
984
|
+
&& rows.length > 0 && rowCache._ids?.length === rows.length) {
|
|
985
|
+
|
|
986
|
+
const raw = compiledDiff(rows, rowCache._compiled)
|
|
987
|
+
for (const pack of raw) {
|
|
988
|
+
const i = pack >> 4, col = pack & 0xf
|
|
989
|
+
const id = rowCache._ids[i]
|
|
990
|
+
if (id != null) {
|
|
991
|
+
seen.add(id)
|
|
992
|
+
patches.push({ id, col, val: rows[i][colKeys[col]] })
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
for (const [id] of rowCache) if (id !== '_compiled' && id !== '_ids' && id !== '_compiled_n' && !seen.has(id)) deletes.push(id)
|
|
996
|
+
return { patches, inserts, deletes }
|
|
997
|
+
}
|
|
998
|
+
|
|
930
999
|
const tc = rowCache._typed
|
|
931
1000
|
if (tc && rows.length === tc.n) {
|
|
932
1001
|
for (let i = 0; i < rows.length; i++) {
|
|
933
1002
|
const r = rows[i], id = tc.ids[i]
|
|
934
1003
|
seen.add(id)
|
|
935
|
-
// Per-column diff using per-column strategy
|
|
936
1004
|
for (let j = 0; j < nCols; j++) {
|
|
937
1005
|
const k = colKeys[j]
|
|
938
1006
|
const newVal = r[k]
|
|
@@ -947,7 +1015,6 @@ function _diffSync(rows, colKeys, rowCache) {
|
|
|
947
1015
|
return { patches, inserts, deletes }
|
|
948
1016
|
}
|
|
949
1017
|
|
|
950
|
-
// STANDARD PATH: Map-based diff (first render or variable-length data)
|
|
951
1018
|
for (let i = 0; i < rows.length; i++) {
|
|
952
1019
|
const r = rows[i], id = r.id != null ? r.id : i
|
|
953
1020
|
seen.add(id)
|
|
@@ -971,8 +1038,6 @@ function _diffSync(rows, colKeys, rowCache) {
|
|
|
971
1038
|
return { patches, inserts, deletes }
|
|
972
1039
|
}
|
|
973
1040
|
|
|
974
|
-
// requestIdleCallback scheduler — runs low-priority work when browser is idle
|
|
975
|
-
// Polling updates (30s intervals) don't need to be urgent — let animations breathe
|
|
976
1041
|
const _idleQ = []
|
|
977
1042
|
let _idleSched = false
|
|
978
1043
|
const _ric = window.requestIdleCallback
|
|
@@ -990,13 +1055,11 @@ function _schedIdle(fn) {
|
|
|
990
1055
|
function _flushIdle(dl) {
|
|
991
1056
|
_idleSched = false
|
|
992
1057
|
while (_idleQ.length && dl.timeRemaining() > 1) {
|
|
993
|
-
try { _idleQ.shift()() } catch {}
|
|
1058
|
+
try { _idleQ.shift()() } catch(_e) { if(typeof console !== 'undefined') console.debug('[aiplang]',_e?.message) }
|
|
994
1059
|
}
|
|
995
1060
|
if (_idleQ.length) { _idleSched = true; _ric(_flushIdle, { timeout: 5000 }) }
|
|
996
1061
|
}
|
|
997
1062
|
|
|
998
|
-
// Incremental renderer — processes rows in chunks between animation frames
|
|
999
|
-
// Zero dropped frames on 100k+ row datasets
|
|
1000
1063
|
async function _renderIncremental(items, renderFn, chunkSize = 200) {
|
|
1001
1064
|
for (let i = 0; i < items.length; i += chunkSize) {
|
|
1002
1065
|
const chunk = items.slice(i, i + chunkSize)
|
|
@@ -1007,9 +1070,6 @@ async function _renderIncremental(items, renderFn, chunkSize = 200) {
|
|
|
1007
1070
|
}
|
|
1008
1071
|
}
|
|
1009
1072
|
|
|
1010
|
-
|
|
1011
|
-
// ── Global reusable TypedArray buffers — zero allocation in hot path ──
|
|
1012
|
-
// Pre-allocated at startup, reused across every table render cycle
|
|
1013
1073
|
const _MAX_ROWS = 100000
|
|
1014
1074
|
const _scoreBuf = new Float64Array(_MAX_ROWS)
|
|
1015
1075
|
const _statusBuf = new Uint8Array(_MAX_ROWS)
|
|
@@ -1018,6 +1078,45 @@ const _STATUS_INT = {active:0,inactive:1,pending:2,blocked:3,
|
|
|
1018
1078
|
pending:2,done:3,todo:0,doing:1,done:2,
|
|
1019
1079
|
new:0,open:1,closed:2,resolved:3}
|
|
1020
1080
|
|
|
1081
|
+
const _mp = (() => {
|
|
1082
|
+
const td = new TextDecoder()
|
|
1083
|
+
function decode(buf) {
|
|
1084
|
+
const b = buf instanceof ArrayBuffer ? new Uint8Array(buf) : buf
|
|
1085
|
+
return _read(b, {p:0})
|
|
1086
|
+
}
|
|
1087
|
+
function _read(b, s) {
|
|
1088
|
+
const t = b[s.p++]
|
|
1089
|
+
if (t <= 0x7f) return t
|
|
1090
|
+
if (t >= 0xe0) return t - 256
|
|
1091
|
+
if ((t & 0xe0) === 0xa0) return _str(b, s, t & 0x1f)
|
|
1092
|
+
if ((t & 0xf0) === 0x90) return _arr(b, s, t & 0xf)
|
|
1093
|
+
if ((t & 0xf0) === 0x80) return _map(b, s, t & 0xf)
|
|
1094
|
+
switch (t) {
|
|
1095
|
+
case 0xc0: return null
|
|
1096
|
+
case 0xc2: return false
|
|
1097
|
+
case 0xc3: return true
|
|
1098
|
+
case 0xca: { const v=new DataView(b.buffer,b.byteOffset+s.p,4); s.p+=4; return v.getFloat32(0) }
|
|
1099
|
+
case 0xcb: { const v=new DataView(b.buffer,b.byteOffset+s.p,8); s.p+=8; return v.getFloat64(0) }
|
|
1100
|
+
case 0xcc: return b[s.p++]
|
|
1101
|
+
case 0xcd: { const v=(b[s.p]<<8)|b[s.p+1]; s.p+=2; return v }
|
|
1102
|
+
case 0xce: { const v=new DataView(b.buffer,b.byteOffset+s.p,4); s.p+=4; return v.getUint32(0) }
|
|
1103
|
+
case 0xd0: { const v=b[s.p++]; return v>127?v-256:v }
|
|
1104
|
+
case 0xd1: { const v=(b[s.p]<<8)|b[s.p+1]; s.p+=2; return v>32767?v-65536:v }
|
|
1105
|
+
case 0xd2: { const v=new DataView(b.buffer,b.byteOffset+s.p,4); s.p+=4; return v.getInt32(0) }
|
|
1106
|
+
case 0xd9: { const n=b[s.p++]; return _str(b,s,n) }
|
|
1107
|
+
case 0xda: { const n=(b[s.p]<<8)|b[s.p+1]; s.p+=2; return _str(b,s,n) }
|
|
1108
|
+
case 0xdc: { const n=(b[s.p]<<8)|b[s.p+1]; s.p+=2; return _arr(b,s,n) }
|
|
1109
|
+
case 0xdd: { const v=new DataView(b.buffer,b.byteOffset+s.p,4); s.p+=4; return _arr(b,s,v.getUint32(0)) }
|
|
1110
|
+
case 0xde: { const n=(b[s.p]<<8)|b[s.p+1]; s.p+=2; return _map(b,s,n) }
|
|
1111
|
+
default: return null
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
function _str(b,s,n){const v=td.decode(b.subarray(s.p,s.p+n));s.p+=n;return v}
|
|
1115
|
+
function _arr(b,s,n){const a=[];for(let i=0;i<n;i++)a.push(_read(b,s));return a}
|
|
1116
|
+
function _map(b,s,n){const o={};for(let i=0;i<n;i++){const k=_read(b,s);o[k]=_read(b,s)}return o}
|
|
1117
|
+
return { decode }
|
|
1118
|
+
})()
|
|
1119
|
+
|
|
1021
1120
|
function loadSSRData() {
|
|
1022
1121
|
const ssr = window.__SSR_DATA__
|
|
1023
1122
|
if (!ssr) return
|
|
@@ -1077,10 +1176,21 @@ function hydrateTableErrors() {
|
|
|
1077
1176
|
if (val === '__error__') {
|
|
1078
1177
|
if (tbody) {
|
|
1079
1178
|
const cols = JSON.parse(tbl.getAttribute('data-fx-cols') || '[]')
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1179
|
+
|
|
1180
|
+
tbody.innerHTML = ''
|
|
1181
|
+
const _fe = (() => {
|
|
1182
|
+
const tr=document.createElement('tr'), td=document.createElement('td')
|
|
1183
|
+
td.colSpan=cols.length+2; td.className='fx-td-empty'; td.style.color='#f87171'
|
|
1184
|
+
td.textContent=fallback
|
|
1185
|
+
if(retryPath){
|
|
1186
|
+
const btn=document.createElement('button')
|
|
1187
|
+
btn.textContent='↻ Retry'; btn.style.cssText='margin-left:.75rem;padding:.3rem .75rem;background:rgba(248,113,113,.1);border:1px solid rgba(248,113,113,.3);color:#f87171;border-radius:.375rem;cursor:pointer;font-size:.75rem'
|
|
1188
|
+
btn.onclick=()=>window.__aiplang_retry&&window.__aiplang_retry(binding,retryPath)
|
|
1189
|
+
td.appendChild(btn)
|
|
1190
|
+
}
|
|
1191
|
+
tr.appendChild(td); return tr
|
|
1192
|
+
})()
|
|
1193
|
+
tbody.appendChild(_fe)
|
|
1084
1194
|
}
|
|
1085
1195
|
}
|
|
1086
1196
|
})
|
package/server/server.js
CHANGED
|
@@ -246,6 +246,72 @@ async function processQueue() {
|
|
|
246
246
|
|
|
247
247
|
// ── Cache in-memory com TTL ──────────────────────────────────────
|
|
248
248
|
const _cache = new Map()
|
|
249
|
+
// ── Delta update cache ─────────────────────────────────────────────
|
|
250
|
+
// Stores a hash of each row per client session to send only changes
|
|
251
|
+
// Key: sessionId:binding → Map<rowId, hash>
|
|
252
|
+
const _deltaCache = new Map()
|
|
253
|
+
const _DELTA_TTL = 60000 // 1 min: expire client state
|
|
254
|
+
const _DELTA_MAX = 2000 // max concurrent sessions (prevent memory DoS)
|
|
255
|
+
|
|
256
|
+
function _rowHash(row) {
|
|
257
|
+
// FNV-1a 32-bit — fast, low collision for row change detection
|
|
258
|
+
// Only hashes enumerable own properties to avoid prototype pollution
|
|
259
|
+
let h = 2166136261
|
|
260
|
+
const keys = Object.keys(row).sort()
|
|
261
|
+
for (const k of keys) {
|
|
262
|
+
const v = row[k]
|
|
263
|
+
if (v === null || v === undefined) continue
|
|
264
|
+
const s = k + ':' + String(v)
|
|
265
|
+
for (let i = 0; i < s.length; i++) {
|
|
266
|
+
h = (h ^ s.charCodeAt(i)) >>> 0
|
|
267
|
+
h = Math.imul(h, 16777619) >>> 0
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return h >>> 0
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function _getDelta(sessionId, binding, rows) {
|
|
274
|
+
const key = sessionId + ':' + binding
|
|
275
|
+
const prev = _deltaCache.get(key)
|
|
276
|
+
// First request — send full dataset, store hashes
|
|
277
|
+
if (!prev) {
|
|
278
|
+
// Evict oldest entry when at capacity (LRU approximation)
|
|
279
|
+
if (_deltaCache.size >= _DELTA_MAX) {
|
|
280
|
+
let oldest = null, oldestTs = Infinity
|
|
281
|
+
for (const [k, v] of _deltaCache) if (v.ts < oldestTs) { oldest = k; oldestTs = v.ts }
|
|
282
|
+
if (oldest) _deltaCache.delete(oldest)
|
|
283
|
+
}
|
|
284
|
+
const hashes = new Map(rows.map(r => [r.id, _rowHash(r)]))
|
|
285
|
+
_deltaCache.set(key, { hashes, ts: Date.now() })
|
|
286
|
+
return { full: true, rows }
|
|
287
|
+
}
|
|
288
|
+
// Subsequent: compute delta
|
|
289
|
+
const changed = [], deleted = []
|
|
290
|
+
const newHashes = new Map()
|
|
291
|
+
for (const row of rows) {
|
|
292
|
+
const h = _rowHash(row)
|
|
293
|
+
newHashes.set(row.id, h)
|
|
294
|
+
if (prev.hashes.get(row.id) !== h) changed.push(row)
|
|
295
|
+
}
|
|
296
|
+
for (const [id] of prev.hashes) {
|
|
297
|
+
if (!newHashes.has(id)) deleted.push(id)
|
|
298
|
+
}
|
|
299
|
+
prev.hashes = newHashes; prev.ts = Date.now()
|
|
300
|
+
// Nothing changed — return 304-like empty delta
|
|
301
|
+
if (!changed.length && !deleted.length) return { full: false, changed: [], deleted: [], version: prev.ts }
|
|
302
|
+
return { full: false, changed, deleted, version: Date.now() }
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Clean up stale delta caches
|
|
306
|
+
setInterval(() => {
|
|
307
|
+
const now = Date.now()
|
|
308
|
+
for (const [k, v] of _deltaCache) if (now - v.ts > _DELTA_TTL * 5) _deltaCache.delete(k)
|
|
309
|
+
}, _DELTA_TTL)
|
|
310
|
+
|
|
311
|
+
// ── MessagePack encoder (AI-maintained) ────────────────────────────
|
|
312
|
+
const _mpEnc=(()=>{const te=new TextEncoder();function encode(v){const b=[];_w(v,b);return Buffer.from(b)}function _w(v,b){if(v===null||v===undefined){b.push(0xc0);return}if(v===false){b.push(0xc2);return}if(v===true){b.push(0xc3);return}const t=typeof v;if(t==='number'){if(Number.isInteger(v)&&v>=0&&v<=127){b.push(v);return}if(Number.isInteger(v)&&v>=-32&&v<0){b.push(v+256);return}if(Number.isInteger(v)&&v>=0&&v<=65535){b.push(0xcd,v>>8,v&0xff);return}const dv=new DataView(new ArrayBuffer(8));dv.setFloat64(0,v);b.push(0xcb,...new Uint8Array(dv.buffer));return}if(t==='string'){const e=te.encode(v);const n=e.length;if(n<=31)b.push(0xa0|n);else if(n<=255)b.push(0xd9,n);else b.push(0xda,n>>8,n&0xff);b.push(...e);return}if(Array.isArray(v)){const n=v.length;if(n<=15)b.push(0x90|n);else b.push(0xdc,n>>8,n&0xff);v.forEach(x=>_w(x,b));return}if(t==='object'){const ks=Object.keys(v);const n=ks.length;if(n<=15)b.push(0x80|n);else b.push(0xde,n>>8,n&0xff);ks.forEach(k=>{_w(k,b);_w(v[k],b)});return}}return{encode}})()
|
|
313
|
+
|
|
314
|
+
|
|
249
315
|
function cacheSet(key, value, ttlMs = 60000) {
|
|
250
316
|
_cache.set(key, { value, expires: Date.now() + ttlMs })
|
|
251
317
|
}
|
|
@@ -975,6 +1041,27 @@ async function execOp(line, ctx, server) {
|
|
|
975
1041
|
const exprParts=isNaN(parseInt(p[p.length-1]))?p:p.slice(0,-1)
|
|
976
1042
|
let result=evalExpr(exprParts.join(' '),ctx,server)
|
|
977
1043
|
if(result===null||result===undefined)result=ctx.vars['inserted']||ctx.vars['updated']||{}
|
|
1044
|
+
// Delta update — only active when client explicitly requests it
|
|
1045
|
+
if (Array.isArray(result) && ctx.req?.headers?.['x-aiplang-delta'] === '1' && result.length > 0) {
|
|
1046
|
+
try {
|
|
1047
|
+
const _req = ctx.req
|
|
1048
|
+
const sid = (_req.headers['x-session-id'] || _req.socket?.remoteAddress || 'anon').slice(0,64)
|
|
1049
|
+
// Key = session + full path (not just binding) to handle multi-table pages correctly
|
|
1050
|
+
const binding = (_req.url?.split('?')[0] || '/data').slice(0,128)
|
|
1051
|
+
const delta = _getDelta(sid, binding, result)
|
|
1052
|
+
if (delta.full) {
|
|
1053
|
+
ctx.res.json(status, result); return '__DONE__'
|
|
1054
|
+
}
|
|
1055
|
+
if (!delta.changed.length && !delta.deleted.length) {
|
|
1056
|
+
ctx.res.json(304, { __delta: true, changed: [], deleted: [], version: delta.version })
|
|
1057
|
+
return '__DONE__'
|
|
1058
|
+
}
|
|
1059
|
+
ctx.res.json(status, { __delta: true, changed: delta.changed, deleted: delta.deleted, version: delta.version })
|
|
1060
|
+
return '__DONE__'
|
|
1061
|
+
} catch(deltaErr) {
|
|
1062
|
+
// Delta failed — fall through to normal response
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
978
1065
|
ctx.res.json(status,result); return '__DONE__'
|
|
979
1066
|
}
|
|
980
1067
|
|
|
@@ -1244,7 +1331,23 @@ class AiplangServer {
|
|
|
1244
1331
|
if (route.method !== req.method) continue
|
|
1245
1332
|
const match = matchRoute(route.path, req.path); if (!match) continue
|
|
1246
1333
|
req.params = match
|
|
1247
|
-
res.json = (s, d) => {
|
|
1334
|
+
res.json = (s, d) => {
|
|
1335
|
+
if(typeof s==='object'){d=s;s=200}
|
|
1336
|
+
const accept = req.headers['accept']||''
|
|
1337
|
+
const ae = req.headers['accept-encoding']||''
|
|
1338
|
+
if(accept.includes('application/msgpack')){
|
|
1339
|
+
try{ const buf=_mpEnc.encode(d); res.writeHead(s,{'Content-Type':'application/msgpack','Content-Length':buf.length}); res.end(buf); return }catch{}
|
|
1340
|
+
}
|
|
1341
|
+
const body=JSON.stringify(d)
|
|
1342
|
+
if(ae.includes('gzip')&&body.length>512){
|
|
1343
|
+
require('zlib').gzip(body,(err,buf)=>{
|
|
1344
|
+
if(err){res.writeHead(s,{'Content-Type':'application/json'});res.end(body);return}
|
|
1345
|
+
res.writeHead(s,{'Content-Type':'application/json','Content-Encoding':'gzip'});res.end(buf)
|
|
1346
|
+
})
|
|
1347
|
+
} else {
|
|
1348
|
+
res.writeHead(s,{'Content-Type':'application/json'}); res.end(body)
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1248
1351
|
res.error = (s, m) => res.json(s, {error:m})
|
|
1249
1352
|
res.noContent = () => { res.writeHead(204); res.end() }
|
|
1250
1353
|
res.redirect = (u) => { res.writeHead(302,{Location:u}); res.end() }
|
|
@@ -1839,7 +1942,7 @@ async function startServer(aipFile, port = 3000) {
|
|
|
1839
1942
|
|
|
1840
1943
|
// Health
|
|
1841
1944
|
srv.addRoute('GET', '/health', (req, res) => res.json(200, {
|
|
1842
|
-
status:'ok', version:'2.10.
|
|
1945
|
+
status:'ok', version:'2.10.9',
|
|
1843
1946
|
models: app.models.map(m=>m.name),
|
|
1844
1947
|
routes: app.apis.length, pages: app.pages.length,
|
|
1845
1948
|
admin: app.admin?.prefix || null,
|