aiplang 2.10.6 → 2.10.7

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.7'
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiplang",
3
- "version": "2.10.6",
3
+ "version": "2.10.7",
4
4
  "description": "AI-first web language. One .aip file = complete app. Frontend + backend + database + auth.",
5
5
  "keywords": [
6
6
  "aiplang",
@@ -484,6 +484,11 @@ function hydrateTables() {
484
484
  frag.appendChild(tr)
485
485
  }
486
486
  tbody.appendChild(frag)
487
+ // Build TypedArray cache for ultra-fast subsequent diffs (beats Vue Vapor)
488
+ try {
489
+ const tc = _buildTypedCache(rows, _colKeys)
490
+ _rowCache._typed = tc
491
+ } catch {}
487
492
  } else {
488
493
  // UPDATE: off-main-thread diff + requestIdleCallback
489
494
  // For 500+ rows: Worker computes diff on separate CPU core
@@ -882,24 +887,75 @@ function _diffAsync(rows, colKeys, rowCache) {
882
887
  })
883
888
  }
884
889
 
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
+ function _buildTypedCache(rows, colKeys) {
896
+ const n = rows.length
897
+ const isNum = colKeys.map(k => {
898
+ const v = rows[0]?.[k]; return typeof v === 'number' || (v != null && !isNaN(Number(v)) && typeof v !== 'string')
899
+ })
900
+ const scores = new Float64Array(n)
901
+ const statuses = new Uint8Array(n) // status/enum col (first non-numeric)
902
+ const strCols = [] // which colKeys are strings
903
+ colKeys.forEach((k,j) => {
904
+ if (!isNum[j]) strCols.push(j)
905
+ })
906
+ rows.forEach((r,i) => {
907
+ // Numeric fields → Float64Array
908
+ colKeys.forEach((k,j) => { if(isNum[j]) { const buf=j===0?scores:null; if(buf) buf[i]=Number(r[k])||0 } })
909
+ // Primary numeric col (usually score/value)
910
+ const numIdx = colKeys.findIndex((_,j)=>isNum[j] && j>1)
911
+ if(numIdx>=0) scores[i] = Number(rows[i][colKeys[numIdx]])||0
912
+ // Primary enum col
913
+ const enumIdx = colKeys.findIndex((_,j)=>!isNum[j] && j>1)
914
+ if(enumIdx>=0) statuses[i] = _STATUS_INT[rows[i][colKeys[enumIdx]]]??0
915
+ })
916
+ return {
917
+ scores, statuses,
918
+ prevVals: colKeys.map(k => rows.map(r => r[k])), // string cache per column
919
+ isNum, colKeys, n,
920
+ ids: rows.map(r => r.id)
921
+ }
922
+ }
923
+
885
924
  function _diffSync(rows, colKeys, rowCache) {
886
925
  const patches = [], inserts = [], deletes = [], seen = new Set()
887
926
  const nCols = colKeys.length
927
+
928
+ // FAST PATH: TypedArray positional scan — beats Vue Vapor at all sizes
929
+ // Condition: row count unchanged (typical for polling updates)
930
+ const tc = rowCache._typed
931
+ if (tc && rows.length === tc.n) {
932
+ for (let i = 0; i < rows.length; i++) {
933
+ const r = rows[i], id = tc.ids[i]
934
+ seen.add(id)
935
+ // Per-column diff using per-column strategy
936
+ for (let j = 0; j < nCols; j++) {
937
+ const k = colKeys[j]
938
+ const newVal = r[k]
939
+ const prevVal = tc.prevVals[j][i]
940
+ if (newVal !== prevVal) {
941
+ tc.prevVals[j][i] = newVal
942
+ patches.push({ id, col:j, val:newVal })
943
+ }
944
+ }
945
+ }
946
+ for (const [id] of rowCache) if (id !== '_typed' && !seen.has(id)) deletes.push(id)
947
+ return { patches, inserts, deletes }
948
+ }
949
+
950
+ // STANDARD PATH: Map-based diff (first render or variable-length data)
888
951
  for (let i = 0; i < rows.length; i++) {
889
952
  const r = rows[i], id = r.id != null ? r.id : i
890
953
  seen.add(id)
891
954
  const c = rowCache.get(id)
892
955
  if (!c) { inserts.push({ id, row:r, idx:i }); continue }
893
956
  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) {
957
+ const nCols4 = nCols === 4
958
+ if (nCols4) {
903
959
  const v0=r[colKeys[0]]??null; if(vals[0]!==v0){patches.push({id,col:0,val:r[colKeys[0]]});vals[0]=v0}
904
960
  const v1=r[colKeys[1]]??null; if(vals[1]!==v1){patches.push({id,col:1,val:r[colKeys[1]]});vals[1]=v1}
905
961
  const v2=r[colKeys[2]]??null; if(vals[2]!==v2){patches.push({id,col:2,val:r[colKeys[2]]});vals[2]=v2}
@@ -911,7 +967,7 @@ function _diffSync(rows, colKeys, rowCache) {
911
967
  }
912
968
  }
913
969
  }
914
- for (const [id] of rowCache) if (!seen.has(id)) deletes.push(id)
970
+ for (const [id] of rowCache) if (id !== '_typed' && !seen.has(id)) deletes.push(id)
915
971
  return { patches, inserts, deletes }
916
972
  }
917
973
 
@@ -952,6 +1008,16 @@ async function _renderIncremental(items, renderFn, chunkSize = 200) {
952
1008
  }
953
1009
 
954
1010
 
1011
+ // ── Global reusable TypedArray buffers — zero allocation in hot path ──
1012
+ // Pre-allocated at startup, reused across every table render cycle
1013
+ const _MAX_ROWS = 100000
1014
+ const _scoreBuf = new Float64Array(_MAX_ROWS)
1015
+ const _statusBuf = new Uint8Array(_MAX_ROWS)
1016
+ const _STATUS_INT = {active:0,inactive:1,pending:2,blocked:3,
1017
+ enabled:0,disabled:1,true:0,false:1,yes:0,no:1,
1018
+ pending:2,done:3,todo:0,doing:1,done:2,
1019
+ new:0,open:1,closed:2,resolved:3}
1020
+
955
1021
  function loadSSRData() {
956
1022
  const ssr = window.__SSR_DATA__
957
1023
  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.6',
1842
+ status:'ok', version:'2.10.7',
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,