@christianirack/brainllm-runtime 0.8.1 → 0.8.4

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.
@@ -70,13 +70,17 @@ export function start(config = {}, env = globalThis) {
70
70
  )
71
71
 
72
72
  // ── identity: anonymous first-party visitor (localStorage) and a session that ends after 30 idle minutes
73
+ // A session belongs to one visitor (the server refuses it from another), so without storage the visitor id lives in
74
+ // memory for the page, and a stored session whose visitor is gone or different starts over.
73
75
  let session = null
76
+ let visitor = null
74
77
  const ids = () => {
75
78
  const t = Date.now()
76
79
  let vid = get(VISITOR)
77
- if (typeof vid !== 'string') put(VISITOR, (vid = uuid()))
80
+ if (typeof vid === 'string') visitor = vid
81
+ else put(VISITOR, (vid = visitor = visitor || uuid()))
78
82
  if (!session) session = get(SESSION)
79
- if (!session || typeof session.id !== 'string' || t - session.last > IDLE_MS) session = { id: uuid(), last: t, pv: 0, ctx: 0 }
83
+ if (!session || typeof session.id !== 'string' || session.vid !== vid || t - session.last > IDLE_MS) session = { id: uuid(), vid, last: t, pv: 0, ctx: 0 }
80
84
  session.last = t
81
85
  put(SESSION, session)
82
86
  return vid
package/brain-form.js CHANGED
@@ -35,7 +35,10 @@ const go = u => {
35
35
  // submitted does not ask (#299): the user chose to save, and a submit() that navigates once the server accepted the
36
36
  // data (`navigate('/products/' + p.id)`) leaves before submit() returns and marks the values saved.
37
37
  const DIRTY = (globalThis.__brainDirty ||= new Set())
38
- if (globalThis.addEventListener && !globalThis.__brainDirtyHook) {
38
+ // registered with the first edit, not at load: a `beforeunload` listener can keep a page out of the back/forward cache,
39
+ // and most pages that load this file are never edited
40
+ const guardUnload = () => {
41
+ if (!globalThis.addEventListener || globalThis.__brainDirtyHook) return
39
42
  globalThis.__brainDirtyHook = 1
40
43
  globalThis.addEventListener('beforeunload', e => {
41
44
  for (const f of DIRTY) if (f.dirty && !f.busy) return void (e.preventDefault(), (e.returnValue = ''))
@@ -109,7 +112,7 @@ function remoteCheck(o, x, checking) {
109
112
  }
110
113
  function createForm(schema, o, readRule, validateRule, remote) {
111
114
  const id = 'f' + ++seq, owner = I.owner() // aria effects are created later (in handlers) but live with the component
112
- const busy = sig(false), error = sig(''), shown = sig(0) // shown: bumped when touched set changes
115
+ const busy = sig(false), error = sig(''), status = sig(0), shown = sig(0) // shown: bumped when touched set changes
113
116
  const checking = sig(0), saved = sig('') // async checks running · JSON of the last saved values
114
117
  const F = [] // flat fields: { path, r, s (value sig), e (error sig), el, touched }
115
118
  const byPath = {}
@@ -227,6 +230,9 @@ function createForm(schema, o, readRule, validateRule, remote) {
227
230
  get error() {
228
231
  return error.v
229
232
  },
233
+ get status() {
234
+ return status.v
235
+ },
230
236
  get dirty() {
231
237
  return now() !== saved.v
232
238
  },
@@ -274,6 +280,7 @@ function createForm(schema, o, readRule, validateRule, remote) {
274
280
  if (!first && x.e.v) first = x
275
281
  }
276
282
  error.v = ''
283
+ status.v = 0
277
284
  shown.v++
278
285
  })
279
286
  if (o.validate && !first) {
@@ -302,6 +309,7 @@ function createForm(schema, o, readRule, validateRule, remote) {
302
309
  }
303
310
  return r
304
311
  } catch (x) {
312
+ status.v = x?.status || 0
305
313
  if (x?.errors) mark(x.errors)
306
314
  // a server failure reads the same as without JS: 4xx and the author's own errors keep their message; 5xx and a
307
315
  // TypeError (the network dropped, or a bug) get the generic one; the runtime's own English sentence (`plain`) gives
@@ -315,6 +323,9 @@ function createForm(schema, o, readRule, validateRule, remote) {
315
323
  },
316
324
  }
317
325
  DIRTY.add(f)
326
+ // outside the fx: the listener outlives the form on purpose, one per page, and is not this effect's to clean up
327
+ const watch = () => fx(() => void (f.dirty && queueMicrotask(guardUnload)))
328
+ owner ? I.root(watch, owner) : watch()
318
329
  if (owner) I.root(() => fx(() => () => DIRTY.delete(f)), owner)
319
330
  return f
320
331
  }
package/brain-inspect.js CHANGED
@@ -264,6 +264,9 @@ const fin = G.FinalizationRegistry ? new FinalizationRegistry(w => (w.dead = 1))
264
264
  // Where a pending request went, without origin or query: evidence for a fix, never a value.
265
265
  const pendingUrls = () => [...inflight.values()].slice(0, 3).map(r => `${r.m} ${r.url.replace(/^[a-z]+:\/\/[^/]+/i, '').split(/[?#]/)[0]}`)
266
266
  const SETTLE_MS = 60000
267
+ // While a suite holds the verdict, an instance only ages from its release: a promise or a script load that begins after
268
+ // the unmount is invisible to the network watch and keeps the instance alive for a bounded time.
269
+ let held_verdict = false
267
270
  function watch(d) {
268
271
  if (!fin) return
269
272
  // its first DOM node: an app that keeps a detached node (a module array, a cache) is the leak to catch. The browser
@@ -279,11 +282,13 @@ function checkLeaks() {
279
282
  for (let j = watched.length - 1; j >= 0; j--) {
280
283
  const w = watched[j]
281
284
  if (w.dead) watched.splice(j, 1)
285
+ else if (held_verdict) w.at = gcs
282
286
  // A request from before the unmount may be what holds it: judge after it settles (bounded, for a long poll).
283
287
  else if (w.net.some(id => inflight.has(id)) && now() - w.since < SETTLE_MS) w.at = gcs
284
288
  else if (gcs - w.at >= 2) {
285
289
  watched.splice(j, 1)
286
- const waited = w.urls.length ? `${w.urls.length > 2 ? 'several requests' : w.urls.join(', ')} ${w.urls.length > 1 ? 'were' : 'was'} in flight when it unmounted; ` : ''
290
+ const finished = !w.net.some(id => inflight.has(id))
291
+ const waited = w.urls.length ? `${w.urls.length > 2 ? 'several requests' : w.urls.join(', ')} ${w.urls.length > 1 ? 'were' : 'was'} in flight when it unmounted${finished ? ' and finished, so look past it' : ''}; ` : ''
287
292
  leak({ code: 'B0601', h: w.h, name: w.name, file: w.file, msg: `unmounted ${w.name} instance still alive after 2 GC cycles`, fix: w.fx.length ? 'add cleanup to the listed fx: they retain the instance' : `${waited}look for what still references its nodes or signals: an in-flight promise (a pending server call, \`.then\` or \`await\`) or timer that writes to the component, global listeners, caches, module closures`, fx: w.fx })
288
293
  }
289
294
  }
@@ -708,7 +713,7 @@ export function inspect(o) {
708
713
  G.document.addEventListener.call(G.document, 'keydown', e => e.altKey && e.shiftKey && e.code === 'KeyB' && toggle())
709
714
  return api
710
715
  }
711
- const api = { tick, drain, comps, onLoaf, onEvents, leaks: () => leaks.slice(), timers: () => [...timers.values()], toggle: () => toggle(), inflight: () => [...inflight.values()].map(r => `${r.m} ${r.url}`), abortInflight: () => { for (const r of inflight.values()) if (r.stop) r.stop() } }
716
+ const api = { tick, drain, comps, onLoaf, onEvents, leaks: () => leaks.slice(), timers: () => [...timers.values()], toggle: () => toggle(), inflight: () => [...inflight.values()].map(r => `${r.m} ${r.url}`), abortInflight: () => { for (const r of inflight.values()) if (r.stop) r.stop() }, holdVerdict: on => (held_verdict = !!on), alive: () => watched.filter(w => !w.dead).length }
712
717
 
713
718
  // ---------- mini panel (Alt+Shift+B): heap, leaks and link to the full debugger (§9.1) ----------
714
719
  let panel = null
package/brain-lazy.js CHANGED
@@ -75,6 +75,8 @@ export function lazy(el, imp, mode, props = {}) {
75
75
  stop = () => {
76
76
  for (const t of ACT) el.removeEventListener(t, f, t === 'click')
77
77
  }
78
+ } else if (G.document && document.visibilityState === 'hidden') {
79
+ go() // a hidden tab runs no observer: without this the content is missing until the tab is shown
78
80
  } else if (G.IntersectionObserver) {
79
81
  const io = new IntersectionObserver(es => es.some(e => e.isIntersecting) && go(), { rootMargin: '200px' })
80
82
  io.observe(el)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@christianirack/brainllm-runtime",
3
- "version": "0.8.1",
3
+ "version": "0.8.4",
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
@@ -81,6 +81,7 @@ export class HttpError extends Error {
81
81
  Object.assign(this, extra)
82
82
  }
83
83
  }
84
+ const bridgeBusy = message => new HttpError(503, message, { code: 'BRIDGE_BUSY', retryAfter: 1, fix: 'The server is at its limit of concurrent backend requests; retry after the Retry-After delay' })
84
85
  // Plain deliberate 4xx signals are portable from server-only application modules; exception messages stay private.
85
86
  function intendedStatus(error) {
86
87
  if (!error || typeof error !== 'object') return null
@@ -179,6 +180,21 @@ export function acceptedEncodings(header = '', order = ['br', 'zstd', 'gzip']) {
179
180
  // already has an encoding, a `no-transform` cache policy or a status without content is sent as is.
180
181
  const COMPRESSIBLE = /^(?:text\/|image\/svg\+xml|application\/(?:json|javascript|xml|[\w.-]+\+(?:json|xml)))/i
181
182
  const DYNAMIC = ['zstd', 'br', 'gzip']
183
+ // Each Vary token once: a relayed backend header and the additions here must not repeat one (tokens are case-insensitive).
184
+ const varyOnce = v => {
185
+ const seen = new Set()
186
+ return String(v).split(',').map(t => t.trim()).filter(t => t && !seen.has(t.toLowerCase()) && seen.add(t.toLowerCase())).join(', ')
187
+ }
188
+ // Parameters that do not make a different page: the list of `<meta seo query>` (brain-check seo.rs, B0503).
189
+ const TRACKING = /^(?:utm_.*|gclid|fbclid|msclkid)$/
190
+ const cacheSearch = url => {
191
+ const kept = new URLSearchParams([...url.searchParams].filter(([key]) => !TRACKING.test(key)))
192
+ kept.sort()
193
+ return kept.size ? '?' + kept : ''
194
+ }
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'])
197
+ const CODE_SHAPE = /^[A-Z][A-Z0-9_]{0,63}$/
182
198
  const varyEncoding = v => (/accept-encoding/i.test(v || '') ? v : [v, 'Accept-Encoding'].filter(Boolean).join(', '))
183
199
  function bodyEncoding(req, status, headers) {
184
200
  if (status < 200 || status === 204 || status === 206 || status === 304 || headers['content-encoding'] || !COMPRESSIBLE.test(headers['content-type'] || '') || /no-transform/i.test(headers['cache-control'] || '')) return null
@@ -250,7 +266,7 @@ function bucketClient(target, key, bucket) {
250
266
  async remove(id) { await send('DELETE', idPath(id), undefined, 10000); return null },
251
267
  })
252
268
  }
253
- export function nativeAuthBridge(raw, refused, serviceKey, report) {
269
+ export function nativeAuthBridge(raw, refused, serviceKey, report, ownSession) {
254
270
  // How to fix the configuration is for the operator: a visitor of a production app reads a generic message.
255
271
  const configuration = (status, detail, rid, shown = 'This service is not available right now') => {
256
272
  if (process.env.NODE_ENV !== 'production') return new HttpError(status, detail)
@@ -412,7 +428,7 @@ export function nativeAuthBridge(raw, refused, serviceKey, report) {
412
428
  const nativeCookies = values => (values || []).filter(value => /^(?:__Host-brain(?:-access|-refresh|-google)?)=/.test(value) && value.length <= 8192).slice(0,8)
413
429
  // /_brain/files: both directions stream with backpressure. The bucket enforces its own maxBytes and access checks.
414
430
  function fileStream(req, res, url, rid) {
415
- if (stopped || streams >= 32) throw new HttpError(503, 'Backend stream capacity is unavailable')
431
+ if (stopped || streams >= 32) throw bridgeBusy('Backend stream capacity is unavailable')
416
432
  if (!['GET','HEAD','POST','DELETE'].includes(req.method)) throw new HttpError(405, 'Method not allowed')
417
433
  const ceiling = 2 * 1024 * 1024 * 1024 + 1024 * 1024
418
434
  if (Number(req.headers['content-length'] || 0) > ceiling) throw new HttpError(413, 'Request body is too large')
@@ -430,7 +446,7 @@ export function nativeAuthBridge(raw, refused, serviceKey, report) {
430
446
  }
431
447
  const closed = () => finish()
432
448
  const count = chunk => { sent += chunk.length; if (sent > ceiling) finish(new HttpError(413, 'Request body is too large')) }
433
- const unavailable = () => new HttpError(503, 'Backend request unavailable')
449
+ const unavailable = transport => new HttpError(503, 'Backend request unavailable', { transport })
434
450
  try {
435
451
  outgoing = httpRequest(new URL(url.pathname + url.search, target), { agent:streamAgent, method:req.method, headers:requestHeaders(req, rid, FILE_HEADERS) }, backend => {
436
452
  const safe = { 'cache-control':'no-store' }
@@ -440,9 +456,11 @@ export function nativeAuthBridge(raw, refused, serviceKey, report) {
440
456
  }
441
457
  const cookies = nativeCookies(backend.headers['set-cookie'])
442
458
  if (cookies.length) safe['set-cookie'] = cookies
459
+ if (safe.vary) safe.vary = varyOnce(safe.vary)
460
+ if (backend.statusCode >= 500) res.$brainCause = 'answered by the native backend'
443
461
  res.writeHead(backend.statusCode || 502, safe)
444
- backend.once('error', () => finish(unavailable()))
445
- backend.once('aborted', () => finish(unavailable()))
462
+ backend.once('error', e => finish(unavailable(e.code)))
463
+ backend.once('aborted', () => finish(unavailable('BACKEND_ABORTED')))
446
464
  backend.once('end', () => { res.end(() => finish()) })
447
465
  backend.pipe(res)
448
466
  })
@@ -451,7 +469,7 @@ export function nativeAuthBridge(raw, refused, serviceKey, report) {
451
469
  outgoing.setTimeout(60000, () => finish(new HttpError(504, 'Backend request timed out')))
452
470
  // The backend's own upload deadline is 300 s; nothing else needs a total limit after the headers.
453
471
  if (req.method === 'POST') deadline = setTimeout(() => finish(new HttpError(504, 'Backend request timed out')), 310000)
454
- outgoing.once('error', () => finish(unavailable()))
472
+ outgoing.once('error', e => finish(unavailable(e.code)))
455
473
  res.once('close', closed)
456
474
  req.on('data', count)
457
475
  req.pipe(outgoing)
@@ -459,14 +477,14 @@ export function nativeAuthBridge(raw, refused, serviceKey, report) {
459
477
  })
460
478
  }
461
479
  function proxy(req, res, url, rid, reply, responseLimit = 20 * 1024 * 1024, declared) {
462
- if (stopped || active >= 8) throw new HttpError(503, 'Backend request capacity is unavailable')
480
+ if (stopped || active >= 8) throw bridgeBusy('Backend request capacity is unavailable')
463
481
  if (!['GET','HEAD','POST','PUT','PATCH','DELETE','OPTIONS'].includes(req.method)) throw new HttpError(405, 'Method not allowed')
464
482
  const post = req.method === 'POST'
465
483
  const limit = url.pathname === '/_brain/uploads' && post ? 5 * 1024 * 1024 + 65536 : declared || post && /^\/api\/[^/]+\/batch$/.test(url.pathname) ? 1024 * 1024 : 65536
466
484
  if (Number(req.headers['content-length'] || 0) > limit) throw new HttpError(413, 'Request body is too large')
467
485
  active++
468
486
  return new Promise((resolve, reject) => {
469
- let outgoing, timer, done = false, bytes = 0, requestBytes = 0, failure
487
+ let outgoing, timer, done = false, bytes = 0, requestBytes = 0, failure, transport
470
488
  const chunks = []
471
489
  const headers = requestHeaders(req, rid, declared?.headers)
472
490
  const finish = (error, backend) => {
@@ -474,7 +492,7 @@ export function nativeAuthBridge(raw, refused, serviceKey, report) {
474
492
  done = true; clearTimeout(timer); active--; proxyBytes -= bytes; owned.delete(outgoing)
475
493
  req.removeListener('aborted',cancel); req.removeListener('data',data); req.removeListener('end',end)
476
494
  res.removeListener('close',cancel)
477
- if (error) { if (!res.destroyed) reject(failure || new HttpError(503,'Backend request unavailable')); else resolve(); return }
495
+ if (error) { if (!res.destroyed) reject(failure || new HttpError(503,'Backend request unavailable',{transport})); else resolve(); return }
478
496
  const safe = { 'cache-control':'no-store' }
479
497
  for (const key of RESPONSE_HEADERS) {
480
498
  const value = backend.headers[key]
@@ -486,6 +504,7 @@ export function nativeAuthBridge(raw, refused, serviceKey, report) {
486
504
  }
487
505
  const cookies = nativeCookies(backend.headers['set-cookie'])
488
506
  if (cookies.length) safe['set-cookie'] = cookies
507
+ if (backend.statusCode >= 500 && !res.$capture) res.$brainCause = 'answered by the native backend'
489
508
  try { reply(res,backend.statusCode || 502,req.method === 'HEAD' ? '' : Buffer.concat(chunks,bytes),safe); resolve() } catch (e) { reject(e) }
490
509
  }
491
510
  const cancel = () => { outgoing?.destroy(); finish(true) }
@@ -502,11 +521,11 @@ export function nativeAuthBridge(raw, refused, serviceKey, report) {
502
521
  if (bytes > responseLimit || proxyBytes > 64 * 1024 * 1024) { failure = new HttpError(502,'Backend response exceeds its byte budget'); outgoing.destroy(); finish(true); return }
503
522
  chunks.push(chunk)
504
523
  })
505
- backend.once('end',()=>finish(false,backend)); backend.once('error',()=>finish(true)); backend.once('aborted',()=>finish(true))
524
+ backend.once('end',()=>finish(false,backend)); backend.once('error',e=>{transport=e.code;finish(true)}); backend.once('aborted',()=>{transport='BACKEND_ABORTED';finish(true)})
506
525
  })
507
526
  owned.add(outgoing)
508
527
  timer = setTimeout(()=>{failure = new HttpError(504,'Backend request timed out');outgoing.destroy();finish(true)},declared || url.pathname === '/_brain/uploads' || url.pathname.startsWith('/_brain/uploads/') ? 35000 : 10000)
509
- outgoing.once('error',()=>finish(true)); req.once('aborted',cancel); res.once('close',cancel)
528
+ outgoing.once('error',e=>{transport=e.code;finish(true)}); req.once('aborted',cancel); res.once('close',cancel)
510
529
  req.on('data',data); req.once('end',end)
511
530
  if (req.readableEnded) end()
512
531
  } catch { outgoing?.destroy(); finish(true) }
@@ -531,7 +550,7 @@ export function nativeAuthBridge(raw, refused, serviceKey, report) {
531
550
  req.method = method
532
551
  req.$brainForwardedFor = forwardedFor(incoming)
533
552
  // A service call acts as the key's owner, never as the visitor: it sends no session.
534
- req.headers = { ...(serviceKey ? { authorization: 'Bearer ' + serviceKey } : { cookie: cookies(incoming, false) }), accept: 'application/json', 'content-type':'application/json', 'content-length':String(bytes.length) }
553
+ req.headers = { ...(serviceKey ? { authorization: 'Bearer ' + serviceKey } : ownSession ? {} : { cookie: cookies(incoming, false) }), accept: 'application/json', 'content-type':'application/json', 'content-length':String(bytes.length) }
535
554
  if (method !== 'GET') {
536
555
  req.headers.origin = bridgeOrigin(incoming, rid, 'Native writes require a same-origin server request')
537
556
  }
@@ -558,6 +577,15 @@ export function nativeAuthBridge(raw, refused, serviceKey, report) {
558
577
  if (!String(headers['content-type'] || '').startsWith('application/json')) throw new HttpError(502, 'Native resource response must be JSON')
559
578
  try { value = JSON.parse(data.toString('utf8')) } catch { throw new HttpError(502, 'Native resource response is invalid') }
560
579
  }, 2 * 1024 * 1024)
580
+ } catch (e) {
581
+ // A read that failed: `load` may catch it and render anyway, so the page answer says what happened (ssr below).
582
+ if (method === 'GET' && !(e?.status >= 400 && e.status < 500)) {
583
+ const seen = incoming.$brainFailedReads || (incoming.$brainFailedReads = { reads: 0, backendCode: undefined, transport: undefined })
584
+ seen.reads++
585
+ seen.backendCode ??= e?.code
586
+ seen.transport ??= e?.transport
587
+ }
588
+ throw e
561
589
  } finally { req.destroy() }
562
590
  if (response.destroyed || response.writableEnded) throw new HttpError(409, 'The server request has ended')
563
591
  return value
@@ -796,18 +824,22 @@ export function createServer(o = {}) {
796
824
  // ctx.env: what the supervisor passed, read-only; the service key stays with ctx.backend.service
797
825
  const { BRAIN_SERVICE_API_KEY: serviceKey, BRAIN_JOBS_API_KEY: _jobs, BRAIN_JOBS_SYSTEM_KEY: _jobsSystem, BRAIN_ROUTES_API_KEY: _routes, BRAIN_ROUTES_SYSTEM_KEY: _routesSystem, ...visibleEnv } = o.env || ENV
798
826
  const env = Object.freeze(visibleEnv)
799
- const native = nativeAuthBridge(o.backendURL ?? env.BRAIN_BACKEND_URL, (...why) => originRefused(...why), serviceKey, (...entry) => log(...entry))
800
- // Using the native backend does not have to mean it owns the visitor's session: `app` keeps the signed cookie.
801
827
  const backendSession = o.backendSession ?? env.BRAIN_BACKEND_SESSION
802
828
  if (backendSession && backendSession !== 'app') throw Error("BRAIN_BACKEND_SESSION must be app, or unset for the backend's own session")
829
+ const native = nativeAuthBridge(o.backendURL ?? env.BRAIN_BACKEND_URL, (...why) => originRefused(...why), serviceKey, (...entry) => log(...entry), backendSession === 'app')
830
+ // Using the native backend does not have to mean it owns the visitor's session: `app` keeps the signed cookie.
803
831
  const nativeSession = native && backendSession !== 'app'
804
832
  if (o.nativeSockets !== undefined && typeof o.nativeSockets !== 'boolean') throw Error('nativeSockets must be an explicit boolean')
805
833
  const SITEMAP_BYTES = +(o.sitemapBytes ?? 50 * 1024 * 1024) // protocol: 50 MB uncompressed per sitemap file
806
834
  const secret = o.secret ?? env.BRAIN_SECRET ?? ''
807
835
  const COMPRESS = o.compress !== false && env.BRAIN_COMPRESS !== 'off' // a proxy that already compresses can opt out
808
836
  const sink = o.log || ((level, msg, extra) => console[level === 'error' ? 'error' : 'log'](json({ t: new Date().toISOString(), level, msg, ...extra })))
837
+ // Requests in flight by rid: a 5xx whose cause reached no log line gets one when the response closes (dispatch below).
838
+ const liveRequests = new Map()
839
+ let statusPageWindow = 0, statusPageLogs = 0
809
840
  const log = (level, msg, extra = {}) => {
810
841
  const safe = { ...extra }
842
+ if (level !== 'info' && extra.rid) { const live = liveRequests.get(extra.rid); if (live) live.logged = true }
811
843
  // Error messages may contain credentials or request data. Retain only bounded V8 source frames.
812
844
  if (typeof safe.stack === 'string') {
813
845
  const kind = /^(TypeError|ReferenceError|RangeError|SyntaxError|AggregateError|Error):/.exec(safe.stack)?.[1] || 'Error'
@@ -843,6 +875,9 @@ export function createServer(o = {}) {
843
875
  // log fields for a thrown value: its stack (sanitized by log) plus, for B09xx budget failures, the typed fields only
844
876
  const trace = e => {
845
877
  const out = { stack: String((e && e.stack) || e) }
878
+ if (e instanceof HttpError && OWN_CAUSES.has(e.message)) out.cause = e.message
879
+ if (e && CODE_SHAPE.test(e.transport || '')) out.transport = e.transport
880
+ if (e && typeof e.code === 'string' && CODE_SHAPE.test(e.code)) out.backendCode = e.code
846
881
  if (e && e.kind === 'budget' && /^B09\d\d$/.test(e.code) && typeof e.budget === 'string' && /^[a-zA-Z]{1,16}$/.test(e.budget)) {
847
882
  const str = (v, n) => typeof v === 'string' ? v.slice(0, n) : null, pos = v => Number.isSafeInteger(v) && v > 0 ? v : null
848
883
  out.budgetFailure = { code: e.code, budget: e.budget, limit: Number.isFinite(e.limit) ? e.limit : null, file: str(e.file, 256), component: str(e.component, 80), line: pos(e.line), column: pos(e.column), fix: str(e.fix, 200) || 'Bound the work or move it to an owned worker.' }
@@ -936,7 +971,7 @@ export function createServer(o = {}) {
936
971
  res.req.$nativeLogout.then(() => send(res, status, body, headers, configuredExternal), () => send(res, 503, '', {}, false))
937
972
  return
938
973
  }
939
- if (native && res.req.$nativeLogoutError) { status = 503; body = ''; headers = {}; res.req.$nativeLogoutError = false }
974
+ if (native && res.req.$nativeLogoutError) { status = 503; body = ''; headers = {}; res.req.$nativeLogoutError = false; res.$brainCause = 'native sign-out failed' }
940
975
  if (native && res.req.$nativeLogoutHeaders) {
941
976
  const relay = res.req.$nativeLogoutHeaders, own = headers['set-cookie']
942
977
  headers = { ...headers, ...relay, 'set-cookie': [...(own ? Array.isArray(own) ? own : [own] : []), ...relay['set-cookie']] }
@@ -974,6 +1009,7 @@ export function createServer(o = {}) {
974
1009
  delete headers['content-length']
975
1010
  }
976
1011
  }
1012
+ if (headers.vary) headers = { ...headers, vary: varyOnce(headers.vary) }
977
1013
  res.writeHead(status, { ...SECURITY, ...(server.draining ? { connection: 'close' } : {}), ...headers })
978
1014
  res.end(res.req.method === 'HEAD' || status === 204 || status === 205 || status === 304 ? undefined : body)
979
1015
  }
@@ -995,8 +1031,8 @@ export function createServer(o = {}) {
995
1031
  const notFound = (req, res) => failPage(res, 404, res.getHeader('x-request-id'))
996
1032
  // `gone()` from load (§33): 410 tells crawlers the URL was removed for good; views/410.brain, else the 404 page
997
1033
  const missing = (req, res, x) => (x && x.gone === true ? failPage(res, 410, res.getHeader('x-request-id')) : notFound(req, res))
998
- const failPage = (res, status, rid) => {
999
- if (!wantsHtml(res.req)) return send(res, status, json({ schemaVersion: 1, status, rid, error: STATUS_CODES[status] || 'Request failed' }), { 'content-type': MIME['.json'] })
1034
+ const failPage = (res, status, rid, detail) => {
1035
+ if (!wantsHtml(res.req)) return send(res, status, json({ schemaVersion: 1, status, rid, error: detail || STATUS_CODES[status] || 'Request failed' }), { 'content-type': MIME['.json'] })
1000
1036
  const custom = (status === 404 && mf.notFound) || (mf.errors && mf.errors[status]) || (status === 410 && !file('410.html') ? mf.notFound : `${status}.html`)
1001
1037
  const path = typeof custom === 'string' && file(custom)
1002
1038
  return html(res, status, path ? readFileSync(path, 'utf8') : defaultError(status, rid, BASE))
@@ -1049,16 +1085,20 @@ export function createServer(o = {}) {
1049
1085
  const SSRC = new Map(), SSRC_MAX = 500, SSRC_BYTES = 32 * 1024 * 1024, SSRC_ENTRY = 1024 * 1024
1050
1086
  let ssrBytes = 0
1051
1087
  async function cached(req, res, route, params, url, rid) {
1052
- const n = +route.cache, ms = n * 1000, key = (route.locale || '') + url.pathname + url.search
1053
- const fill = () =>
1054
- new Promise(ok => {
1088
+ const n = +route.cache, ms = n * 1000, key = (route.locale || '') + url.pathname + cacheSearch(url)
1089
+ const fill = () => {
1090
+ let failed = false
1091
+ return new Promise(ok => {
1055
1092
  const cap = { req, $capture: (out, h, st) => {
1056
1093
  const headers = Object.fromEntries(Object.entries(h).map(([k, v]) => [k.toLowerCase(), v]))
1057
1094
  const privateResponse = headers['set-cookie'] || headers['vary'] || /private|no-store|no-cache/i.test(headers['cache-control'] || '')
1095
+ if (st >= 500) failed = true
1058
1096
  ok(st === 200 ? { out, h, cacheable: !privateResponse, etag: 'W/"' + tag(out) + '"', t: Date.now(), bytes: Buffer.byteLength(out) } : null)
1059
- }, writeHead() {}, end: () => ok(null), setHeader() {}, getHeader() {} }
1097
+ }, writeHead(st) { if (st >= 500) failed = true }, end: () => ok(null), setHeader() {}, getHeader: name => (name === 'x-request-id' ? rid : undefined), once() {}, removeListener() {} }
1060
1098
  ssr(req, cap, route, params, url, rid, null, 200, null, true).catch(() => ok(null))
1061
1099
  }).then(c => {
1100
+ // A failed revalidation keeps the copy until 2N, which is what `stale-if-error` promises downstream.
1101
+ if ((!c || !c.cacheable) && (failed || req.$brainFailedReads) && SSRC.has(key)) return c
1062
1102
  if (!c || !c.cacheable) {
1063
1103
  ssrBytes -= SSRC.get(key)?.bytes || 0
1064
1104
  SSRC.delete(key)
@@ -1076,7 +1116,8 @@ export function createServer(o = {}) {
1076
1116
  }
1077
1117
  return c
1078
1118
  })
1079
- const serve = c => html(res, 200, c.out, { ...c.h, 'cache-control': c.cacheable ? `public, s-maxage=${n}, stale-while-revalidate=${n}` : 'no-store' }, c.etag)
1119
+ }
1120
+ const serve = c => html(res, 200, c.out, { ...c.h, 'cache-control': c.cacheable ? `public, s-maxage=${n}, stale-while-revalidate=${n}, stale-if-error=${n}` : 'no-store' }, c.etag)
1080
1121
  let c = SSRC.get(key)
1081
1122
  const age = c ? Date.now() - c.t : Infinity
1082
1123
  if (c && age < 2 * ms) {
@@ -1129,8 +1170,17 @@ export function createServer(o = {}) {
1129
1170
  const loadOutcome = e => {
1130
1171
  if (e && e.redirect) return send(res, e.status || 303, '', { location: withBase(linkTo(route, redirect(e.redirect, e.status ?? 303).redirect)), ...ctx.session.headers() })
1131
1172
  if ((e instanceof HttpError && e.status === 404) || (e && e.notFound === true)) return missing(req, res, e) // `notFound()` 404 / `gone()` 410
1173
+ // the backend or another service said "not now": a temporary 503 tells crawlers to come back, a 500 tells them it broke
1174
+ // a status the page chose itself (`new HttpError(429)`) stays as it is, with the same hint to come back
1175
+ if (e && (e.status === 429 || e.status === 503) && !(e instanceof HttpError) && !res.headersSent) {
1176
+ log('warn', 'load temporarily unavailable', { rid, path: url.pathname, status: 503, ...trace(e) })
1177
+ res.setHeader('retry-after', String(Math.min(Math.ceil(e.retryAfter) || 5, 3600)))
1178
+ res.setHeader('cache-control', 'no-store')
1179
+ return errorPage(req, res, route, params, url, rid, 503)
1180
+ }
1132
1181
  const status = intendedStatus(e) || 500
1133
1182
  log('error', 'load failed', { rid, path: url.pathname, status, ...trace(e) })
1183
+ if ((status === 429 || status === 503) && !res.headersSent) res.setHeader('retry-after', String(Math.min(Math.ceil(e.retryAfter) || 5, 3600)))
1134
1184
  return status === 500 ? errorPage(req, res, route, params, url, rid) : failPage(res, status, rid)
1135
1185
  }
1136
1186
  if (route.stream && route.server && !fixed && !forms && status === 200 && !res.$capture) return streamed(req, res, route, params, url, rid, ctx, keep(extra || {}), loadOutcome)
@@ -1144,6 +1194,10 @@ export function createServer(o = {}) {
1144
1194
  return loadOutcome(e)
1145
1195
  }
1146
1196
  if (props && (props.redirect || props.notFound === true)) return loadOutcome(props) // `return redirect()` / `return notFound()`
1197
+ if (req.$brainFailedReads && !fixed) {
1198
+ softFailure(req, rid, url, status)
1199
+ extra = { ...extra, 'cache-control': 'no-store' } // what it rendered instead is not worth keeping
1200
+ }
1147
1201
  let body, seo, failed
1148
1202
  try {
1149
1203
  const at = { path: url.pathname, params: params || {}, query: Object.fromEntries(url.searchParams) } // route() (§41.5)
@@ -1172,6 +1226,11 @@ export function createServer(o = {}) {
1172
1226
  // (head, CSS, layout shell) is sent, the view when load() ends. The status and headers are out by then, so a late
1173
1227
  // redirect becomes location.replace and a late failure an inline notice with the request id (both logged); load()
1174
1228
  // cannot change the session on a streamed route.
1229
+ // `load` caught a failed backend read and rendered: the status it asked for stands, the log says what is behind it.
1230
+ const softFailure = (req, rid, url, status) => {
1231
+ const { reads, backendCode, transport } = req.$brainFailedReads
1232
+ log('warn', 'page rendered after a failed backend read', { rid, path: url.pathname, status, reads, backendCode, transport, fix: 'load caught the failure and returned other props; let it throw for a 503, or show that the data is missing' })
1233
+ }
1175
1234
  async function streamed(req, res, route, params, url, rid, ctx, extra, outcome) {
1176
1235
  const [top, bottom = ''] = readFileSync(file(route.page), 'utf8').split('<!--brain:ssr-->')
1177
1236
  const sm = await load(route.server)
@@ -1241,6 +1300,7 @@ export function createServer(o = {}) {
1241
1300
  log('error', 'load failed (streamed)', { rid, path: url.pathname, status: 200, ...trace(e) }) // headers already out
1242
1301
  return fail()
1243
1302
  }
1303
+ if (req.$brainFailedReads) softFailure(req, rid, url, 200)
1244
1304
  try {
1245
1305
  const { body, failed } = await (queue = queue.then(() => render(route, props, null, at), () => render(route, props, null, at)))
1246
1306
  for (const f of failed || []) log('error', 'component failed during SSR', { rid, path: url.pathname, comp: f.comp, where: f.w, ...trace(f.e) })
@@ -1272,9 +1332,9 @@ export function createServer(o = {}) {
1272
1332
  return failPage(res, 403, rid)
1273
1333
  }
1274
1334
  // a failing load or guard → the folder's _error.brain (route `error`, props { message, rid }), else the generic page
1275
- function errorPage(req, res, route, params, url, rid) {
1276
- if (wantsHtml(req) && route.error) return ssr(req, res, route.error, params, url, rid, null, 500, { message: 'Something went wrong.', rid })
1277
- return failPage(res, 500, rid)
1335
+ function errorPage(req, res, route, params, url, rid, status = 500) {
1336
+ if (wantsHtml(req) && route.error) return ssr(req, res, route.error, params, url, rid, null, status, { message: 'Something went wrong.', rid })
1337
+ return failPage(res, status, rid)
1278
1338
  }
1279
1339
  async function render(route, props, forms, at = null) {
1280
1340
  const { install } = await load('_server/dom.js')
@@ -1518,9 +1578,19 @@ export function createServer(o = {}) {
1518
1578
  if (path === null) return false
1519
1579
  const md = file(mdPath(path))
1520
1580
  if (!md) return false
1521
- send(res, 200, req.method === 'HEAD' ? '' : readFileSync(md), { 'content-type': MIME['.md'], 'cache-control': 'public, max-age=3600', vary: 'accept' })
1581
+ send(res, 200, req.method === 'HEAD' ? '' : readFileSync(md), { 'content-type': MIME['.md'], 'cache-control': 'public, max-age=3600', vary: 'accept', ...twinCanonical(path) })
1522
1582
  return true
1523
1583
  }
1584
+ // A Markdown copy repeats its page: the header names the canonical the page itself declares. A page without one (no
1585
+ // site configured) or with a per-request pattern gets no header, so this never invents a URL.
1586
+ function twinCanonical(path) {
1587
+ const stem = path.replace(/\.md$/, '').replace(/\/+$/, '')
1588
+ const page = file(stem ? stem + '.html' : 'index.html') || file(stem + '/index.html')
1589
+ if (!page) return {}
1590
+ const tag = /<link\b[^>]*\brel=["']canonical["'][^>]*>/i.exec(readFileSync(page, 'utf8'))?.[0]
1591
+ const href = tag && /\bhref="(https?:\/\/[^"<>\s]+)"/.exec(tag)?.[1].replaceAll('&amp;', '&')
1592
+ return href && URL.canParse(href) && !/\/[:*]/.test(new URL(href).pathname) ? { link: `<${href}>; rel="canonical"` } : {}
1593
+ }
1524
1594
  function serveStatic(req, res, url) {
1525
1595
  const p = publicStaticPath(url.pathname)
1526
1596
  if (p === null) return false
@@ -1538,6 +1608,7 @@ export function createServer(o = {}) {
1538
1608
  return html(res, 200, page, md ? { vary: 'accept', 'x-brain-static': '1' } : { 'x-brain-static': '1' }, '"' + (mf.build || tag(page)) + '"'), true
1539
1609
  }
1540
1610
  const headers = {
1611
+ ...(ext === '.md' ? twinCanonical(p) : {}),
1541
1612
  'content-type': type,
1542
1613
  // the worker script must be revalidated on every check, or a CDN could keep an old worker for an hour (§35.7)
1543
1614
  'cache-control': p.startsWith('/_b/') ? 'public, max-age=31536000, immutable' : p === '/sw.js' ? 'no-cache' : 'public, max-age=3600',
@@ -1568,7 +1639,43 @@ export function createServer(o = {}) {
1568
1639
  if (req.method !== 'GET' && req.method !== 'HEAD') return notFound(req, res)
1569
1640
  if (!native) return send(res, 200, json({ ok: true }), headers)
1570
1641
  try { await native.proxy(req, res, new URL(url.pathname, url), rid, send) }
1571
- catch { if (!res.headersSent) send(res, 503, json({ ok: false }), headers) }
1642
+ catch (e) {
1643
+ if (!res.headersSent) {
1644
+ log('warn', 'health probe could not reach the native backend', { rid, path: url.pathname, status: 503, ...trace(e) })
1645
+ send(res, 503, json({ ok: false }), headers)
1646
+ }
1647
+ }
1648
+ }
1649
+
1650
+ const urlVariant = req => {
1651
+ if (req.method !== 'GET' && req.method !== 'HEAD') return null
1652
+ const q = req.url.indexOf('?'), raw = q < 0 ? req.url : req.url.slice(0, q)
1653
+ if (!raw.includes('//') && !raw.endsWith('/index.html')) return null
1654
+ let path = raw.replace(/\/{2,}/g, '/')
1655
+ const app = '/' + (path.startsWith(BASE) ? path.slice(BASE.length) : path.slice(1))
1656
+ if (/^\/(?:api|_brain|_r|s)(?:\/|$)/.test(app) && !raw.startsWith('//')) return null
1657
+ if (path.endsWith('/index.html')) path = path.slice(0, -'index.html'.length)
1658
+ if (path !== BASE && path !== '/' && (path.endsWith('/') && SLASH !== 'always')) path = path.replace(/\/+$/, '')
1659
+ const location = path + (q < 0 ? '' : req.url.slice(q))
1660
+ try { safeLocation(location) } catch { return null }
1661
+ return location
1662
+ }
1663
+ // `<meta seo query="page">`: `page` is a number from 1 and page 1 is the URL without it. A view decides past its last
1664
+ // page with notFound(). → null (nothing to do) · 404 · the URL to redirect to.
1665
+ const pagedRoutes = new Map()
1666
+ const pageParameter = (route, url) => {
1667
+ if (!url.searchParams.has('page') || !route.page) return null
1668
+ if (!pagedRoutes.has(route.page)) {
1669
+ const keys = /<meta name="brain-query" content="([^"]*)">/.exec(readFileSync(file(route.page), 'utf8'))?.[1]
1670
+ pagedRoutes.set(route.page, !!keys && keys.split(/[\s,]+/).includes('page'))
1671
+ }
1672
+ if (!pagedRoutes.get(route.page)) return null
1673
+ const values = url.searchParams.getAll('page')
1674
+ if (values.length > 1 || !/^[1-9]\d{0,8}$/.test(values[0])) return 404
1675
+ if (values[0] !== '1') return null
1676
+ const rest = new URLSearchParams(url.search)
1677
+ rest.delete('page')
1678
+ return url.pathname + (rest.size ? '?' + rest : '')
1572
1679
  }
1573
1680
 
1574
1681
  // ---------- dispatch ----------
@@ -1585,13 +1692,23 @@ export function createServer(o = {}) {
1585
1692
  }
1586
1693
  const rid = req.headers['x-request-id'] && /^[\w-]{1,64}$/.test(req.headers['x-request-id']) ? req.headers['x-request-id'] : randomUUID()
1587
1694
  res.setHeader('x-request-id', rid)
1695
+ const live = { logged: false }
1696
+ liveRequests.set(rid, live)
1697
+ res.once('close', () => {
1698
+ if (liveRequests.get(rid) === live) liveRequests.delete(rid)
1699
+ // The net under every path: a 5xx that was answered always leaves a line with its request id.
1700
+ if (res.statusCode >= 500 && !live.logged && !res.$brainMuted) log(res.$brainCause === STATUS_PAGE ? 'info' : 'error', 'server error answered', { rid, path: req.$brainPath || String(req.url || '').split('?')[0], status: res.statusCode, cause: res.$brainCause || 'no cause recorded on this path', ...(res.$brainCause === STATUS_PAGE ? {} : { fix: 'Search this rid in the native backend log; its failed requests carry the same id' }) })
1701
+ })
1588
1702
  const t0 = performance.now()
1589
1703
  res.on('finish', () => {
1590
1704
  const ms = performance.now() - t0
1591
1705
  if (ms > 1000) log('warn', 'slow request', { rid, path: req.$brainPath || String(req.url || '').split('?')[0], status: res.statusCode, ms: Math.round(ms) })
1592
1706
  })
1593
1707
  try {
1594
- // `//` makes new URL throw and `//host/x` would be read as another host
1708
+ // One URL per page: `//route`, `/a//b` and `/route/index.html` answer one 301 on this origin. The path is rewritten
1709
+ // as text, before URL parsing: `//host/x` would be read as another host. Backend paths are left to the backend.
1710
+ const variant = urlVariant(req)
1711
+ if (variant) return send(res, 301, '', { location: variant, 'cache-control': 'public, max-age=3600' })
1595
1712
  if (req.url.startsWith('//')) return notFound(req, res)
1596
1713
  const full = new URL(req.url, 'http://' + (req.headers.host || 'localhost'))
1597
1714
  // /_health stays at the root: the supervisor calls it there whatever the base
@@ -1620,13 +1737,23 @@ export function createServer(o = {}) {
1620
1737
  // a status page opened directly answers its own status: `/403` is never a 200 that a crawler or a probe can trust
1621
1738
  const reserved = post ? 0 : reservedStatus(url.pathname)
1622
1739
  if (reserved === 403) return forbidden(req, res, url, rid, '')
1623
- if (reserved) return failPage(res, reserved, rid)
1740
+ if (reserved) {
1741
+ // Anyone can open it, so the note about it is bounded and is not an error.
1742
+ const minute = Math.floor(Date.now() / 60000)
1743
+ if (statusPageWindow !== minute) { statusPageWindow = minute; statusPageLogs = 0 }
1744
+ res.$brainCause = STATUS_PAGE
1745
+ res.$brainMuted = statusPageLogs++ >= 16
1746
+ return failPage(res, reserved, rid)
1747
+ }
1624
1748
  for (const route of routes) {
1625
1749
  const m = route.rx.exec(url.pathname)
1626
1750
  if (!m) continue
1627
1751
  // a form page without load is prerendered: GET serves that file (build ETag); only its POST needs the server
1628
1752
  if (!post && route.forms && serveStatic(req, res, url)) return
1629
1753
  if (!post && serveTwin(req, res, url)) return
1754
+ const paged = post ? null : pageParameter(route, url)
1755
+ if (paged === 404) return notFound(req, res)
1756
+ if (paged) return send(res, 301, '', { location: withBase(paged), 'cache-control': 'public, max-age=3600' })
1630
1757
  return await (post ? formPost : ssr)(req, res, route, m.groups ? { ...m.groups } : {}, url, rid)
1631
1758
  }
1632
1759
  if (post) return send(res, 405, 'Method not allowed', { allow: 'GET, HEAD' })
@@ -1637,9 +1764,19 @@ export function createServer(o = {}) {
1637
1764
  if (isProbe(url.pathname)) return await answerProbe(req, res, url, rid)
1638
1765
  notFound(req, res)
1639
1766
  } catch (e) {
1640
- log('error', 'unhandled', { rid, path: req.url, ...trace(e) })
1641
- if (!res.headersSent) failPage(res, intendedStatus(e) || 500, rid)
1642
- else res.end()
1767
+ const busy = e instanceof HttpError && e.code === 'BRIDGE_BUSY'
1768
+ if (busy) log('warn', 'backend bridge busy', { rid, path: req.$brainPath || String(req.url || '').split('?')[0], fix: e.fix })
1769
+ else log('error', 'unhandled', { rid, path: req.url, ...trace(e) })
1770
+ try {
1771
+ if (busy && !res.headersSent) { res.setHeader('retry-after', String(e.retryAfter)); failPage(res, 503, rid, { code: e.code, message: 'Service Unavailable', fix: e.fix }) }
1772
+ else if (!res.headersSent) failPage(res, intendedStatus(e) || 500, rid)
1773
+ else res.end()
1774
+ } catch (again) {
1775
+ // the error page itself failed: still an answer, never an unhandled rejection that takes the process down
1776
+ log('error', 'error page failed', { rid, path: req.url, ...trace(again) })
1777
+ if (!res.headersSent) res.writeHead(500, SECURITY)
1778
+ res.end()
1779
+ }
1643
1780
  }
1644
1781
  })
1645
1782
  if (native) server.once('close', () => native.close())
@@ -1651,6 +1788,7 @@ export function createServer(o = {}) {
1651
1788
  }
1652
1789
 
1653
1790
  // ---------- session (signed cookie; shared with the dev server) ----------
1791
+ const STATUS_PAGE = 'status page opened directly'
1654
1792
  const COOKIE = '__Host-session', SECRET_COOKIE = '__Host-session-secret', SECRET_MAX = 3500, DEFAULT_SESSION_SECONDS = 7 * 24 * 3600
1655
1793
  // Keys that still verify and decrypt during a rotation; new cookies always use the current key.
1656
1794
  const previousKeys = () => (process.env.BRAIN_SECRET_PREVIOUS || '').split(',').map(k => k.trim()).filter(Boolean)
@@ -2193,6 +2331,8 @@ function real(p) {
2193
2331
  }
2194
2332
  if (process.argv[1] && real(process.argv[1]) === real(fileURLToPath(import.meta.url))) {
2195
2333
  process.env.NODE_ENV = 'production'
2334
+ // A closed log pipe is the operator's matter: requests do not depend on it.
2335
+ for (const stream of [process.stdout, process.stderr]) stream.on('error', error => { if (error.code !== 'EPIPE') throw error })
2196
2336
  const s = createServer({ dist: fileURLToPath(new URL('.', import.meta.url)) })
2197
2337
  const fd = ENV.BRAIN_LISTEN_FD
2198
2338
  const where = fd ? { fd: Number(fd) } : { port: +(ENV.PORT || 3000), host: ENV.HOST || '127.0.0.1' }