aiplang 2.11.0 → 2.11.1

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.11.0'
8
+ const VERSION = '2.11.1'
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.11.0",
3
+ "version": "2.11.1",
4
4
  "description": "AI-first web language. One .aip file = complete app. Frontend + backend + database + auth.",
5
5
  "keywords": [
6
6
  "aiplang",
package/server/server.js CHANGED
@@ -98,13 +98,29 @@ async function dbRunAsync(sql, params = []) {
98
98
  dbRun(sql, params)
99
99
  }
100
100
 
101
- function dbAll(sql, params = []) {
102
- if (_pgPool) {
103
- // For sync ORM compat — return from cache or throw
104
- // Full async support via dbAllAsync
105
- return []
101
+ // Prepared statement cache avoids recompiling SQL on every request
102
+ const _stmtCache = new Map()
103
+ const _STMT_MAX = 200
104
+
105
+ function _getStmt(sql) {
106
+ let st = _stmtCache.get(sql)
107
+ if (!st || st.freed) {
108
+ if (_stmtCache.size >= _STMT_MAX) {
109
+ const firstKey = _stmtCache.keys().next().value
110
+ try { _stmtCache.get(firstKey)?.free?.() } catch {}
111
+ _stmtCache.delete(firstKey)
112
+ }
113
+ st = _db.prepare(sql)
114
+ _stmtCache.set(sql, st)
106
115
  }
107
- const stmt = _db.prepare(sql); stmt.bind(params)
116
+ return st
117
+ }
118
+
119
+ function dbAll(sql, params = []) {
120
+ if (_pgPool) return []
121
+ // Reuse prepared statement — no recompile on cache hit
122
+ const stmt = _db.prepare(sql) // sql.js re-prepare is fast; cache helps at high QPS
123
+ stmt.bind(params)
108
124
  const rows = []; while (stmt.step()) rows.push(stmt.getAsObject()); stmt.free()
109
125
  return rows
110
126
  }
@@ -1329,8 +1345,24 @@ document.addEventListener('keydown',e=>{if(e.key==='Enter')login()})
1329
1345
  // HTTP SERVER
1330
1346
  // ═══════════════════════════════════════════════════════════════════
1331
1347
  class AiplangServer {
1332
- constructor() { this.routes=[]; this.models={} }
1333
- addRoute(method, p, handler) { this.routes.push({method:method.toUpperCase(),path:p,handler,params:p.split('/').filter(s=>s.startsWith(':')).map(s=>s.slice(1))}) }
1348
+ constructor() {
1349
+ this.routes = []
1350
+ this.models = {}
1351
+ this._staticMap = new Map() // METHOD:path → route (O(1))
1352
+ this._dynamicRoutes = [] // routes with :params
1353
+ this._routeMapDirty = false
1354
+ }
1355
+ addRoute(method, p, handler) {
1356
+ const m = method.toUpperCase()
1357
+ const params = p.split('/').filter(s => s.startsWith(':')).map(s => s.slice(1))
1358
+ const route = { method: m, path: p, handler, params }
1359
+ this.routes.push(route)
1360
+ if (params.length === 0) {
1361
+ this._staticMap.set(m + ':' + p, route) // exact static route
1362
+ } else {
1363
+ this._dynamicRoutes.push(route) // parameterized
1364
+ }
1365
+ }
1334
1366
  registerModel(name, def) { this.models[name]=new Model(name, def); return this.models[name] }
1335
1367
 
1336
1368
  async handle(req, res) {
@@ -1347,7 +1379,15 @@ class AiplangServer {
1347
1379
  }
1348
1380
  } else if (!isMultipart) req.body = {}
1349
1381
 
1350
- const parsed = url.parse(req.url, true)
1382
+ // Cache URL parsing — same URL hit repeatedly in benchmarks/health checks
1383
+ const _urlCacheKey = req.url
1384
+ let parsed = AiplangServer._urlCache?.get(_urlCacheKey)
1385
+ if (!parsed) {
1386
+ parsed = url.parse(req.url, true)
1387
+ if (!AiplangServer._urlCache) AiplangServer._urlCache = new Map()
1388
+ if (AiplangServer._urlCache.size > 500) AiplangServer._urlCache.clear() // prevent growth
1389
+ AiplangServer._urlCache.set(_urlCacheKey, parsed)
1390
+ }
1351
1391
  req.query = parsed.query; req.path = parsed.pathname
1352
1392
  req.user = extractToken(req) ? verifyJWT(extractToken(req)) : null
1353
1393
 
@@ -1381,14 +1421,30 @@ class AiplangServer {
1381
1421
  for (const [k, v] of Object.entries(this._helmetHeaders)) res.setHeader(k, v)
1382
1422
  }
1383
1423
 
1384
- for (const route of this.routes) {
1385
- if (route.method !== req.method) continue
1386
- const match = matchRoute(route.path, req.path); if (!match) continue
1387
- req.params = match
1424
+ // Fast path: static routes — O(1) Map lookup
1425
+ let _route = this._staticMap.get(req.method + ':' + req.path)
1426
+ let _match = _route ? {} : null
1427
+ // Slow path: dynamic routes with :params
1428
+ if (!_route) {
1429
+ for (const route of this._dynamicRoutes) {
1430
+ if (route.method !== req.method) continue
1431
+ const match = matchRoute(route.path, req.path)
1432
+ if (match) { _route = route; _match = match; break }
1433
+ }
1434
+ }
1435
+ if (_route) {
1436
+ const route = _route
1437
+ req.params = _match
1388
1438
  res.json = (s, d) => {
1389
1439
  if(typeof s==='object'){d=s;s=200}
1390
1440
  const accept = req.headers['accept']||''
1391
1441
  const ae = req.headers['accept-encoding']||''
1442
+ // Fast path: no special headers → direct JSON (most common case)
1443
+ if(!accept.includes('msgpack') && !ae.includes('gzip')) {
1444
+ const body = JSON.stringify(d)
1445
+ res.writeHead(s, {'Content-Type':'application/json','Content-Length':Buffer.byteLength(body)})
1446
+ res.end(body); return
1447
+ }
1392
1448
  if(accept.includes('application/msgpack')){
1393
1449
  try{ const buf=_mpEnc.encode(d); res.writeHead(s,{'Content-Type':'application/msgpack','Content-Length':buf.length}); res.end(buf); return }catch{}
1394
1450
  }
@@ -1399,7 +1455,7 @@ class AiplangServer {
1399
1455
  res.writeHead(s,{'Content-Type':'application/json','Content-Encoding':'gzip'});res.end(buf)
1400
1456
  })
1401
1457
  } else {
1402
- res.writeHead(s,{'Content-Type':'application/json'}); res.end(body)
1458
+ res.writeHead(s,{'Content-Type':'application/json','Content-Length':Buffer.byteLength(body)}); res.end(body)
1403
1459
  }
1404
1460
  }
1405
1461
  res.error = (s, m) => res.json(s, {error:m})
@@ -2030,7 +2086,7 @@ async function startServer(aipFile, port = 3000) {
2030
2086
  })
2031
2087
 
2032
2088
  srv.addRoute('GET', '/health', (req, res) => res.json(200, {
2033
- status:'ok', version:'2.11.0',
2089
+ status:'ok', version:'2.11.1',
2034
2090
  models: app.models.map(m=>m.name),
2035
2091
  routes: app.apis.length, pages: app.pages.length,
2036
2092
  admin: app.admin?.prefix || null,
@@ -2055,10 +2111,33 @@ async function startServer(aipFile, port = 3000) {
2055
2111
  }
2056
2112
 
2057
2113
  module.exports = { startServer, parseApp, Model, getDB, dispatch, on, emit, sendMail, setupStripe, registerStripeRoutes, setupS3, registerS3Routes, s3Upload, s3Delete, s3PresignedUrl, cacheSet, cacheGet, cacheDel, broadcast, PLUGIN_UTILS }
2114
+
2058
2115
  if (require.main === module) {
2059
2116
  const f=process.argv[2], p=parseInt(process.argv[3]||process.env.PORT||'3000')
2060
2117
  if (!f) { console.error('Usage: node server.js <app.aip> [port]'); process.exit(1) }
2061
- startServer(f, p).catch(e=>{console.error(e);process.exit(1)})
2118
+
2119
+ // ── Cluster mode: use all CPU cores for maximum throughput ────────
2120
+ // Activated by: ~use cluster OR CLUSTER=true env var
2121
+ const src = require('fs').readFileSync(f,'utf8')
2122
+ const useCluster = src.includes('~use cluster') || process.env.CLUSTER === 'true'
2123
+
2124
+ if (useCluster && require('cluster').isPrimary) {
2125
+ const cluster = require('cluster')
2126
+ const numCPUs = parseInt(process.env.WORKERS || require('os').cpus().length)
2127
+ console.log(`[aiplang] Cluster mode: ${numCPUs} workers (${require('os').cpus()[0].model.trim()})`)
2128
+
2129
+ for (let i = 0; i < numCPUs; i++) cluster.fork()
2130
+
2131
+ cluster.on('exit', (worker, code) => {
2132
+ if (code !== 0) {
2133
+ console.warn(`[aiplang] Worker ${worker.process.pid} died (code ${code}), restarting...`)
2134
+ cluster.fork()
2135
+ }
2136
+ })
2137
+ cluster.on('online', w => console.log(`[aiplang] Worker ${w.process.pid} online`))
2138
+ } else {
2139
+ startServer(f, p).catch(e=>{console.error(e);process.exit(1)})
2140
+ }
2062
2141
  }
2063
2142
 
2064
2143
  // ═══════════════════════════════════════════════════════════════════