@christianirack/brainllm-runtime 0.6.1 → 0.7.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 +4 -2
- package/brain-env.js +8 -6
- package/brain-form.js +37 -29
- 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-router.js +223 -178
- package/brain-trace.js +269 -0
- package/package.json +1 -1
- package/server/native-sockets.mjs +13 -10
- package/server/server.mjs +593 -86
- package/shim/dom.js +11 -2
- package/sw/brain-sw.js +17 -9
- package/ui/error-state.brain +1 -1
package/shim/dom.js
CHANGED
|
@@ -156,12 +156,20 @@ class Node {
|
|
|
156
156
|
if (!e.defaultPrevented && e.type === 'click' && this.tagName === 'INPUT' && this.type === 'checkbox') this.checked = !this.checked
|
|
157
157
|
return !e.defaultPrevented
|
|
158
158
|
}
|
|
159
|
+
closest(sel) {
|
|
160
|
+
const tests = sel.split(',').map(x => compound(x.trim()))
|
|
161
|
+
for (let n = this; n && n.getAttribute; n = n.parentNode) if (tests.some(t => t(n))) return n
|
|
162
|
+
return null
|
|
163
|
+
}
|
|
164
|
+
getBoundingClientRect() {
|
|
165
|
+
return { top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0, x: 0, y: 0 }
|
|
166
|
+
}
|
|
159
167
|
contains(n) {
|
|
160
168
|
for (; n; n = n.parentNode) if (n === this) return true
|
|
161
169
|
return false
|
|
162
170
|
}
|
|
163
171
|
// 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.
|
|
172
|
+
// `[attr=v]` (quoted or not), `*` and `:not(<compound>)`. Enough for the runtime and the ui kit.
|
|
165
173
|
querySelectorAll(sel) {
|
|
166
174
|
const alts = sel.split(',').map(a => a.trim().split(/\s+/).map(compound))
|
|
167
175
|
const out = []
|
|
@@ -409,12 +417,13 @@ class Document extends Node {
|
|
|
409
417
|
// Selector helpers for querySelectorAll: a compound is a list of tests on one element.
|
|
410
418
|
function compound(str) {
|
|
411
419
|
const tests = []
|
|
412
|
-
const rx = /^(
|
|
420
|
+
const rx = /^(?:\*|([\w-]+)|\.([\w-]+)|#([\w-]+)|\[([\w-]+)(?:=["']?([^"'\]]*)["']?)?\]|:not\(([^)]*)\))/
|
|
413
421
|
let rest = str
|
|
414
422
|
while (rest) {
|
|
415
423
|
const m = rx.exec(rest)
|
|
416
424
|
if (!m) throw Error('unsupported selector: ' + str)
|
|
417
425
|
rest = rest.slice(m[0].length)
|
|
426
|
+
if (m[0] === '*') continue
|
|
418
427
|
if (m[1]) tests.push(e => e.tagName === m[1].toUpperCase())
|
|
419
428
|
else if (m[2]) tests.push(e => (e.getAttribute('class') || '').split(/\s+/).includes(m[2]))
|
|
420
429
|
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
|
}
|