@christianirack/brainllm-runtime 0.9.0 → 0.10.0

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.
@@ -0,0 +1,9 @@
1
+ // One opt-in startup watchdog. It cannot outlive this process or target any other process.
2
+ import { workerData, parentPort } from 'node:worker_threads'
3
+ import { writeSync } from 'node:fs'
4
+ const remaining = Math.max(0, Number(BigInt(workerData.deadline) - process.hrtime.bigint()) / 1e6)
5
+ setTimeout(() => {
6
+ try { writeSync(2, '{"event":"startup-warmup-failed","code":"WARMUP_DEADLINE","private":true,"valuesCaptured":false}\n') } catch {}
7
+ process.kill(process.pid, 'SIGKILL')
8
+ }, remaining)
9
+ parentPort.postMessage('armed')
@@ -0,0 +1,134 @@
1
+ // Opt-in anonymous read-only SSR warmup. No background work or outbound request exists when it is absent.
2
+ import { createServer, request } from 'node:http'
3
+ import { Worker } from 'node:worker_threads'
4
+ const MAX_BODY = 256 * 1024, MAX_TOTAL = 2 * 1024 * 1024
5
+ const configError = () => Object.assign(new Error('server.warmup requires up to eight exact public cached SSR paths, a valid site origin, timeoutMs from 100 to 10000, and onError continue or abort'), { code:'WARMUP_CONFIG_INVALID' })
6
+ export function safePath(path) {
7
+ if (typeof path !== 'string' || Buffer.byteLength(path) > 256 || !path.startsWith('/') || /[\\?#\s\u0000-\u0020\u007f-\u009f]/.test(path) || path.includes('//') || /%2f|%5c/i.test(path)) return false
8
+ let decoded
9
+ try { decoded = decodeURIComponent(path) } catch { return false }
10
+ if (/[\\?#\s\u0000-\u0020\u007f-\u009f]/.test(decoded) || decoded.includes('//') || decoded.split('/').some(part => part === '.' || part === '..')) return false
11
+ return new URL(path, 'http://warmup.invalid').pathname === path
12
+ }
13
+ export function warmupSettings(value, routes, base, site, redirects = {}) {
14
+ if (value === undefined) return null
15
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !['paths','timeoutMs','onError'].includes(key))) throw configError()
16
+ const paths = value.paths, timeoutMs = value.timeoutMs ?? 8000, onError = value.onError ?? 'continue'
17
+ if (!Array.isArray(paths) || paths.length < 1 || paths.length > 8 || new Set(paths).size !== paths.length || !Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 10000 || !['continue','abort'].includes(onError)) throw configError()
18
+ let origin
19
+ try {
20
+ if (typeof site !== 'string' || /[\s\u0000-\u0020\u007f-\u009f]/.test(site)) throw configError()
21
+ origin = new URL(site)
22
+ if (!['http:','https:'].includes(origin.protocol) || origin.username || origin.password || origin.search || origin.hash || origin.pathname !== '/' || !origin.hostname || origin.hostname.length > 253 || origin.host.length > 260 || /(?:^|\.)(?:example\.(?:com|org|net)|example|invalid|test)$/.test(origin.hostname)) throw configError()
23
+ } catch { throw configError() }
24
+ for (const path of paths) {
25
+ if (!safePath(path) || base !== '/' && !path.startsWith(base)) throw configError()
26
+ const local = base === '/' ? path : '/' + path.slice(base.length)
27
+ if (/^\/(?:api|_brain|_r|_server|s|_health|health)(?:\/|$)/.test(local)) throw configError()
28
+ const route = routes.find(route => {
29
+ const canonical = redirects.slash === 'always' && route.path !== '/' && !route.path.endsWith('/') && !/\.[^/]*$/.test(route.path) ? route.path + '/' : route.path
30
+ return canonical === local
31
+ })
32
+ if (typeof route?.server !== 'string' || !route.server || !Number.isSafeInteger(route.cache) || route.cache <= 0 || /[:*\[\]]/.test(route.path) || route.forms || route.guards?.length || route.middleware?.length) throw configError()
33
+ }
34
+ return Object.freeze({paths:Object.freeze([...paths]), timeoutMs, onError, host:origin.host})
35
+ }
36
+ const monotonic = () => process.hrtime.bigint()
37
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
38
+
39
+ // Called only by an explicitly warming production instance, before it binds its public listener.
40
+ export async function runWarmup({ server, settings, warm, prepare, log, started = monotonic() }) {
41
+ const deadline = started + BigInt(settings.timeoutMs) * 1000000n
42
+ const reserve = Math.min(1000, Math.max(25, Math.floor(settings.timeoutMs / 8)))
43
+ const requestDeadline = deadline - BigInt(reserve) * 1000000n
44
+ const state = { pending:new Set(), controllers:new Set(), deadline:requestDeadline }
45
+ const watchdog = new Worker(new URL('./warmup-watchdog.mjs', import.meta.url), {workerData:{deadline:String(deadline)},env:{},execArgv:[]})
46
+ const owned = new Set(), peers = new Map(), sockets = new Set(), handlers = new Set()
47
+ let total = 0, failed = 0, stopped = false, timer, privateServer, modules
48
+ const abort = () => {
49
+ stopped = true
50
+ for (const controller of state.controllers) controller.abort()
51
+ for (const outgoing of owned) outgoing.destroy()
52
+ for (const socket of sockets) socket.destroy()
53
+ }
54
+ const track = (promise, set) => {
55
+ set.add(promise)
56
+ promise.then(() => set.delete(promise), () => set.delete(promise))
57
+ return promise
58
+ }
59
+ const secondsLeft = until => Math.max(0, Number(until - monotonic()) / 1e6)
60
+ let failure
61
+ try {
62
+ await new Promise((resolve, reject) => { watchdog.once('message', message => message === 'armed' ? resolve() : reject(configError())); watchdog.once('error', reject); watchdog.once('exit', () => reject(configError())) })
63
+ timer = setTimeout(abort, secondsLeft(requestDeadline))
64
+ const controller = new AbortController()
65
+ state.controllers.add(controller)
66
+ modules = await track(Promise.resolve().then(() => warm(controller.signal)), state.pending)
67
+ if (stopped) throw Object.assign(new Error('Startup warmup deadline'), {code:'WARMUP_TIMEOUT'})
68
+ const dispatch = server.listeners('request')[0]
69
+ privateServer = createServer((incoming, response) => {
70
+ const peer = peers.get(incoming.socket.remotePort)
71
+ if (!peer || peer.path !== incoming.url || incoming.method !== 'GET' || incoming.headers.authorization || incoming.headers.cookie || handlers.size >= 2) { response.writeHead(403,{connection:'close'}); response.end(); return }
72
+ prepare(incoming, peer.controller, state)
73
+ const handling = Promise.resolve().then(() => dispatch.call(server, incoming, response))
74
+ track(handling, handlers).catch(() => { if (!response.headersSent) response.writeHead(500); response.end() })
75
+ })
76
+ privateServer.on('connection', socket => { sockets.add(socket); socket.once('close', () => sockets.delete(socket)) })
77
+ privateServer.maxConnections = 2
78
+ privateServer.maxRequestsPerSocket = 1
79
+ privateServer.maxHeadersCount = 16
80
+ privateServer.headersTimeout = settings.timeoutMs
81
+ privateServer.requestTimeout = settings.timeoutMs
82
+ await new Promise((resolve, reject) => { privateServer.once('error', reject); privateServer.listen(0, '127.0.0.1', resolve) })
83
+ const port = privateServer.address().port
84
+ const one = path => new Promise(resolve => {
85
+ if (stopped) return resolve()
86
+ const controller = new AbortController()
87
+ state.controllers.add(controller)
88
+ let outgoing, settled = false, localPort
89
+ const finish = ok => {
90
+ if (settled) return
91
+ settled = true
92
+ if (!ok) { failed++; controller.abort() }
93
+ owned.delete(outgoing); peers.delete(localPort); state.controllers.delete(controller)
94
+ resolve()
95
+ }
96
+ outgoing = request({host:'127.0.0.1',port,path,method:'GET',agent:false,headers:{host:settings.host,accept:'text/html','accept-encoding':'identity',connection:'close'}}, response => {
97
+ let bytes = 0
98
+ const rejected = response.statusCode !== 200 || response.headers['set-cookie'] || /(?:private|no-store|no-cache)/i.test(response.headers['cache-control'] || '') || response.headers['content-encoding'] || Number(response.headers['content-length']) > MAX_BODY
99
+ if (rejected) { response.destroy(); outgoing.destroy(); return finish(false) }
100
+ response.on('data', chunk => {
101
+ bytes += chunk.length; total += chunk.length
102
+ if (bytes > MAX_BODY || total > MAX_TOTAL) { response.destroy(); outgoing.destroy(); finish(false) }
103
+ })
104
+ response.once('end', () => finish(true))
105
+ response.once('error', () => finish(false))
106
+ response.once('aborted', () => finish(false))
107
+ })
108
+ owned.add(outgoing)
109
+ outgoing.once('socket', socket => socket.once('connect', () => { localPort = socket.localPort; peers.set(localPort, {path,controller}) }))
110
+ outgoing.once('error', () => finish(false))
111
+ outgoing.end()
112
+ })
113
+ let next = 0
114
+ await Promise.all(Array.from({length:Math.min(2,settings.paths.length)}, async () => { while (!stopped && next < settings.paths.length) await one(settings.paths[next++]) }))
115
+ if (stopped) throw Object.assign(new Error('Startup warmup deadline'), {code:'WARMUP_TIMEOUT'})
116
+ if (failed && settings.onError === 'abort') throw Object.assign(new Error('Required startup warmup failed'), {code:'WARMUP_FAILED'})
117
+ } catch (error) { failure = error }
118
+ finally {
119
+ abort()
120
+ if (privateServer) {
121
+ privateServer.closeAllConnections()
122
+ await new Promise(resolve => { if (!privateServer.listening) return resolve(); privateServer.close(resolve) })
123
+ }
124
+ // A load/import that ignores cancellation is not allowed to run behind a ready process.
125
+ // The independent watchdog terminates this owned instance if cleanup cannot finish within the absolute budget.
126
+ while (owned.size || sockets.size || handlers.size || state.pending.size) await delay(1)
127
+ clearTimeout(timer)
128
+ await watchdog.terminate()
129
+ }
130
+ if (monotonic() >= deadline) throw Object.assign(new Error('Startup warmup cleanup exceeded its absolute deadline'),{code:'WARMUP_DEADLINE'})
131
+ log?.(failed || failure ? 'warn' : 'info', 'startup data warmup', {event:'startup-data-warmup',requested:settings.paths.length,failed,bytes:total,code:failure?.code || (failed ? 'WARMUP_PARTIAL' : 'WARMUP_READY'),private:true,valuesCaptured:false})
132
+ if (failure && settings.onError === 'abort') throw failure
133
+ return {...modules,dataPaths:settings.paths.length,dataFailed:failed}
134
+ }