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 +1 -1
- package/package.json +1 -1
- package/server/server.js +95 -16
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.
|
|
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
package/server/server.js
CHANGED
|
@@ -98,13 +98,29 @@ async function dbRunAsync(sql, params = []) {
|
|
|
98
98
|
dbRun(sql, params)
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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() {
|
|
1333
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
// ═══════════════════════════════════════════════════════════════════
|