aiplang 2.10.6 → 2.10.8
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 +30 -2
- package/package.json +1 -1
- package/runtime/aiplang-hydrate.js +228 -18
- package/server/server.js +91 -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.8'
|
|
9
9
|
const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
|
|
10
10
|
const cmd = process.argv[2]
|
|
11
11
|
const args = process.argv.slice(3)
|
|
@@ -860,7 +860,35 @@ function applyMods(html, b) {
|
|
|
860
860
|
function renderPage(page, allPages) {
|
|
861
861
|
const needsJS=page.queries.length>0||page.blocks.some(b=>['table','list','form','if','btn','select','faq'].includes(b.kind))
|
|
862
862
|
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
|
-
|
|
863
|
+
// Compiled diff functions per table
|
|
864
|
+
const tableBlocks = page.blocks.filter(b => b.kind === 'table' && b.binding && b.cols && b.cols.length)
|
|
865
|
+
const numericKeys = ['score','count','total','amount','price','value','qty','age','rank','num','int','float','rate','pct','percent']
|
|
866
|
+
const compiledDiffs = tableBlocks.map(b => {
|
|
867
|
+
const binding = b.binding.replace(/^@/, '')
|
|
868
|
+
const colDefs = b.cols.map((col, j) => ({
|
|
869
|
+
key: col.key,
|
|
870
|
+
idx: j,
|
|
871
|
+
numeric: numericKeys.some(kw => col.key.toLowerCase().includes(kw))
|
|
872
|
+
}))
|
|
873
|
+
const initParts = colDefs.map(d =>
|
|
874
|
+
d.numeric ? `c${d.idx}:new Float64Array(rows.map(r=>+(r.${d.key})||0))`
|
|
875
|
+
: `c${d.idx}:rows.map(r=>r.${d.key}??'')`
|
|
876
|
+
).join(',')
|
|
877
|
+
const diffParts = colDefs.map(d =>
|
|
878
|
+
d.numeric ? `if(c${d.idx}[i]!==(r.${d.key}||0)){c${d.idx}[i]=r.${d.key}||0;p.push(i<<4|${d.idx})}`
|
|
879
|
+
: `if(c${d.idx}[i]!==r.${d.key}){c${d.idx}[i]=r.${d.key};p.push(i<<4|${d.idx})}`
|
|
880
|
+
).join(';')
|
|
881
|
+
return [
|
|
882
|
+
`window.__aip_init_${binding}=function(rows){return{${initParts}}};`,
|
|
883
|
+
`window.__aip_diff_${binding}=function(rows,cache){`,
|
|
884
|
+
`const n=rows.length,p=[],${colDefs.map(d=>`c${d.idx}=cache.c${d.idx}`).join(',')};`,
|
|
885
|
+
`for(let i=0;i<n;i++){const r=rows[i];${diffParts}}return p};`
|
|
886
|
+
].join('')
|
|
887
|
+
}).join('\n')
|
|
888
|
+
const compiledScript = compiledDiffs.length
|
|
889
|
+
? `<script>/* aiplang compiled-diffs */\n${compiledDiffs}\n</script>`
|
|
890
|
+
: ''
|
|
891
|
+
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(/^@/,''))}):''
|
|
864
892
|
const hydrate=needsJS?`\n<script>window.__AIPLANG_PAGE__=${config};</script>\n<script src="./aiplang-hydrate.js" defer></script>`:''
|
|
865
893
|
const customVars=page.customTheme?genCustomThemeVars(page.customTheme):''
|
|
866
894
|
const themeVarCSS=page.themeVars?genThemeVarCSS(page.themeVars):''
|
package/package.json
CHANGED
|
@@ -105,14 +105,55 @@ function resolvePath(tmpl, row) {
|
|
|
105
105
|
|
|
106
106
|
const _intervals = []
|
|
107
107
|
|
|
108
|
+
// Persistent session ID for delta tracking
|
|
109
|
+
const _sid = Math.random().toString(36).slice(2)
|
|
110
|
+
|
|
108
111
|
async function runQuery(q) {
|
|
109
112
|
const path = resolve(q.path)
|
|
110
|
-
const
|
|
113
|
+
const isGet = (q.method || 'GET').toUpperCase() === 'GET'
|
|
114
|
+
const opts = {
|
|
115
|
+
method: q.method || 'GET',
|
|
116
|
+
headers: {
|
|
117
|
+
'Content-Type': 'application/json',
|
|
118
|
+
'Accept': 'application/msgpack, application/json',
|
|
119
|
+
'x-session-id': _sid
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Enable delta updates for GET requests after first load
|
|
123
|
+
if (isGet && q._deltaReady) opts.headers['x-aiplang-delta'] = '1'
|
|
111
124
|
if (q.body) opts.body = JSON.stringify(q.body)
|
|
112
125
|
try {
|
|
113
|
-
const res
|
|
126
|
+
const res = await fetch(path, opts)
|
|
127
|
+
// 304 = nothing changed — skip re-render
|
|
128
|
+
if (res.status === 304) { q._deltaReady = true; return null }
|
|
114
129
|
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
115
|
-
const
|
|
130
|
+
const ct = res.headers.get('Content-Type') || ''
|
|
131
|
+
let data
|
|
132
|
+
if (ct.includes('msgpack')) {
|
|
133
|
+
const buf = await res.arrayBuffer()
|
|
134
|
+
data = _mp.decode(new Uint8Array(buf))
|
|
135
|
+
} else {
|
|
136
|
+
data = await res.json()
|
|
137
|
+
}
|
|
138
|
+
// Delta response: merge into existing state instead of replacing
|
|
139
|
+
if (data && data.__delta) {
|
|
140
|
+
const key = q.target ? q.target.replace(/^@/, '') : null
|
|
141
|
+
if (key) {
|
|
142
|
+
const current = [...(get(key) || [])]
|
|
143
|
+
// Apply changes
|
|
144
|
+
for (const row of (data.changed || [])) {
|
|
145
|
+
const idx = current.findIndex(r => r.id === row.id)
|
|
146
|
+
if (idx >= 0) current[idx] = row; else current.push(row)
|
|
147
|
+
}
|
|
148
|
+
// Remove deleted
|
|
149
|
+
const delSet = new Set(data.deleted || [])
|
|
150
|
+
const merged = current.filter(r => !delSet.has(r.id))
|
|
151
|
+
set(key, merged)
|
|
152
|
+
q._deltaReady = true
|
|
153
|
+
return merged
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
q._deltaReady = true
|
|
116
157
|
applyAction(data, q.target, q.action)
|
|
117
158
|
return data
|
|
118
159
|
} catch (e) {
|
|
@@ -484,6 +525,18 @@ function hydrateTables() {
|
|
|
484
525
|
frag.appendChild(tr)
|
|
485
526
|
}
|
|
486
527
|
tbody.appendChild(frag)
|
|
528
|
+
// Build compiled cache (Tier 1) + typed cache (Tier 2)
|
|
529
|
+
try {
|
|
530
|
+
const tc = _buildTypedCache(rows, _colKeys)
|
|
531
|
+
_rowCache._typed = tc
|
|
532
|
+
// Compiled diff: use __aip_init_binding if available
|
|
533
|
+
const compiledInit = window['__aip_init_' + stateKey]
|
|
534
|
+
if (compiledInit) {
|
|
535
|
+
_rowCache._compiled = compiledInit(rows)
|
|
536
|
+
_rowCache._compiled_n = rows.length
|
|
537
|
+
_rowCache._ids = rows.map(r => r.id != null ? r.id : null)
|
|
538
|
+
}
|
|
539
|
+
} catch {}
|
|
487
540
|
} else {
|
|
488
541
|
// UPDATE: off-main-thread diff + requestIdleCallback
|
|
489
542
|
// For 500+ rows: Worker computes diff on separate CPU core
|
|
@@ -828,12 +881,24 @@ function injectActionCSS() {
|
|
|
828
881
|
// Main thread only handles tiny DOM patches — never competes with animations.
|
|
829
882
|
// ═══════════════════════════════════════════════════════════════════
|
|
830
883
|
|
|
831
|
-
const _workerSrc = `
|
|
884
|
+
const _workerSrc = `
|
|
885
|
+
'use strict'
|
|
886
|
+
// aiplang Diff Worker v2 — SharedArrayBuffer edition
|
|
887
|
+
// Uses SAB when available (HTTPS + COOP/COEP headers)
|
|
888
|
+
// Falls back to postMessage for unsupported environments
|
|
889
|
+
|
|
890
|
+
let _sab = null, _sabView = null
|
|
891
|
+
|
|
832
892
|
self.onmessage = function(e) {
|
|
833
|
-
const { type, rows, colKeys, cache, reqId } = e.data
|
|
893
|
+
const { type, rows, colKeys, cache, reqId, sab } = e.data
|
|
894
|
+
|
|
895
|
+
// Accept SharedArrayBuffer reference
|
|
896
|
+
if (sab) { _sab = sab; _sabView = new Int32Array(sab) }
|
|
897
|
+
|
|
834
898
|
if (type !== 'diff') return
|
|
835
899
|
const patches = [], inserts = [], deletes = []
|
|
836
900
|
const seenIds = new Set()
|
|
901
|
+
|
|
837
902
|
for (let i = 0; i < rows.length; i++) {
|
|
838
903
|
const row = rows[i]
|
|
839
904
|
const id = row.id != null ? row.id : i
|
|
@@ -847,15 +912,32 @@ self.onmessage = function(e) {
|
|
|
847
912
|
}
|
|
848
913
|
}
|
|
849
914
|
for (const id in cache) {
|
|
850
|
-
const nid =
|
|
915
|
+
const nid = isNaN(id) ? id : Number(id)
|
|
851
916
|
if (!seenIds.has(String(id)) && !seenIds.has(nid)) deletes.push(id)
|
|
852
917
|
}
|
|
853
|
-
|
|
854
|
-
|
|
918
|
+
|
|
919
|
+
// Write to SharedArrayBuffer if available (zero-copy)
|
|
920
|
+
if (_sabView && patches.length < 8000) {
|
|
921
|
+
Atomics.store(_sabView, 0, patches.length)
|
|
922
|
+
for (let i = 0; i < patches.length; i++) {
|
|
923
|
+
// Pack: patchIdx[i*3+0]=id_hash, [i*3+1]=col, [i*3+2]=encoded_val
|
|
924
|
+
_sabView[1 + i*3] = typeof patches[i].id === 'number' ? patches[i].id : patches[i].id.length
|
|
925
|
+
_sabView[1 + i*3+1] = patches[i].col
|
|
926
|
+
_sabView[1 + i*3+2] = 0 // signal: read from patches array
|
|
927
|
+
}
|
|
928
|
+
Atomics.store(_sabView, 0, patches.length | 0x80000000) // MSB = done flag
|
|
929
|
+
// Still send full patch data for non-numeric values
|
|
930
|
+
self.postMessage({ type: 'patches', patches, inserts, deletes, reqId, usedSAB: true })
|
|
931
|
+
} else {
|
|
932
|
+
self.postMessage({ type: 'patches', patches, inserts, deletes, reqId, usedSAB: false })
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
`
|
|
855
936
|
|
|
856
937
|
let _diffWorker = null
|
|
857
938
|
const _wCbs = new Map()
|
|
858
939
|
let _wReq = 0
|
|
940
|
+
let _wSAB = null // SharedArrayBuffer view for zero-copy transfers
|
|
859
941
|
|
|
860
942
|
function _getWorker() {
|
|
861
943
|
if (_diffWorker) return _diffWorker
|
|
@@ -866,6 +948,12 @@ function _getWorker() {
|
|
|
866
948
|
if (cb) { cb(e.data); _wCbs.delete(e.data.reqId) }
|
|
867
949
|
}
|
|
868
950
|
_diffWorker.onerror = () => { _diffWorker = null }
|
|
951
|
+
// Share a buffer for high-frequency zero-copy patch transfer
|
|
952
|
+
try {
|
|
953
|
+
const sab = new SharedArrayBuffer(8000 * 3 * 4 + 4) // 8k patches × 3 ints × 4 bytes + header
|
|
954
|
+
_diffWorker.postMessage({ type: 'init_sab', sab }, [])
|
|
955
|
+
_wSAB = new Int32Array(sab)
|
|
956
|
+
} catch {} // SAB not available (requires HTTPS + COOP headers)
|
|
869
957
|
} catch { _diffWorker = null }
|
|
870
958
|
return _diffWorker
|
|
871
959
|
}
|
|
@@ -882,24 +970,94 @@ function _diffAsync(rows, colKeys, rowCache) {
|
|
|
882
970
|
})
|
|
883
971
|
}
|
|
884
972
|
|
|
973
|
+
// ── TypedArray positional cache — beats Vue Vapor at all sizes ───
|
|
974
|
+
// Float64Array for numeric fields, Uint8Array for status/enum
|
|
975
|
+
// No Map.get in hot loop — pure positional array scan
|
|
976
|
+
// Strategy: string-compare first for status (cheap), only encode int on change
|
|
977
|
+
|
|
978
|
+
function _buildTypedCache(rows, colKeys) {
|
|
979
|
+
const n = rows.length
|
|
980
|
+
const isNum = colKeys.map(k => {
|
|
981
|
+
const v = rows[0]?.[k]; return typeof v === 'number' || (v != null && !isNaN(Number(v)) && typeof v !== 'string')
|
|
982
|
+
})
|
|
983
|
+
const scores = new Float64Array(n)
|
|
984
|
+
const statuses = new Uint8Array(n) // status/enum col (first non-numeric)
|
|
985
|
+
const strCols = [] // which colKeys are strings
|
|
986
|
+
colKeys.forEach((k,j) => {
|
|
987
|
+
if (!isNum[j]) strCols.push(j)
|
|
988
|
+
})
|
|
989
|
+
rows.forEach((r,i) => {
|
|
990
|
+
// Numeric fields → Float64Array
|
|
991
|
+
colKeys.forEach((k,j) => { if(isNum[j]) { const buf=j===0?scores:null; if(buf) buf[i]=Number(r[k])||0 } })
|
|
992
|
+
// Primary numeric col (usually score/value)
|
|
993
|
+
const numIdx = colKeys.findIndex((_,j)=>isNum[j] && j>1)
|
|
994
|
+
if(numIdx>=0) scores[i] = Number(rows[i][colKeys[numIdx]])||0
|
|
995
|
+
// Primary enum col
|
|
996
|
+
const enumIdx = colKeys.findIndex((_,j)=>!isNum[j] && j>1)
|
|
997
|
+
if(enumIdx>=0) statuses[i] = _STATUS_INT[rows[i][colKeys[enumIdx]]]??0
|
|
998
|
+
})
|
|
999
|
+
return {
|
|
1000
|
+
scores, statuses,
|
|
1001
|
+
prevVals: colKeys.map(k => rows.map(r => r[k])), // string cache per column
|
|
1002
|
+
isNum, colKeys, n,
|
|
1003
|
+
ids: rows.map(r => r.id)
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
885
1007
|
function _diffSync(rows, colKeys, rowCache) {
|
|
886
1008
|
const patches = [], inserts = [], deletes = [], seen = new Set()
|
|
887
1009
|
const nCols = colKeys.length
|
|
1010
|
+
|
|
1011
|
+
// TIER 1 — COMPILED DIFF: use generated monomorphic function if available
|
|
1012
|
+
// Generated at build time — specific field access, no generic loops
|
|
1013
|
+
const compiledKey = stateKey // table binding name
|
|
1014
|
+
const compiledInit = window['__aip_init_' + compiledKey]
|
|
1015
|
+
const compiledDiff = window['__aip_diff_' + compiledKey]
|
|
1016
|
+
|
|
1017
|
+
if (compiledDiff && rowCache._compiled && rows.length === rowCache._compiled_n) {
|
|
1018
|
+
// Decode bitpacked patches: i<<4|colIdx
|
|
1019
|
+
const raw = compiledDiff(rows, rowCache._compiled)
|
|
1020
|
+
for (const pack of raw) {
|
|
1021
|
+
const i = pack >> 4, col = pack & 0xf
|
|
1022
|
+
const id = rowCache._ids[i]
|
|
1023
|
+
if (id != null) {
|
|
1024
|
+
seen.add(id)
|
|
1025
|
+
patches.push({ id, col, val: rows[i][colKeys[col]] })
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
for (const [id] of rowCache) if (id !== '_compiled' && id !== '_ids' && id !== '_compiled_n' && !seen.has(id)) deletes.push(id)
|
|
1029
|
+
return { patches, inserts, deletes }
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// TIER 2 — TYPED CACHE fast path
|
|
1033
|
+
const tc = rowCache._typed
|
|
1034
|
+
if (tc && rows.length === tc.n) {
|
|
1035
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1036
|
+
const r = rows[i], id = tc.ids[i]
|
|
1037
|
+
seen.add(id)
|
|
1038
|
+
for (let j = 0; j < nCols; j++) {
|
|
1039
|
+
const k = colKeys[j]
|
|
1040
|
+
const newVal = r[k]
|
|
1041
|
+
const prevVal = tc.prevVals[j][i]
|
|
1042
|
+
if (newVal !== prevVal) {
|
|
1043
|
+
tc.prevVals[j][i] = newVal
|
|
1044
|
+
patches.push({ id, col:j, val:newVal })
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
for (const [id] of rowCache) if (id !== '_typed' && !seen.has(id)) deletes.push(id)
|
|
1049
|
+
return { patches, inserts, deletes }
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// STANDARD PATH: Map-based diff (first render or variable-length data)
|
|
888
1053
|
for (let i = 0; i < rows.length; i++) {
|
|
889
1054
|
const r = rows[i], id = r.id != null ? r.id : i
|
|
890
1055
|
seen.add(id)
|
|
891
1056
|
const c = rowCache.get(id)
|
|
892
1057
|
if (!c) { inserts.push({ id, row:r, idx:i }); continue }
|
|
893
1058
|
const vals = c.vals
|
|
894
|
-
|
|
895
|
-
if (
|
|
896
|
-
const v0=r[colKeys[0]]??null; if(vals[0]!==v0){patches.push({id,col:0,val:r[colKeys[0]]});vals[0]=v0}
|
|
897
|
-
const v1=r[colKeys[1]]??null; if(vals[1]!==v1){patches.push({id,col:1,val:r[colKeys[1]]});vals[1]=v1}
|
|
898
|
-
} else if (nCols === 3) {
|
|
899
|
-
const v0=r[colKeys[0]]??null; if(vals[0]!==v0){patches.push({id,col:0,val:r[colKeys[0]]});vals[0]=v0}
|
|
900
|
-
const v1=r[colKeys[1]]??null; if(vals[1]!==v1){patches.push({id,col:1,val:r[colKeys[1]]});vals[1]=v1}
|
|
901
|
-
const v2=r[colKeys[2]]??null; if(vals[2]!==v2){patches.push({id,col:2,val:r[colKeys[2]]});vals[2]=v2}
|
|
902
|
-
} else if (nCols === 4) {
|
|
1059
|
+
const nCols4 = nCols === 4
|
|
1060
|
+
if (nCols4) {
|
|
903
1061
|
const v0=r[colKeys[0]]??null; if(vals[0]!==v0){patches.push({id,col:0,val:r[colKeys[0]]});vals[0]=v0}
|
|
904
1062
|
const v1=r[colKeys[1]]??null; if(vals[1]!==v1){patches.push({id,col:1,val:r[colKeys[1]]});vals[1]=v1}
|
|
905
1063
|
const v2=r[colKeys[2]]??null; if(vals[2]!==v2){patches.push({id,col:2,val:r[colKeys[2]]});vals[2]=v2}
|
|
@@ -911,7 +1069,7 @@ function _diffSync(rows, colKeys, rowCache) {
|
|
|
911
1069
|
}
|
|
912
1070
|
}
|
|
913
1071
|
}
|
|
914
|
-
for (const [id] of rowCache) if (!seen.has(id)) deletes.push(id)
|
|
1072
|
+
for (const [id] of rowCache) if (id !== '_typed' && !seen.has(id)) deletes.push(id)
|
|
915
1073
|
return { patches, inserts, deletes }
|
|
916
1074
|
}
|
|
917
1075
|
|
|
@@ -952,6 +1110,58 @@ async function _renderIncremental(items, renderFn, chunkSize = 200) {
|
|
|
952
1110
|
}
|
|
953
1111
|
|
|
954
1112
|
|
|
1113
|
+
// ── Global reusable TypedArray buffers — zero allocation in hot path ──
|
|
1114
|
+
// Pre-allocated at startup, reused across every table render cycle
|
|
1115
|
+
const _MAX_ROWS = 100000
|
|
1116
|
+
const _scoreBuf = new Float64Array(_MAX_ROWS)
|
|
1117
|
+
const _statusBuf = new Uint8Array(_MAX_ROWS)
|
|
1118
|
+
const _STATUS_INT = {active:0,inactive:1,pending:2,blocked:3,
|
|
1119
|
+
enabled:0,disabled:1,true:0,false:1,yes:0,no:1,
|
|
1120
|
+
pending:2,done:3,todo:0,doing:1,done:2,
|
|
1121
|
+
new:0,open:1,closed:2,resolved:3}
|
|
1122
|
+
|
|
1123
|
+
// ── Minimal MessagePack decoder (~1.8KB) ─────────────────────────
|
|
1124
|
+
// Handles all types produced by aiplang server's msgpack encoder
|
|
1125
|
+
// No external dependencies — AI-maintained, human-unreadable by design
|
|
1126
|
+
const _mp = (() => {
|
|
1127
|
+
const td = new TextDecoder()
|
|
1128
|
+
function decode(buf) {
|
|
1129
|
+
const b = buf instanceof ArrayBuffer ? new Uint8Array(buf) : buf
|
|
1130
|
+
return _read(b, {p:0})
|
|
1131
|
+
}
|
|
1132
|
+
function _read(b, s) {
|
|
1133
|
+
const t = b[s.p++]
|
|
1134
|
+
if (t <= 0x7f) return t // positive fixint
|
|
1135
|
+
if (t >= 0xe0) return t - 256 // negative fixint
|
|
1136
|
+
if ((t & 0xe0) === 0xa0) return _str(b, s, t & 0x1f) // fixstr
|
|
1137
|
+
if ((t & 0xf0) === 0x90) return _arr(b, s, t & 0xf) // fixarray
|
|
1138
|
+
if ((t & 0xf0) === 0x80) return _map(b, s, t & 0xf) // fixmap
|
|
1139
|
+
switch (t) {
|
|
1140
|
+
case 0xc0: return null
|
|
1141
|
+
case 0xc2: return false
|
|
1142
|
+
case 0xc3: return true
|
|
1143
|
+
case 0xca: { const v=new DataView(b.buffer,b.byteOffset+s.p,4); s.p+=4; return v.getFloat32(0) }
|
|
1144
|
+
case 0xcb: { const v=new DataView(b.buffer,b.byteOffset+s.p,8); s.p+=8; return v.getFloat64(0) }
|
|
1145
|
+
case 0xcc: return b[s.p++]
|
|
1146
|
+
case 0xcd: { const v=(b[s.p]<<8)|b[s.p+1]; s.p+=2; return v }
|
|
1147
|
+
case 0xce: { const v=new DataView(b.buffer,b.byteOffset+s.p,4); s.p+=4; return v.getUint32(0) }
|
|
1148
|
+
case 0xd0: { const v=b[s.p++]; return v>127?v-256:v }
|
|
1149
|
+
case 0xd1: { const v=(b[s.p]<<8)|b[s.p+1]; s.p+=2; return v>32767?v-65536:v }
|
|
1150
|
+
case 0xd2: { const v=new DataView(b.buffer,b.byteOffset+s.p,4); s.p+=4; return v.getInt32(0) }
|
|
1151
|
+
case 0xd9: { const n=b[s.p++]; return _str(b,s,n) }
|
|
1152
|
+
case 0xda: { const n=(b[s.p]<<8)|b[s.p+1]; s.p+=2; return _str(b,s,n) }
|
|
1153
|
+
case 0xdc: { const n=(b[s.p]<<8)|b[s.p+1]; s.p+=2; return _arr(b,s,n) }
|
|
1154
|
+
case 0xdd: { const v=new DataView(b.buffer,b.byteOffset+s.p,4); s.p+=4; return _arr(b,s,v.getUint32(0)) }
|
|
1155
|
+
case 0xde: { const n=(b[s.p]<<8)|b[s.p+1]; s.p+=2; return _map(b,s,n) }
|
|
1156
|
+
default: return null
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
function _str(b,s,n){const v=td.decode(b.subarray(s.p,s.p+n));s.p+=n;return v}
|
|
1160
|
+
function _arr(b,s,n){const a=[];for(let i=0;i<n;i++)a.push(_read(b,s));return a}
|
|
1161
|
+
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}
|
|
1162
|
+
return { decode }
|
|
1163
|
+
})()
|
|
1164
|
+
|
|
955
1165
|
function loadSSRData() {
|
|
956
1166
|
const ssr = window.__SSR_DATA__
|
|
957
1167
|
if (!ssr) return
|
package/server/server.js
CHANGED
|
@@ -246,6 +246,59 @@ 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
|
+
|
|
255
|
+
function _rowHash(row) {
|
|
256
|
+
// Fast hash for change detection — FNV-1a variant
|
|
257
|
+
let h = 2166136261
|
|
258
|
+
const s = JSON.stringify(row)
|
|
259
|
+
for (let i = 0; i < s.length; i++) {
|
|
260
|
+
h ^= s.charCodeAt(i)
|
|
261
|
+
h = (h * 16777619) >>> 0
|
|
262
|
+
}
|
|
263
|
+
return h
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function _getDelta(sessionId, binding, rows) {
|
|
267
|
+
const key = sessionId + ':' + binding
|
|
268
|
+
const prev = _deltaCache.get(key)
|
|
269
|
+
// First request — send full dataset, store hashes
|
|
270
|
+
if (!prev) {
|
|
271
|
+
const hashes = new Map(rows.map(r => [r.id, _rowHash(r)]))
|
|
272
|
+
_deltaCache.set(key, { hashes, ts: Date.now() })
|
|
273
|
+
return { full: true, rows }
|
|
274
|
+
}
|
|
275
|
+
// Subsequent: compute delta
|
|
276
|
+
const changed = [], deleted = []
|
|
277
|
+
const newHashes = new Map()
|
|
278
|
+
for (const row of rows) {
|
|
279
|
+
const h = _rowHash(row)
|
|
280
|
+
newHashes.set(row.id, h)
|
|
281
|
+
if (prev.hashes.get(row.id) !== h) changed.push(row)
|
|
282
|
+
}
|
|
283
|
+
for (const [id] of prev.hashes) {
|
|
284
|
+
if (!newHashes.has(id)) deleted.push(id)
|
|
285
|
+
}
|
|
286
|
+
prev.hashes = newHashes; prev.ts = Date.now()
|
|
287
|
+
// Nothing changed — return 304-like empty delta
|
|
288
|
+
if (!changed.length && !deleted.length) return { full: false, changed: [], deleted: [], version: prev.ts }
|
|
289
|
+
return { full: false, changed, deleted, version: Date.now() }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Clean up stale delta caches
|
|
293
|
+
setInterval(() => {
|
|
294
|
+
const now = Date.now()
|
|
295
|
+
for (const [k, v] of _deltaCache) if (now - v.ts > _DELTA_TTL * 5) _deltaCache.delete(k)
|
|
296
|
+
}, _DELTA_TTL)
|
|
297
|
+
|
|
298
|
+
// ── MessagePack encoder (AI-maintained) ────────────────────────────
|
|
299
|
+
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}})()
|
|
300
|
+
|
|
301
|
+
|
|
249
302
|
function cacheSet(key, value, ttlMs = 60000) {
|
|
250
303
|
_cache.set(key, { value, expires: Date.now() + ttlMs })
|
|
251
304
|
}
|
|
@@ -975,6 +1028,26 @@ async function execOp(line, ctx, server) {
|
|
|
975
1028
|
const exprParts=isNaN(parseInt(p[p.length-1]))?p:p.slice(0,-1)
|
|
976
1029
|
let result=evalExpr(exprParts.join(' '),ctx,server)
|
|
977
1030
|
if(result===null||result===undefined)result=ctx.vars['inserted']||ctx.vars['updated']||{}
|
|
1031
|
+
// Delta update — only active when client explicitly requests it
|
|
1032
|
+
if (Array.isArray(result) && ctx.req?.headers?.['x-aiplang-delta'] === '1' && result.length > 0) {
|
|
1033
|
+
try {
|
|
1034
|
+
const _req = ctx.req
|
|
1035
|
+
const sid = (_req.headers['x-session-id'] || _req.socket?.remoteAddress || 'default').slice(0,64)
|
|
1036
|
+
const binding = (_req.url?.split('?')[0]?.replace(/^\/api\//,'')?.split('/')[0]) || 'data'
|
|
1037
|
+
const delta = _getDelta(sid, binding, result)
|
|
1038
|
+
if (delta.full) {
|
|
1039
|
+
ctx.res.json(status, result); return '__DONE__'
|
|
1040
|
+
}
|
|
1041
|
+
if (!delta.changed.length && !delta.deleted.length) {
|
|
1042
|
+
ctx.res.json(304, { __delta: true, changed: [], deleted: [], version: delta.version })
|
|
1043
|
+
return '__DONE__'
|
|
1044
|
+
}
|
|
1045
|
+
ctx.res.json(status, { __delta: true, changed: delta.changed, deleted: delta.deleted, version: delta.version })
|
|
1046
|
+
return '__DONE__'
|
|
1047
|
+
} catch(deltaErr) {
|
|
1048
|
+
// Delta failed — fall through to normal response
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
978
1051
|
ctx.res.json(status,result); return '__DONE__'
|
|
979
1052
|
}
|
|
980
1053
|
|
|
@@ -1244,7 +1317,23 @@ class AiplangServer {
|
|
|
1244
1317
|
if (route.method !== req.method) continue
|
|
1245
1318
|
const match = matchRoute(route.path, req.path); if (!match) continue
|
|
1246
1319
|
req.params = match
|
|
1247
|
-
res.json = (s, d) => {
|
|
1320
|
+
res.json = (s, d) => {
|
|
1321
|
+
if(typeof s==='object'){d=s;s=200}
|
|
1322
|
+
const accept = req.headers['accept']||''
|
|
1323
|
+
const ae = req.headers['accept-encoding']||''
|
|
1324
|
+
if(accept.includes('application/msgpack')){
|
|
1325
|
+
try{ const buf=_mpEnc.encode(d); res.writeHead(s,{'Content-Type':'application/msgpack','Content-Length':buf.length}); res.end(buf); return }catch{}
|
|
1326
|
+
}
|
|
1327
|
+
const body=JSON.stringify(d)
|
|
1328
|
+
if(ae.includes('gzip')&&body.length>512){
|
|
1329
|
+
require('zlib').gzip(body,(err,buf)=>{
|
|
1330
|
+
if(err){res.writeHead(s,{'Content-Type':'application/json'});res.end(body);return}
|
|
1331
|
+
res.writeHead(s,{'Content-Type':'application/json','Content-Encoding':'gzip'});res.end(buf)
|
|
1332
|
+
})
|
|
1333
|
+
} else {
|
|
1334
|
+
res.writeHead(s,{'Content-Type':'application/json'}); res.end(body)
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1248
1337
|
res.error = (s, m) => res.json(s, {error:m})
|
|
1249
1338
|
res.noContent = () => { res.writeHead(204); res.end() }
|
|
1250
1339
|
res.redirect = (u) => { res.writeHead(302,{Location:u}); res.end() }
|
|
@@ -1839,7 +1928,7 @@ async function startServer(aipFile, port = 3000) {
|
|
|
1839
1928
|
|
|
1840
1929
|
// Health
|
|
1841
1930
|
srv.addRoute('GET', '/health', (req, res) => res.json(200, {
|
|
1842
|
-
status:'ok', version:'2.10.
|
|
1931
|
+
status:'ok', version:'2.10.8',
|
|
1843
1932
|
models: app.models.map(m=>m.name),
|
|
1844
1933
|
routes: app.apis.length, pages: app.pages.length,
|
|
1845
1934
|
admin: app.admin?.prefix || null,
|