@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 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.7",
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 authentication is temporarily unavailable', 'Native resource response must be JSON', 'Native resource response is invalid', 'Native resource request failed', 'Invalid final response status', 'Invalid configured redirect location', 'timeout'])
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, 'Native authentication is temporarily unavailable')
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('&amp;', '&')
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 full = new URL(req.url, 'http://' + (req.headers.host || 'localhost'))
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 (DYN.length && /^\/sitemap(-static|-dynamic(-\d+)?)?\.xml$/.test(url.pathname) && (await serveSitemap(req, res, url))) return
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)) out[k] = decodeURIComponent(part.slice(i + 1).trim())
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