aiplang 2.10.4 → 2.10.5
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 +1 -1
- package/package.json +1 -1
- package/runtime/aiplang-hydrate.js +156 -58
- package/server/server.js +1 -1
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.5'
|
|
9
9
|
const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
|
|
10
10
|
const cmd = process.argv[2]
|
|
11
11
|
const args = process.argv.slice(3)
|
package/package.json
CHANGED
|
@@ -485,69 +485,52 @@ function hydrateTables() {
|
|
|
485
485
|
}
|
|
486
486
|
tbody.appendChild(frag)
|
|
487
487
|
} else {
|
|
488
|
-
// UPDATE:
|
|
489
|
-
|
|
490
|
-
|
|
488
|
+
// UPDATE: off-main-thread diff + requestIdleCallback
|
|
489
|
+
// For 500+ rows: Worker computes diff on separate CPU core
|
|
490
|
+
// For <500 rows: sync diff (worker overhead not worth it)
|
|
491
|
+
// DOM patches always run on main thread but are minimal
|
|
491
492
|
|
|
492
|
-
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
cached.vals[c] = row[_colKeys[c]]
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
// Ensure tr is in correct position
|
|
509
|
-
if (tbodyRows[i] !== cached.tr) tbody.insertBefore(cached.tr, tbodyRows[i] || null)
|
|
510
|
-
} else {
|
|
511
|
-
// New row — create and insert
|
|
512
|
-
const tr = document.createElement('tr')
|
|
513
|
-
tr.className = 'fx-tr'
|
|
514
|
-
tr.dataset.id = rowId
|
|
515
|
-
for (const col of cols) {
|
|
516
|
-
const td = document.createElement('td')
|
|
517
|
-
td.className = 'fx-td'
|
|
518
|
-
td.textContent = row[col.key] != null ? row[col.key] : ''
|
|
519
|
-
tr.appendChild(td)
|
|
520
|
-
}
|
|
521
|
-
if (editPath || delPath) {
|
|
522
|
-
const actTd = document.createElement('td')
|
|
523
|
-
actTd.className = 'fx-td fx-td-actions'; actTd.style.cssText = 'white-space:nowrap'
|
|
524
|
-
if (editPath) {
|
|
525
|
-
const eb = document.createElement('button')
|
|
526
|
-
eb.className = 'fx-action-btn fx-edit-btn'; eb.textContent = '✎ Edit'
|
|
527
|
-
const _row = row, _i = i
|
|
528
|
-
eb.onclick = async () => { const upd = await editModal(_row,cols,editPath,editMethod,key); if(!upd) return; const arr=[...(get(key)||[])]; arr[_i]={..._row,...upd}; set(key,arr) }
|
|
529
|
-
actTd.appendChild(eb)
|
|
530
|
-
}
|
|
531
|
-
if (delPath) {
|
|
532
|
-
const db = document.createElement('button')
|
|
533
|
-
db.className = 'fx-action-btn fx-delete-btn'; db.textContent = '✕ Delete'
|
|
534
|
-
const _row = row, _i = i
|
|
535
|
-
db.onclick = async () => { if(!await confirm('Delete this record?')) return; const {ok,data}=await http('DELETE',resolvePath(delPath,_row),null); if(ok){set(key,(get(key)||[]).filter((_,j)=>j!==_i));toast('Deleted','ok')}else toast(data.message||'Error','err') }
|
|
536
|
-
actTd.appendChild(db)
|
|
537
|
-
}
|
|
538
|
-
tr.appendChild(actTd)
|
|
539
|
-
}
|
|
540
|
-
_rowCache.set(rowId, { vals: _colKeys.map(k => row[k]), tr })
|
|
541
|
-
tbody.insertBefore(tr, tbody.querySelectorAll('tr.fx-tr')[i] || null)
|
|
493
|
+
const _makeRow = (row, idx) => {
|
|
494
|
+
const tr = document.createElement('tr')
|
|
495
|
+
tr.className = 'fx-tr'; tr.dataset.id = row.id != null ? row.id : idx
|
|
496
|
+
for (const col of cols) {
|
|
497
|
+
const td = document.createElement('td'); td.className = 'fx-td'
|
|
498
|
+
td.textContent = row[col.key] != null ? row[col.key] : ''; tr.appendChild(td)
|
|
499
|
+
}
|
|
500
|
+
if (editPath || delPath) {
|
|
501
|
+
const actTd = document.createElement('td')
|
|
502
|
+
actTd.className = 'fx-td fx-td-actions'; actTd.style.cssText = 'white-space:nowrap'
|
|
503
|
+
if (editPath) { const eb=document.createElement('button');eb.className='fx-action-btn fx-edit-btn';eb.textContent='✎ Edit';const _r=row,_i=idx;eb.onclick=async()=>{const upd=await editModal(_r,cols,editPath,editMethod,key);if(!upd)return;const arr=[...(get(key)||[])];arr[_i]={..._r,...upd};set(key,arr)};actTd.appendChild(eb) }
|
|
504
|
+
if (delPath) { const db=document.createElement('button');db.className='fx-action-btn fx-delete-btn';db.textContent='✕ Delete';const _r=row,_i=idx;db.onclick=async()=>{if(!await confirm('Delete?'))return;const{ok,data}=await http('DELETE',resolvePath(delPath,_r),null);if(ok){set(key,(get(key)||[]).filter((_,j)=>j!==_i));toast('Deleted','ok')}else toast(data.message||'Error','err')};actTd.appendChild(db) }
|
|
505
|
+
tr.appendChild(actTd)
|
|
542
506
|
}
|
|
507
|
+
return tr
|
|
543
508
|
}
|
|
544
509
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
510
|
+
const _applyResult = ({ patches, inserts, deletes }) => {
|
|
511
|
+
for (const { id, col, val } of patches) {
|
|
512
|
+
const rc = _rowCache.get(id) || _rowCache.get(isNaN(id)?id:Number(id))
|
|
513
|
+
if (!rc) continue
|
|
514
|
+
const cells = rc.tr.querySelectorAll('.fx-td')
|
|
515
|
+
if (cells[col]) { cells[col].textContent = val != null ? val : ''; rc.vals[col] = val }
|
|
550
516
|
}
|
|
517
|
+
for (const { id, row, idx } of inserts) {
|
|
518
|
+
const tr = _makeRow(row, idx)
|
|
519
|
+
_rowCache.set(id, { vals: _colKeys.map(k => row[k]), tr })
|
|
520
|
+
tbody.insertBefore(tr, tbody.querySelectorAll('tr.fx-tr')[idx] || null)
|
|
521
|
+
}
|
|
522
|
+
for (const id of deletes) {
|
|
523
|
+
const rc = _rowCache.get(id) || _rowCache.get(isNaN(id)?id:Number(id))
|
|
524
|
+
if (rc) { rc.tr.remove(); _rowCache.delete(id); _rowCache.delete(String(id)) }
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (rows.length >= 500) {
|
|
529
|
+
// Large: worker diff → idle callback apply (zero main thread impact)
|
|
530
|
+
_diffAsync(rows, _colKeys, _rowCache).then(result => _schedIdle(() => _applyResult(result)))
|
|
531
|
+
} else {
|
|
532
|
+
// Small: sync diff, immediate apply
|
|
533
|
+
_applyResult(_diffSync(rows, _colKeys, _rowCache))
|
|
551
534
|
}
|
|
552
535
|
}
|
|
553
536
|
}
|
|
@@ -838,6 +821,121 @@ function injectActionCSS() {
|
|
|
838
821
|
document.head.appendChild(style)
|
|
839
822
|
}
|
|
840
823
|
|
|
824
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
825
|
+
// OFF-MAIN-THREAD ENGINE — better than React Fiber
|
|
826
|
+
// Fiber splits work across frames on the SAME thread.
|
|
827
|
+
// This moves diff computation to a SEPARATE CPU core via Web Worker.
|
|
828
|
+
// Main thread only handles tiny DOM patches — never competes with animations.
|
|
829
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
830
|
+
|
|
831
|
+
const _workerSrc = `'use strict'
|
|
832
|
+
self.onmessage = function(e) {
|
|
833
|
+
const { type, rows, colKeys, cache, reqId } = e.data
|
|
834
|
+
if (type !== 'diff') return
|
|
835
|
+
const patches = [], inserts = [], deletes = []
|
|
836
|
+
const seenIds = new Set()
|
|
837
|
+
for (let i = 0; i < rows.length; i++) {
|
|
838
|
+
const row = rows[i]
|
|
839
|
+
const id = row.id != null ? row.id : i
|
|
840
|
+
seenIds.add(typeof id === 'number' ? id : String(id))
|
|
841
|
+
const cached = cache[id]
|
|
842
|
+
if (!cached) { inserts.push({ id, row, idx: i }); continue }
|
|
843
|
+
for (let c = 0; c < colKeys.length; c++) {
|
|
844
|
+
const nStr = row[colKeys[c]] != null ? String(row[colKeys[c]]) : ''
|
|
845
|
+
const oStr = cached[c] != null ? String(cached[c]) : ''
|
|
846
|
+
if (nStr !== oStr) patches.push({ id, col: c, val: row[colKeys[c]] })
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
for (const id in cache) {
|
|
850
|
+
const nid = typeof id === 'number' ? id : (isNaN(id) ? id : Number(id))
|
|
851
|
+
if (!seenIds.has(String(id)) && !seenIds.has(nid)) deletes.push(id)
|
|
852
|
+
}
|
|
853
|
+
self.postMessage({ type: 'patches', patches, inserts, deletes, reqId })
|
|
854
|
+
}`
|
|
855
|
+
|
|
856
|
+
let _diffWorker = null
|
|
857
|
+
const _wCbs = new Map()
|
|
858
|
+
let _wReq = 0
|
|
859
|
+
|
|
860
|
+
function _getWorker() {
|
|
861
|
+
if (_diffWorker) return _diffWorker
|
|
862
|
+
try {
|
|
863
|
+
_diffWorker = new Worker(URL.createObjectURL(new Blob([_workerSrc], { type:'application/javascript' })))
|
|
864
|
+
_diffWorker.onmessage = (e) => {
|
|
865
|
+
const cb = _wCbs.get(e.data.reqId)
|
|
866
|
+
if (cb) { cb(e.data); _wCbs.delete(e.data.reqId) }
|
|
867
|
+
}
|
|
868
|
+
_diffWorker.onerror = () => { _diffWorker = null }
|
|
869
|
+
} catch { _diffWorker = null }
|
|
870
|
+
return _diffWorker
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function _diffAsync(rows, colKeys, rowCache) {
|
|
874
|
+
return new Promise(resolve => {
|
|
875
|
+
const w = _getWorker()
|
|
876
|
+
if (!w) { resolve(_diffSync(rows, colKeys, rowCache)); return }
|
|
877
|
+
const id = ++_wReq
|
|
878
|
+
_wCbs.set(id, resolve)
|
|
879
|
+
const cObj = {}
|
|
880
|
+
for (const [k, v] of rowCache.entries()) cObj[k] = v.vals
|
|
881
|
+
w.postMessage({ type:'diff', rows, colKeys, cache:cObj, reqId:id })
|
|
882
|
+
})
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function _diffSync(rows, colKeys, rowCache) {
|
|
886
|
+
const patches = [], inserts = [], deletes = [], seen = new Set()
|
|
887
|
+
for (let i = 0; i < rows.length; i++) {
|
|
888
|
+
const r = rows[i], id = r.id != null ? r.id : i
|
|
889
|
+
seen.add(id)
|
|
890
|
+
const c = rowCache.get(id)
|
|
891
|
+
if (!c) { inserts.push({ id, row:r, idx:i }); continue }
|
|
892
|
+
for (let j = 0; j < colKeys.length; j++) {
|
|
893
|
+
const n = r[colKeys[j]] != null ? String(r[colKeys[j]]) : ''
|
|
894
|
+
const o = c.vals[j] != null ? String(c.vals[j]) : ''
|
|
895
|
+
if (n !== o) patches.push({ id, col:j, val:r[colKeys[j]] })
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
for (const [id] of rowCache) if (!seen.has(id)) deletes.push(id)
|
|
899
|
+
return { patches, inserts, deletes }
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// requestIdleCallback scheduler — runs low-priority work when browser is idle
|
|
903
|
+
// Polling updates (30s intervals) don't need to be urgent — let animations breathe
|
|
904
|
+
const _idleQ = []
|
|
905
|
+
let _idleSched = false
|
|
906
|
+
const _ric = window.requestIdleCallback
|
|
907
|
+
? window.requestIdleCallback.bind(window)
|
|
908
|
+
: (cb) => setTimeout(() => cb({ timeRemaining: () => 16 }), 4)
|
|
909
|
+
|
|
910
|
+
function _schedIdle(fn) {
|
|
911
|
+
_idleQ.push(fn)
|
|
912
|
+
if (!_idleSched) {
|
|
913
|
+
_idleSched = true
|
|
914
|
+
_ric(_flushIdle, { timeout: 5000 })
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function _flushIdle(dl) {
|
|
919
|
+
_idleSched = false
|
|
920
|
+
while (_idleQ.length && dl.timeRemaining() > 1) {
|
|
921
|
+
try { _idleQ.shift()() } catch {}
|
|
922
|
+
}
|
|
923
|
+
if (_idleQ.length) { _idleSched = true; _ric(_flushIdle, { timeout: 5000 }) }
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Incremental renderer — processes rows in chunks between animation frames
|
|
927
|
+
// Zero dropped frames on 100k+ row datasets
|
|
928
|
+
async function _renderIncremental(items, renderFn, chunkSize = 200) {
|
|
929
|
+
for (let i = 0; i < items.length; i += chunkSize) {
|
|
930
|
+
const chunk = items.slice(i, i + chunkSize)
|
|
931
|
+
chunk.forEach((item, j) => renderFn(item, i + j))
|
|
932
|
+
if (i + chunkSize < items.length) {
|
|
933
|
+
await new Promise(r => requestAnimationFrame(r))
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
|
|
841
939
|
function loadSSRData() {
|
|
842
940
|
const ssr = window.__SSR_DATA__
|
|
843
941
|
if (!ssr) return
|
package/server/server.js
CHANGED
|
@@ -1839,7 +1839,7 @@ async function startServer(aipFile, port = 3000) {
|
|
|
1839
1839
|
|
|
1840
1840
|
// Health
|
|
1841
1841
|
srv.addRoute('GET', '/health', (req, res) => res.json(200, {
|
|
1842
|
-
status:'ok', version:'2.10.
|
|
1842
|
+
status:'ok', version:'2.10.5',
|
|
1843
1843
|
models: app.models.map(m=>m.name),
|
|
1844
1844
|
routes: app.apis.length, pages: app.pages.length,
|
|
1845
1845
|
admin: app.admin?.prefix || null,
|