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 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.6'
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
- 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||{}}):''
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiplang",
3
- "version": "2.10.6",
3
+ "version": "2.10.8",
4
4
  "description": "AI-first web language. One .aip file = complete app. Frontend + backend + database + auth.",
5
5
  "keywords": [
6
6
  "aiplang",
@@ -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 opts = { method: q.method, headers: { 'Content-Type': 'application/json' } }
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 = await fetch(path, opts)
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 data = await res.json()
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 = `'use strict'
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 = typeof id === 'number' ? id : (isNaN(id) ? id : Number(id))
915
+ const nid = isNaN(id) ? id : Number(id)
851
916
  if (!seenIds.has(String(id)) && !seenIds.has(nid)) deletes.push(id)
852
917
  }
853
- self.postMessage({ type: 'patches', patches, inserts, deletes, reqId })
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
- // Unrolled loops for 2-4 columns — avoids JS loop overhead (Vue does this via template compiler)
895
- if (nCols === 2) {
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) => { if(typeof s==='object'){d=s;s=200}; res.writeHead(s,{'Content-Type':'application/json'}); res.end(JSON.stringify(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.6',
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,