@christianirack/brainllm-runtime 0.8.4 → 0.8.5
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-inspect.js +5 -4
- package/package.json +1 -1
- package/server/server.mjs +45 -21
package/brain-inspect.js
CHANGED
|
@@ -214,13 +214,14 @@ function leak(r) {
|
|
|
214
214
|
leaks.push(r)
|
|
215
215
|
push(r)
|
|
216
216
|
}
|
|
217
|
-
function b602(n, what, line) {
|
|
217
|
+
function b602(n, what, line, foreign) {
|
|
218
218
|
if (!n || flagged.has(n) || n.s >= 3) return
|
|
219
219
|
flagged.add(n)
|
|
220
220
|
const i = instOf(n), name = i ? nameOf(i) : '?'
|
|
221
221
|
const fx = `fx in ${name} (${what})${line ? ' ' + line : ''}`
|
|
222
222
|
if (i) (b602ByInst.get(i.r) || b602ByInst.set(i.r, []).get(i.r)).push(fx)
|
|
223
|
-
|
|
223
|
+
// a listener another module registered while this fx ran is not the component's: name where it really comes from
|
|
224
|
+
leak({ code: 'B0602', h: i ? i.h : null, name, file: foreign && line ? line.replace(/:\d+$/, '') : i && i.C.f, line, msg: `global ${what} inside an fx${foreign && line ? ` of ${name}, registered by ${line},` : ''} without cleanup`, fix: 'return a cleanup function from the fx: `fx(() => { …; return () => remove() })`', fx: [fx] })
|
|
224
225
|
}
|
|
225
226
|
function patchGlobalListeners() {
|
|
226
227
|
for (const t of [G.window, G.document].filter(Boolean)) {
|
|
@@ -229,8 +230,8 @@ function patchGlobalListeners() {
|
|
|
229
230
|
t.addEventListener = function (type, ...a) {
|
|
230
231
|
const c = ctx()
|
|
231
232
|
if (c && c.o === 'fx') {
|
|
232
|
-
const line = site()
|
|
233
|
-
O.st.call(G, () => !c.n.k && b602(c.n, `addEventListener('${type}')`, line), 0)
|
|
233
|
+
const { line, foreign } = site()
|
|
234
|
+
O.st.call(G, () => !c.n.k && b602(c.n, `addEventListener('${type}')`, line, foreign), 0)
|
|
234
235
|
}
|
|
235
236
|
return add.call(this, type, ...a)
|
|
236
237
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@christianirack/brainllm-runtime",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
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
|
@@ -546,17 +546,20 @@ export function nativeAuthBridge(raw, refused, serviceKey, report, ownSession) {
|
|
|
546
546
|
try { bytes = Buffer.from(JSON.stringify(body)) } catch { throw new HttpError(400, 'Native resource values must be JSON serializable') }
|
|
547
547
|
if (bytes.length > 65536) throw new HttpError(413, 'Native resource values exceed their byte budget')
|
|
548
548
|
}
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
549
|
+
const outgoing = () => {
|
|
550
|
+
const req = Readable.from(bytes.length ? [bytes] : [])
|
|
551
|
+
req.method = method
|
|
552
|
+
req.$brainForwardedFor = forwardedFor(incoming)
|
|
553
|
+
// A service call acts as the key's owner, never as the visitor: it sends no session.
|
|
554
|
+
req.headers = { ...(serviceKey ? { authorization: 'Bearer ' + serviceKey } : ownSession ? {} : { cookie: cookies(incoming, false) }), accept: 'application/json', 'content-type':'application/json', 'content-length':String(bytes.length) }
|
|
555
|
+
if (method !== 'GET') {
|
|
556
|
+
req.headers.origin = bridgeOrigin(incoming, rid, 'Native writes require a same-origin server request')
|
|
557
|
+
}
|
|
558
|
+
return req
|
|
556
559
|
}
|
|
560
|
+
let req = outgoing()
|
|
557
561
|
let value
|
|
558
|
-
|
|
559
|
-
await proxy(req, response, new URL(path, target), rid, (_res, status, data, headers) => {
|
|
562
|
+
const answered = (_res, status, data, headers) => {
|
|
560
563
|
if (status === 422) {
|
|
561
564
|
// the backend's own field messages (never driver text): a server function that rethrows answers 422 {errors}
|
|
562
565
|
let fields
|
|
@@ -576,7 +579,20 @@ export function nativeAuthBridge(raw, refused, serviceKey, report, ownSession) {
|
|
|
576
579
|
if (status === 204) { value = null; return }
|
|
577
580
|
if (!String(headers['content-type'] || '').startsWith('application/json')) throw new HttpError(502, 'Native resource response must be JSON')
|
|
578
581
|
try { value = JSON.parse(data.toString('utf8')) } catch { throw new HttpError(502, 'Native resource response is invalid') }
|
|
579
|
-
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
// A restarting backend refuses connections for a few seconds: a read waits for it instead of failing the page.
|
|
585
|
+
for (let wait = 250; ; wait *= 2) {
|
|
586
|
+
try {
|
|
587
|
+
await proxy(req, response, new URL(path, target), rid, answered, 2 * 1024 * 1024)
|
|
588
|
+
break
|
|
589
|
+
} catch (e) {
|
|
590
|
+
if (method !== 'GET' || wait > 2000 || stopped || response.destroyed || !['ECONNREFUSED', 'ECONNRESET'].includes(e?.transport)) throw e
|
|
591
|
+
req.destroy()
|
|
592
|
+
await new Promise(done => setTimeout(done, wait))
|
|
593
|
+
req = outgoing()
|
|
594
|
+
}
|
|
595
|
+
}
|
|
580
596
|
} catch (e) {
|
|
581
597
|
// A read that failed: `load` may catch it and render anyway, so the page answer says what happened (ssr below).
|
|
582
598
|
if (method === 'GET' && !(e?.status >= 400 && e.status < 500)) {
|
|
@@ -1352,6 +1368,7 @@ export function createServer(o = {}) {
|
|
|
1352
1368
|
}
|
|
1353
1369
|
if (route.i18n) await load(route.i18n) // the route's dictionary chunk: add(locale, {…}) before anything renders
|
|
1354
1370
|
globalThis.__brainForms = forms // form() reads it while the view is built (renders are serialized)
|
|
1371
|
+
globalThis.__brainIds = (globalThis.__brainIds || 0) + 1 // each page numbers its instance ids from the start, as the client will
|
|
1355
1372
|
globalThis.__brainLocale = route.locale || null // t(), fmt and locale() of this page (§37)
|
|
1356
1373
|
globalThis.__brainAlternates = route.alternates || null // <LangSwitch> links, in the HTML before any JS
|
|
1357
1374
|
globalThis.__brainRoute = at // route(): app path, params, query of this request
|
|
@@ -1468,8 +1485,8 @@ export function createServer(o = {}) {
|
|
|
1468
1485
|
// pages, plus an optional `lastmod`, at request time. /sitemap.xml is the build's file with those URLs added (a sitemap
|
|
1469
1486
|
// index gets one more child, /sitemap-dynamic.xml). A file holds at most 50 000 URLs (sitemaps.org): when the build's
|
|
1470
1487
|
// URLs plus the dynamic ones pass that, /sitemap.xml becomes an index of /sitemap-static.xml (the build's file) and
|
|
1471
|
-
// /sitemap-dynamic.xml; dynamic URLs past 50 000 are dropped with a warning. Absolute URLs take the origin
|
|
1472
|
-
// build's sitemap
|
|
1488
|
+
// /sitemap-dynamic.xml; dynamic URLs past 50 000 are dropped with a warning. Absolute URLs take the site origin from
|
|
1489
|
+
// the manifest (an older manifest: from the build's sitemap); a build without a sitemap serves none. The list is kept 5 min (one sitemap() run
|
|
1473
1490
|
// at a time when it expires) and the response says max-age=300; a sitemap() that throws is logged and skipped.
|
|
1474
1491
|
const DYN = routes.filter(r => r.sitemap && r.server)
|
|
1475
1492
|
let dynList = null
|
|
@@ -1485,14 +1502,23 @@ export function createServer(o = {}) {
|
|
|
1485
1502
|
const day = v => (v instanceof Date ? v.toISOString().slice(0, 10) : typeof v === 'string' && /^\d{4}-\d{2}-\d{2}/.test(v) ? v : null)
|
|
1486
1503
|
const MAX_URLS = 50000
|
|
1487
1504
|
let dynRun = null
|
|
1505
|
+
const dynGood = new Map()
|
|
1488
1506
|
async function collect(origin) {
|
|
1489
1507
|
const urls = []
|
|
1490
1508
|
let dropped = 0
|
|
1491
|
-
|
|
1509
|
+
// Views answer at the same time, so one slow view costs one timeout; a view that fails keeps its last good list.
|
|
1510
|
+
const lists = await Promise.all(DYN.map(async r => {
|
|
1492
1511
|
try {
|
|
1493
1512
|
const sm = await load(r.server)
|
|
1494
1513
|
const list = typeof sm.sitemap === 'function' ? await withTimeout(Promise.resolve().then(() => sm.sitemap({ env })), TIMEOUT) : []
|
|
1495
|
-
|
|
1514
|
+
dynGood.set(r, Array.isArray(list) ? list : [])
|
|
1515
|
+
} catch (e) {
|
|
1516
|
+
log('error', 'sitemap() failed', { view: r.view, ...trace(e) })
|
|
1517
|
+
}
|
|
1518
|
+
return dynGood.get(r) || []
|
|
1519
|
+
}))
|
|
1520
|
+
DYN.forEach((r, n) => {
|
|
1521
|
+
for (const p of lists[n]) {
|
|
1496
1522
|
const path = fill(r.path, p)
|
|
1497
1523
|
if (!path) continue
|
|
1498
1524
|
const loc = origin + withBase(path)
|
|
@@ -1500,19 +1526,17 @@ export function createServer(o = {}) {
|
|
|
1500
1526
|
else if (urls.length < MAX_URLS) urls.push({ loc, lastmod: day(p.lastmod) })
|
|
1501
1527
|
else dropped++
|
|
1502
1528
|
}
|
|
1503
|
-
|
|
1504
|
-
log('error', 'sitemap() failed', { view: r.view, ...trace(e) })
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1529
|
+
})
|
|
1507
1530
|
if (dropped) log('warn', 'sitemap(): more than 50000 URLs, the rest are not listed', { dropped })
|
|
1531
|
+
if (!urls.length) log('warn', 'sitemap(): no view returned a URL, so /sitemap.xml lists only the pages of the build', { views: DYN.map(r => r.view) })
|
|
1508
1532
|
dynList = { at: Date.now(), urls }
|
|
1509
1533
|
return urls
|
|
1510
1534
|
}
|
|
1511
1535
|
async function dynamicUrls(origin) {
|
|
1512
1536
|
if (dynList && Date.now() - dynList.at < 300000) return dynList.urls
|
|
1513
|
-
// concurrent requests after expiry share one run of every sitemap()
|
|
1537
|
+
// concurrent requests after expiry share one run of every sitemap(); a crawler never waits while a list exists
|
|
1514
1538
|
if (!dynRun) dynRun = collect(origin).finally(() => (dynRun = null))
|
|
1515
|
-
return dynRun
|
|
1539
|
+
return dynList ? dynList.urls : dynRun
|
|
1516
1540
|
}
|
|
1517
1541
|
// §33 S01: every sitemap file holds at most 50 000 URLs and SITEMAP_BYTES bytes of UTF-8 XML; the dynamic URLs fill
|
|
1518
1542
|
// /sitemap-dynamic.xml, /sitemap-dynamic-2.xml, … in sitemap() order, so the split is deterministic
|
|
@@ -1535,7 +1559,7 @@ export function createServer(o = {}) {
|
|
|
1535
1559
|
const f = file('/sitemap.xml')
|
|
1536
1560
|
if (!f) return false
|
|
1537
1561
|
const text = readFileSync(f, 'utf8')
|
|
1538
|
-
const origin = [...text.matchAll(/<loc>(https?:\/\/[^<]+)<\/loc>/g)].map(m => URL.canParse(m[1]) && new URL(m[1]).origin).find(Boolean)
|
|
1562
|
+
const origin = mf.site || [...text.matchAll(/<loc>(https?:\/\/[^<]+)<\/loc>/g)].map(m => URL.canParse(m[1]) && new URL(m[1]).origin).find(Boolean)
|
|
1539
1563
|
if (!origin) return false
|
|
1540
1564
|
const index = text.includes('<sitemapindex')
|
|
1541
1565
|
const sets = pages(await dynamicUrls(origin))
|