@christianirack/brainllm-runtime 0.6.1 → 0.8.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.
- package/brain-data.js +5 -2
- package/brain-env.js +8 -6
- package/brain-form.js +46 -36
- package/brain-inspect.js +19 -6
- package/brain-island.js +2 -1
- package/brain-lazy.js +2 -0
- package/brain-links.js +37 -0
- package/brain-live.js +21 -5
- package/brain-observe.js +120 -0
- package/brain-router.js +224 -178
- package/brain-schema.js +41 -9
- package/brain-trace.js +269 -0
- package/brain-vitals.js +60 -0
- package/package.json +1 -1
- package/server/llm.mjs +199 -0
- package/server/native-sockets.mjs +13 -10
- package/server/server.mjs +663 -105
- package/shim/dom.js +42 -7
- package/sw/brain-sw.js +17 -9
- package/ui/error-state.brain +1 -1
package/shim/dom.js
CHANGED
|
@@ -34,6 +34,11 @@ export class MouseEvent extends Event {}
|
|
|
34
34
|
|
|
35
35
|
// Minimal MutationObserver: only records for nodes connected to the document, plus takeRecords() (used by brain-inspect).
|
|
36
36
|
const MOS = new Set()
|
|
37
|
+
// A node's position in its parent, from a hint the walk keeps: `_at` may be stale after any mutation, so it is checked.
|
|
38
|
+
const indexIn = (parent, node) => {
|
|
39
|
+
if (parent.childNodes[node._at] === node) return node._at
|
|
40
|
+
return (node._at = parent.childNodes.indexOf(node))
|
|
41
|
+
}
|
|
37
42
|
const rec = (type, target) => {
|
|
38
43
|
if (MOS.size && target.isConnected) for (const o of MOS) o._q.push({ type, target })
|
|
39
44
|
}
|
|
@@ -62,6 +67,7 @@ class Node {
|
|
|
62
67
|
this.ownerDocument = doc
|
|
63
68
|
this.parentNode = null
|
|
64
69
|
this.childNodes = []
|
|
70
|
+
this._at = 0
|
|
65
71
|
this._l = { cap: {}, bub: {} }
|
|
66
72
|
}
|
|
67
73
|
get firstChild() {
|
|
@@ -72,11 +78,19 @@ class Node {
|
|
|
72
78
|
}
|
|
73
79
|
get nextSibling() {
|
|
74
80
|
const p = this.parentNode
|
|
75
|
-
|
|
81
|
+
if (!p) return null
|
|
82
|
+
const i = indexIn(p, this) + 1
|
|
83
|
+
const s = p.childNodes[i]
|
|
84
|
+
if (s) s._at = i
|
|
85
|
+
return s || null
|
|
76
86
|
}
|
|
77
87
|
get previousSibling() {
|
|
78
88
|
const p = this.parentNode
|
|
79
|
-
|
|
89
|
+
if (!p) return null
|
|
90
|
+
const i = indexIn(p, this) - 1
|
|
91
|
+
const s = p.childNodes[i]
|
|
92
|
+
if (s) s._at = i
|
|
93
|
+
return s || null
|
|
80
94
|
}
|
|
81
95
|
get isConnected() {
|
|
82
96
|
let n = this
|
|
@@ -85,14 +99,26 @@ class Node {
|
|
|
85
99
|
}
|
|
86
100
|
insertBefore(n, ref) {
|
|
87
101
|
if (n.nodeType === 11) {
|
|
88
|
-
|
|
102
|
+
const moved = n.childNodes
|
|
103
|
+
if (!moved.length) return n
|
|
104
|
+
const at = ref ? indexIn(this, ref) : this.childNodes.length
|
|
105
|
+
if (at < 0) throw Error('ref is not a child')
|
|
106
|
+
const kids = this.childNodes
|
|
107
|
+
const tail = kids.splice(at)
|
|
108
|
+
for (const c of moved) kids.push(c)
|
|
109
|
+
for (const c of tail) kids.push(c)
|
|
110
|
+
for (const c of moved) {
|
|
111
|
+
c.parentNode = this
|
|
112
|
+
rec('childList', this)
|
|
113
|
+
}
|
|
114
|
+
moved.length = 0
|
|
89
115
|
return n
|
|
90
116
|
}
|
|
91
117
|
if (n.parentNode) {
|
|
92
118
|
if (n.isConnected) stats.moves++
|
|
93
119
|
n.parentNode.removeChild(n)
|
|
94
120
|
}
|
|
95
|
-
const i = ref ? this
|
|
121
|
+
const i = ref ? indexIn(this, ref) : -1
|
|
96
122
|
if (ref && i < 0) throw Error('ref is not a child')
|
|
97
123
|
i < 0 ? this.childNodes.push(n) : this.childNodes.splice(i, 0, n)
|
|
98
124
|
n.parentNode = this
|
|
@@ -103,7 +129,7 @@ class Node {
|
|
|
103
129
|
return this.insertBefore(n, null)
|
|
104
130
|
}
|
|
105
131
|
removeChild(n) {
|
|
106
|
-
const i = this
|
|
132
|
+
const i = indexIn(this, n)
|
|
107
133
|
if (i < 0) throw Error('not a child')
|
|
108
134
|
this.childNodes.splice(i, 1)
|
|
109
135
|
n.parentNode = null
|
|
@@ -156,12 +182,20 @@ class Node {
|
|
|
156
182
|
if (!e.defaultPrevented && e.type === 'click' && this.tagName === 'INPUT' && this.type === 'checkbox') this.checked = !this.checked
|
|
157
183
|
return !e.defaultPrevented
|
|
158
184
|
}
|
|
185
|
+
closest(sel) {
|
|
186
|
+
const tests = sel.split(',').map(x => compound(x.trim()))
|
|
187
|
+
for (let n = this; n && n.getAttribute; n = n.parentNode) if (tests.some(t => t(n))) return n
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
getBoundingClientRect() {
|
|
191
|
+
return { top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0, x: 0, y: 0 }
|
|
192
|
+
}
|
|
159
193
|
contains(n) {
|
|
160
194
|
for (; n; n = n.parentNode) if (n === this) return true
|
|
161
195
|
return false
|
|
162
196
|
}
|
|
163
197
|
// Supported selectors: lists (`a, b`), descendant (`a b`), compounds of `tag`, `.class`, `#id`, `[attr]`,
|
|
164
|
-
// `[attr=v]` (quoted or not) and `:not(<compound>)`. Enough for the runtime and the ui kit.
|
|
198
|
+
// `[attr=v]` (quoted or not), `*` and `:not(<compound>)`. Enough for the runtime and the ui kit.
|
|
165
199
|
querySelectorAll(sel) {
|
|
166
200
|
const alts = sel.split(',').map(a => a.trim().split(/\s+/).map(compound))
|
|
167
201
|
const out = []
|
|
@@ -409,12 +443,13 @@ class Document extends Node {
|
|
|
409
443
|
// Selector helpers for querySelectorAll: a compound is a list of tests on one element.
|
|
410
444
|
function compound(str) {
|
|
411
445
|
const tests = []
|
|
412
|
-
const rx = /^(
|
|
446
|
+
const rx = /^(?:\*|([\w-]+)|\.([\w-]+)|#([\w-]+)|\[([\w-]+)(?:=["']?([^"'\]]*)["']?)?\]|:not\(([^)]*)\))/
|
|
413
447
|
let rest = str
|
|
414
448
|
while (rest) {
|
|
415
449
|
const m = rx.exec(rest)
|
|
416
450
|
if (!m) throw Error('unsupported selector: ' + str)
|
|
417
451
|
rest = rest.slice(m[0].length)
|
|
452
|
+
if (m[0] === '*') continue
|
|
418
453
|
if (m[1]) tests.push(e => e.tagName === m[1].toUpperCase())
|
|
419
454
|
else if (m[2]) tests.push(e => (e.getAttribute('class') || '').split(/\s+/).includes(m[2]))
|
|
420
455
|
else if (m[3]) tests.push(e => e.getAttribute('id') === m[3])
|
package/sw/brain-sw.js
CHANGED
|
@@ -6,18 +6,21 @@
|
|
|
6
6
|
// files (§35.7). A new worker takes over at once and keeps only the previous build's cache, and only for the chunks an
|
|
7
7
|
// open tab still asks for: navigations are answered from the current build alone, so nothing old is shown by mistake.
|
|
8
8
|
// After taking over it tells every tab `brain:build <sig>` (the router then does full loads). Caches that are not per
|
|
9
|
-
// build are named `brain:*`. (`sw: false` is a separate kill worker emitted by the build.)
|
|
9
|
+
// build are named `brain:*`. (`sw: false` is a separate kill worker emitted by the build.) An app under a base path
|
|
10
|
+
// prefixes every name with it (`brain/app-<sig>`, `brain/app:meta`): apps sharing an origin never touch each other's
|
|
11
|
+
// caches, and an app at the root keeps the historical names.
|
|
10
12
|
// `cache.routes` (brain.json) adds declarative data routes: { match, prefix, strategy: cache-first | network-first |
|
|
11
13
|
// stale-while-revalidate | network-only, maxEntries, maxAge (s), timeout (ms) }, each in its own `brain:route:<match>`
|
|
12
14
|
// cache that survives builds, bounded by entries and age; personal responses are never stored. `cache.update:
|
|
13
15
|
// "prompt"` keeps a new worker waiting (while an older one controls tabs) until a page posts `brain:skip-waiting`.
|
|
14
16
|
const B = self.BRAIN_BUILD || { hash: 'dev', precache: [], offline: null }
|
|
15
17
|
const BASE = B.base || '/' // app served under a base path (§23 #148); precache/offline URLs already carry it
|
|
16
|
-
const
|
|
18
|
+
const NS = 'brain' + BASE.replace(/\/$/, '') // `brain` at the root, `brain/app` under /app/
|
|
19
|
+
const BUILD = NS + '-' + B.hash, META = NS + ':meta'
|
|
17
20
|
const NAV_TIMEOUT = 3000
|
|
18
21
|
// responses marked personal or uncacheable are never stored
|
|
19
22
|
const storable = r => r.ok && !/no-store|private/i.test(r.headers.get('cache-control') || '')
|
|
20
|
-
const ROUTES = (B.routes || []).map(r => ({ ...r, cache: '
|
|
23
|
+
const ROUTES = (B.routes || []).map(r => ({ ...r, cache: NS + ':route:' + r.match + (r.prefix ? '*' : '') }))
|
|
21
24
|
const route = p => ROUTES.find(r => (r.prefix ? p.startsWith(r.match) : p === r.match))
|
|
22
25
|
// identity-bound: an Authorization header, a response that varies by cookie/authorization/*, or one that sets a cookie
|
|
23
26
|
const personal = (req, r) =>
|
|
@@ -51,7 +54,7 @@ self.addEventListener('activate', e => {
|
|
|
51
54
|
const keep = [...gens.filter(g => g !== BUILD).slice(-1), BUILD]
|
|
52
55
|
await meta.put('/__brain_generations', new Response(JSON.stringify(keep)))
|
|
53
56
|
for (const k of await caches.keys())
|
|
54
|
-
if ((k.startsWith('
|
|
57
|
+
if ((k.startsWith(NS + '-') && !keep.includes(k)) || (k.startsWith(NS + ':route:') && !ROUTES.some(r => r.cache === k))) await caches.delete(k)
|
|
55
58
|
await self.clients.claim()
|
|
56
59
|
for (const c of await self.clients.matchAll({ type: 'window', includeUncontrolled: true })) c.postMessage('brain:build ' + B.hash)
|
|
57
60
|
})()
|
|
@@ -69,7 +72,7 @@ self.addEventListener('fetch', e => {
|
|
|
69
72
|
if (url.origin !== self.location.origin || url.pathname.startsWith(BASE + '_r/') || url.pathname === BASE + 'sw.js') return
|
|
70
73
|
// offline: false (B.offline null) → navigations go straight to the network: no page cache, no offline page
|
|
71
74
|
if (req.mode === 'navigate') return B.offline ? e.respondWith(navigate(e)) : undefined
|
|
72
|
-
if (url.pathname.startsWith(BASE + '_b/')) return e.respondWith(hashed(
|
|
75
|
+
if (url.pathname.startsWith(BASE + '_b/')) return e.respondWith(hashed(e))
|
|
73
76
|
const rt = route(url.pathname)
|
|
74
77
|
if (rt) return e.respondWith(rt.strategy === 'network-only' ? fetch(req) : data(e, rt))
|
|
75
78
|
// only files the page itself loads (img, style, script, font, manifest…); fetch()/XHR data is never touched (§35:
|
|
@@ -77,12 +80,17 @@ self.addEventListener('fetch', e => {
|
|
|
77
80
|
if (req.destination) e.respondWith(revalidate(e))
|
|
78
81
|
})
|
|
79
82
|
|
|
80
|
-
// cache-first: any generation may hold it (an old tab asks for old chunks); misses are stored in the current build
|
|
81
|
-
|
|
83
|
+
// cache-first: any generation may hold it (an old tab asks for old chunks); misses are stored in the current build.
|
|
84
|
+
// Every cache write is handed to waitUntil, so the worker stays alive until it lands; a failed write never fails the response.
|
|
85
|
+
async function hashed(e) {
|
|
86
|
+
const req = e.request
|
|
82
87
|
const hit = await caches.match(req)
|
|
83
88
|
if (hit) return hit
|
|
84
89
|
const r = await fetch(req)
|
|
85
|
-
if (storable(r))
|
|
90
|
+
if (storable(r)) {
|
|
91
|
+
const copy = r.clone()
|
|
92
|
+
e.waitUntil(caches.open(BUILD).then(c => c.put(req, copy)).catch(() => {}))
|
|
93
|
+
}
|
|
86
94
|
return r
|
|
87
95
|
}
|
|
88
96
|
|
|
@@ -114,7 +122,7 @@ async function navigate(e) {
|
|
|
114
122
|
async function revalidate(e) {
|
|
115
123
|
const c = await caches.open(BUILD)
|
|
116
124
|
const hit = await c.match(e.request)
|
|
117
|
-
const net = fetch(e.request).then(r => (storable(r) && c.put(e.request, r.clone()), r))
|
|
125
|
+
const net = fetch(e.request).then(r => (storable(r) && e.waitUntil(c.put(e.request, r.clone()).catch(() => {})), r))
|
|
118
126
|
e.waitUntil(net.catch(() => {}))
|
|
119
127
|
return hit || net
|
|
120
128
|
}
|
package/ui/error-state.brain
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
if (e.status === 401) return say('unauthorized', 'Sign in to continue.')
|
|
26
26
|
if (e.status === 403) return say('forbidden', 'You do not have permission to see this.')
|
|
27
27
|
if (e.status === 404) return say('not_found', 'Not found.')
|
|
28
|
-
if (e.status === 422) return say('invalid', 'Check the highlighted fields.')
|
|
28
|
+
if (e.status === 422) return e.errors && Object.keys(e.errors).length ? say('invalid', 'Check the highlighted fields.') : say('rejected', e.message || 'The server rejected this request. Check it and try again.')
|
|
29
29
|
if (e.status === 429) return say('rate_limited', 'Too many requests. Trying again in {n} seconds.', { n: e.retryAfter || 30 })
|
|
30
30
|
return say('server', 'Something went wrong on our side.')
|
|
31
31
|
}
|