@christianirack/brainllm-runtime 0.8.7 → 0.8.9
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/brain.js +10 -2
- package/package.json +1 -1
- package/server/server.mjs +29 -13
package/brain.js
CHANGED
|
@@ -276,15 +276,23 @@ export function insert(p, f, end) {
|
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
// aria-* and data-* are enumerated, not boolean: true and false render as text (k[4] is their dash)
|
|
279
|
+
// A select only takes a value its options offer, and {#each}/{#if} options mount after attr() or change later.
|
|
280
|
+
const keepSelected = (el, f, M = globalThis.MutationObserver) =>
|
|
281
|
+
M && new M(() => {
|
|
282
|
+
const i = el.selectedIndex
|
|
283
|
+
el.value = typeof f == 'function' ? untrack(f) : f
|
|
284
|
+
if (el.selectedIndex < 0) el.selectedIndex = i // no option matches yet: the browser's choice stays
|
|
285
|
+
}).observe(el, { childList: true, subtree: true })
|
|
286
|
+
|
|
279
287
|
export function attr(el, k, f) {
|
|
280
288
|
bind(
|
|
281
289
|
f,
|
|
282
290
|
k.startsWith('style:')
|
|
283
291
|
? ((k = k.slice(6)), v => (v == null || v === false ? el.style.removeProperty(k) : el.style.setProperty(k, v)))
|
|
284
292
|
: /^(value|checked|selected)$/.test(k) && !(k == 'value' && el.tagName == 'OPTION') // an option's value is its attribute (SSR, value="")
|
|
285
|
-
? v => {
|
|
293
|
+
? ((el.tagName == 'SELECT' && k == 'value') ? keepSelected(el, f) : 0, v => {
|
|
286
294
|
if (el[k] !== v) el[k] = v
|
|
287
|
-
}
|
|
295
|
+
})
|
|
288
296
|
: v => (v == null || (v === false && k[4] != '-') ? el.removeAttribute(k) : el.setAttribute(k, v === true && k[4] != '-' ? '' : /^(href|src|action|formaction|poster)$/.test(k) && typeof v == 'string' ? inBase(v) : v))
|
|
289
297
|
)
|
|
290
298
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@christianirack/brainllm-runtime",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.9",
|
|
4
4
|
"description": "The BRAINLLM runtime for the Rust-built web compiler for LLMs: dependency-free ES modules (signals, DOM, router, data, forms, channels). The brain binary embeds this same version.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/server/server.mjs
CHANGED
|
@@ -193,7 +193,7 @@ const cacheSearch = url => {
|
|
|
193
193
|
return kept.size ? '?' + kept : ''
|
|
194
194
|
}
|
|
195
195
|
// Messages this file writes itself, safe to log as the cause; anything else in an error message may hold private data.
|
|
196
|
-
const OWN_CAUSES = new Set(['Backend request unavailable', 'Backend request timed out', 'Backend response exceeds its byte budget', 'Backend request capacity is unavailable', 'Backend stream capacity is unavailable', 'Native file request failed', 'Native file response is invalid', 'Native
|
|
196
|
+
const OWN_CAUSES = new Set(['Backend request unavailable', 'Backend request timed out', 'Backend response exceeds its byte budget', 'Backend request capacity is unavailable', 'Backend stream capacity is unavailable', 'Native file request failed', 'Native file response is invalid', 'Native resource response must be JSON', 'Native resource response is invalid', 'Native resource request failed', 'Invalid final response status', 'Invalid configured redirect location', 'timeout'])
|
|
197
197
|
const CODE_SHAPE = /^[A-Z][A-Z0-9_]{0,63}$/
|
|
198
198
|
const varyEncoding = v => (/accept-encoding/i.test(v || '') ? v : [v, 'Accept-Encoding'].filter(Boolean).join(', '))
|
|
199
199
|
function bodyEncoding(req, status, headers) {
|
|
@@ -308,9 +308,9 @@ export function nativeAuthBridge(raw, refused, serviceKey, report, ownSession) {
|
|
|
308
308
|
if (!user || typeof user.id !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(user.id) || typeof user.email !== 'string' || user.email.length > 254 || typeof user.role !== 'string' || !/^[a-z][a-z0-9_]{0,47}$/.test(user.role) || ['public','owner'].includes(user.role) || typeof user.verified !== 'boolean') throw new HttpError(503,'Native authentication response is unavailable')
|
|
309
309
|
return Object.freeze({id:user.id,email:user.email,role:user.role,verified:user.verified})
|
|
310
310
|
}
|
|
311
|
-
const unavailable = () => new HttpError(503,
|
|
311
|
+
const unavailable = (why = 'the backend did not answer') => new HttpError(503, `Native authentication is unavailable: ${why}`)
|
|
312
312
|
const call = (incoming, response, path, method, rid, origin) => {
|
|
313
|
-
if (stopped || active >= 8) return Promise.reject(unavailable())
|
|
313
|
+
if (stopped || active >= 8) return Promise.reject(unavailable(stopped ? 'the server is shutting down' : '8 session checks are already in flight'))
|
|
314
314
|
const cookie = cookies(incoming, method === 'POST')
|
|
315
315
|
const client = forwardedFor(incoming)
|
|
316
316
|
active++
|
|
@@ -350,7 +350,7 @@ export function nativeAuthBridge(raw, refused, serviceKey, report, ownSession) {
|
|
|
350
350
|
})
|
|
351
351
|
})
|
|
352
352
|
owned.add(outgoing); incoming.once('aborted', cancel); response.once('close', disconnected)
|
|
353
|
-
timer = setTimeout(() => outgoing.destroy(unavailable()), 2000)
|
|
353
|
+
timer = setTimeout(() => outgoing.destroy(unavailable('the backend took more than 2 s')), 2000)
|
|
354
354
|
outgoing.once('error', () => finish(true)); outgoing.end()
|
|
355
355
|
} catch { outgoing?.destroy(); finish(true) }
|
|
356
356
|
})
|
|
@@ -363,7 +363,7 @@ export function nativeAuthBridge(raw, refused, serviceKey, report, ownSession) {
|
|
|
363
363
|
let origin
|
|
364
364
|
try { origin = new URL(process.env.PUBLIC_SITE_URL).origin } catch { origin = 'https://' + (req.headers['x-forwarded-host'] || req.headers.host) }
|
|
365
365
|
return new Promise((resolve, reject) => {
|
|
366
|
-
if (stopped || active >= 8) return reject(unavailable())
|
|
366
|
+
if (stopped || active >= 8) return reject(unavailable(stopped ? 'the server is shutting down' : '8 session checks are already in flight'))
|
|
367
367
|
active++
|
|
368
368
|
let settled = false
|
|
369
369
|
const done = (error, value) => { if (settled) return; settled = true; active--; clearTimeout(timer); error ? reject(unavailable()) : resolve(value) }
|
|
@@ -388,7 +388,7 @@ export function nativeAuthBridge(raw, refused, serviceKey, report, ownSession) {
|
|
|
388
388
|
} catch { done(true) }
|
|
389
389
|
})
|
|
390
390
|
})
|
|
391
|
-
const timer = setTimeout(() => outgoing.destroy(unavailable()), 2000)
|
|
391
|
+
const timer = setTimeout(() => outgoing.destroy(unavailable('the backend took more than 2 s')), 2000)
|
|
392
392
|
owned.add(outgoing); outgoing.once('close', () => owned.delete(outgoing))
|
|
393
393
|
outgoing.once('error', () => done(true)); outgoing.end()
|
|
394
394
|
})
|
|
@@ -949,7 +949,8 @@ export function createServer(o = {}) {
|
|
|
949
949
|
// log fields for a thrown value: its stack (sanitized by log) plus, for B09xx budget failures, the typed fields only
|
|
950
950
|
const trace = e => {
|
|
951
951
|
const out = { stack: String((e && e.stack) || e) }
|
|
952
|
-
if (e instanceof HttpError && OWN_CAUSES.has(e.message)) out.cause = e.message
|
|
952
|
+
if (e instanceof HttpError && (OWN_CAUSES.has(e.message) || e.message.startsWith('Native authentication is unavailable:'))) out.cause = e.message
|
|
953
|
+
if (e && e.code === 'ERR_MODULE_NOT_FOUND') out.fix = 'this process runs an older build whose files are gone; restart it after brain build'
|
|
953
954
|
if (e && CODE_SHAPE.test(e.transport || '')) out.transport = e.transport
|
|
954
955
|
if (e && typeof e.code === 'string' && CODE_SHAPE.test(e.code)) out.backendCode = e.code
|
|
955
956
|
if (e && e.kind === 'budget' && /^B09\d\d$/.test(e.code) && typeof e.budget === 'string' && /^[a-zA-Z]{1,16}$/.test(e.budget)) {
|
|
@@ -1706,6 +1707,15 @@ export function createServer(o = {}) {
|
|
|
1706
1707
|
const href = tag && /\bhref="(https?:\/\/[^"<>\s]+)"/.exec(tag)?.[1].replaceAll('&', '&')
|
|
1707
1708
|
return href && URL.canParse(href) && !/\/[:*]/.test(new URL(href).pathname) ? { link: `<${href}>; rel="canonical"` } : {}
|
|
1708
1709
|
}
|
|
1710
|
+
// What exists whatever the routes say: generated sitemaps, built files and the icon browsers ask for by habit.
|
|
1711
|
+
async function serveFixed(req, res, url) {
|
|
1712
|
+
if (DYN.length && /^\/sitemap(-static|-dynamic(-\d+)?)?\.xml$/.test(url.pathname) && (await serveSitemap(req, res, url))) return true
|
|
1713
|
+
if (serveStatic(req, res, url)) return true
|
|
1714
|
+
if (url.pathname !== '/favicon.ico' || !file('favicon.svg')) return false
|
|
1715
|
+
send(res, 301, '', { location: withBase('/favicon.svg'), 'cache-control': 'public, max-age=86400' })
|
|
1716
|
+
return true
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1709
1719
|
function serveStatic(req, res, url) {
|
|
1710
1720
|
const p = publicStaticPath(url.pathname)
|
|
1711
1721
|
if (p === null) return false
|
|
@@ -1825,7 +1835,9 @@ export function createServer(o = {}) {
|
|
|
1825
1835
|
const variant = urlVariant(req)
|
|
1826
1836
|
if (variant) return send(res, 301, '', { location: variant, 'cache-control': 'public, max-age=3600' })
|
|
1827
1837
|
if (req.url.startsWith('//')) return notFound(req, res)
|
|
1828
|
-
const
|
|
1838
|
+
const host = req.headers.host || 'localhost'
|
|
1839
|
+
let full
|
|
1840
|
+
try { full = new URL(req.url, 'http://' + host) } catch { return send(res, 400, 'Bad Request') }
|
|
1829
1841
|
// /_health stays at the root: the supervisor calls it there whatever the base
|
|
1830
1842
|
if (full.pathname === '/_health' || full.pathname === BASE + '_health') return send(res, 200, json({ ok: true, build: mf.build || null }), { 'content-type': MIME['.json'], 'cache-control': 'no-store' })
|
|
1831
1843
|
let url = full
|
|
@@ -1865,6 +1877,8 @@ export function createServer(o = {}) {
|
|
|
1865
1877
|
if (!m) continue
|
|
1866
1878
|
// a form page without load is prerendered: GET serves that file (build ETag); only its POST needs the server
|
|
1867
1879
|
if (!post && route.forms && serveStatic(req, res, url)) return
|
|
1880
|
+
// a parameter never hides a real file: `/[code]` must not swallow /favicon.svg, /sitemap.xml or a prerendered /login
|
|
1881
|
+
if (!post && m.groups && (await serveFixed(req, res, url))) return
|
|
1868
1882
|
if (!post && serveTwin(req, res, url)) return
|
|
1869
1883
|
const paged = post ? null : pageParameter(route, url)
|
|
1870
1884
|
if (paged === 404) return notFound(req, res)
|
|
@@ -1872,15 +1886,13 @@ export function createServer(o = {}) {
|
|
|
1872
1886
|
return await (post ? formPost : ssr)(req, res, route, m.groups ? { ...m.groups } : {}, url, rid)
|
|
1873
1887
|
}
|
|
1874
1888
|
if (post) return send(res, 405, 'Method not allowed', { allow: 'GET, HEAD' })
|
|
1875
|
-
if (
|
|
1876
|
-
if (serveStatic(req, res, url)) return
|
|
1877
|
-
// browsers still ask for /favicon.ico: the app's SVG icon answers it
|
|
1878
|
-
if (url.pathname === '/favicon.ico' && file('favicon.svg')) return send(res, 301, '', { location: withBase('/favicon.svg'), 'cache-control': 'public, max-age=86400' })
|
|
1889
|
+
if (await serveFixed(req, res, url)) return
|
|
1879
1890
|
if (isProbe(url.pathname)) return await answerProbe(req, res, url, rid)
|
|
1880
1891
|
notFound(req, res)
|
|
1881
1892
|
} catch (e) {
|
|
1882
1893
|
const busy = e instanceof HttpError && e.code === 'BRIDGE_BUSY'
|
|
1883
1894
|
if (busy) log('warn', 'backend bridge busy', { rid, path: req.$brainPath || String(req.url || '').split('?')[0], fix: e.fix })
|
|
1895
|
+
else if (e instanceof HttpError && OWN_CAUSES.has(e.message)) log('warn', 'backend unavailable', { rid, path: req.$brainPath || String(req.url || '').split('?')[0], status: e.status, cause: e.message, ...(CODE_SHAPE.test(e.transport || '') ? { transport: e.transport } : {}) })
|
|
1884
1896
|
else log('error', 'unhandled', { rid, path: req.url, ...trace(e) })
|
|
1885
1897
|
try {
|
|
1886
1898
|
if (busy && !res.headersSent) { res.setHeader('retry-after', String(e.retryAfter)); failPage(res, 503, rid, { code: e.code, message: 'Service Unavailable', fix: e.fix }) }
|
|
@@ -2247,7 +2259,11 @@ function parseCookies(h = '') {
|
|
|
2247
2259
|
const i = part.indexOf('=')
|
|
2248
2260
|
if (i > 0) {
|
|
2249
2261
|
const k = part.slice(0, i).trim()
|
|
2250
|
-
if (k && !(k in out))
|
|
2262
|
+
if (k && !(k in out)) {
|
|
2263
|
+
// Another site's cookie on this domain may hold anything: it must not cost the page.
|
|
2264
|
+
const value = part.slice(i + 1).trim()
|
|
2265
|
+
try { out[k] = decodeURIComponent(value) } catch { out[k] = value }
|
|
2266
|
+
}
|
|
2251
2267
|
}
|
|
2252
2268
|
}
|
|
2253
2269
|
return out
|