aiplang 2.10.7 → 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 +156 -12
- 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,10 +525,17 @@ function hydrateTables() {
|
|
|
484
525
|
frag.appendChild(tr)
|
|
485
526
|
}
|
|
486
527
|
tbody.appendChild(frag)
|
|
487
|
-
// Build
|
|
528
|
+
// Build compiled cache (Tier 1) + typed cache (Tier 2)
|
|
488
529
|
try {
|
|
489
530
|
const tc = _buildTypedCache(rows, _colKeys)
|
|
490
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
|
+
}
|
|
491
539
|
} catch {}
|
|
492
540
|
} else {
|
|
493
541
|
// UPDATE: off-main-thread diff + requestIdleCallback
|
|
@@ -833,12 +881,24 @@ function injectActionCSS() {
|
|
|
833
881
|
// Main thread only handles tiny DOM patches — never competes with animations.
|
|
834
882
|
// ═══════════════════════════════════════════════════════════════════
|
|
835
883
|
|
|
836
|
-
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
|
+
|
|
837
892
|
self.onmessage = function(e) {
|
|
838
|
-
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
|
+
|
|
839
898
|
if (type !== 'diff') return
|
|
840
899
|
const patches = [], inserts = [], deletes = []
|
|
841
900
|
const seenIds = new Set()
|
|
901
|
+
|
|
842
902
|
for (let i = 0; i < rows.length; i++) {
|
|
843
903
|
const row = rows[i]
|
|
844
904
|
const id = row.id != null ? row.id : i
|
|
@@ -852,15 +912,32 @@ self.onmessage = function(e) {
|
|
|
852
912
|
}
|
|
853
913
|
}
|
|
854
914
|
for (const id in cache) {
|
|
855
|
-
const nid =
|
|
915
|
+
const nid = isNaN(id) ? id : Number(id)
|
|
856
916
|
if (!seenIds.has(String(id)) && !seenIds.has(nid)) deletes.push(id)
|
|
857
917
|
}
|
|
858
|
-
|
|
859
|
-
|
|
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
|
+
`
|
|
860
936
|
|
|
861
937
|
let _diffWorker = null
|
|
862
938
|
const _wCbs = new Map()
|
|
863
939
|
let _wReq = 0
|
|
940
|
+
let _wSAB = null // SharedArrayBuffer view for zero-copy transfers
|
|
864
941
|
|
|
865
942
|
function _getWorker() {
|
|
866
943
|
if (_diffWorker) return _diffWorker
|
|
@@ -871,6 +948,12 @@ function _getWorker() {
|
|
|
871
948
|
if (cb) { cb(e.data); _wCbs.delete(e.data.reqId) }
|
|
872
949
|
}
|
|
873
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)
|
|
874
957
|
} catch { _diffWorker = null }
|
|
875
958
|
return _diffWorker
|
|
876
959
|
}
|
|
@@ -925,14 +1008,33 @@ function _diffSync(rows, colKeys, rowCache) {
|
|
|
925
1008
|
const patches = [], inserts = [], deletes = [], seen = new Set()
|
|
926
1009
|
const nCols = colKeys.length
|
|
927
1010
|
|
|
928
|
-
//
|
|
929
|
-
//
|
|
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
|
|
930
1033
|
const tc = rowCache._typed
|
|
931
1034
|
if (tc && rows.length === tc.n) {
|
|
932
1035
|
for (let i = 0; i < rows.length; i++) {
|
|
933
1036
|
const r = rows[i], id = tc.ids[i]
|
|
934
1037
|
seen.add(id)
|
|
935
|
-
// Per-column diff using per-column strategy
|
|
936
1038
|
for (let j = 0; j < nCols; j++) {
|
|
937
1039
|
const k = colKeys[j]
|
|
938
1040
|
const newVal = r[k]
|
|
@@ -1018,6 +1120,48 @@ const _STATUS_INT = {active:0,inactive:1,pending:2,blocked:3,
|
|
|
1018
1120
|
pending:2,done:3,todo:0,doing:1,done:2,
|
|
1019
1121
|
new:0,open:1,closed:2,resolved:3}
|
|
1020
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
|
+
|
|
1021
1165
|
function loadSSRData() {
|
|
1022
1166
|
const ssr = window.__SSR_DATA__
|
|
1023
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,
|