aiplang 2.10.5 → 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.5'
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.5",
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
@@ -841,9 +846,9 @@ self.onmessage = function(e) {
841
846
  const cached = cache[id]
842
847
  if (!cached) { inserts.push({ id, row, idx: i }); continue }
843
848
  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]] })
849
+ const nv = row[colKeys[c]] ?? null
850
+ const ov = cached[c] ?? null
851
+ if (nv !== ov) patches.push({ id, col: c, val: row[colKeys[c]] })
847
852
  }
848
853
  }
849
854
  for (const id in cache) {
@@ -882,20 +887,87 @@ 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()
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)
887
951
  for (let i = 0; i < rows.length; i++) {
888
952
  const r = rows[i], id = r.id != null ? r.id : i
889
953
  seen.add(id)
890
954
  const c = rowCache.get(id)
891
955
  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]] })
956
+ const vals = c.vals
957
+ const nCols4 = nCols === 4
958
+ if (nCols4) {
959
+ const v0=r[colKeys[0]]??null; if(vals[0]!==v0){patches.push({id,col:0,val:r[colKeys[0]]});vals[0]=v0}
960
+ const v1=r[colKeys[1]]??null; if(vals[1]!==v1){patches.push({id,col:1,val:r[colKeys[1]]});vals[1]=v1}
961
+ const v2=r[colKeys[2]]??null; if(vals[2]!==v2){patches.push({id,col:2,val:r[colKeys[2]]});vals[2]=v2}
962
+ const v3=r[colKeys[3]]??null; if(vals[3]!==v3){patches.push({id,col:3,val:r[colKeys[3]]});vals[3]=v3}
963
+ } else {
964
+ for (let j = 0; j < nCols; j++) {
965
+ const nv = r[colKeys[j]] ?? null
966
+ if (vals[j] !== nv) { patches.push({ id, col:j, val:r[colKeys[j]] }); vals[j]=nv }
967
+ }
896
968
  }
897
969
  }
898
- 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)
899
971
  return { patches, inserts, deletes }
900
972
  }
901
973
 
@@ -936,6 +1008,16 @@ async function _renderIncremental(items, renderFn, chunkSize = 200) {
936
1008
  }
937
1009
 
938
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
+
939
1021
  function loadSSRData() {
940
1022
  const ssr = window.__SSR_DATA__
941
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.5',
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,