@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/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
- return p ? p.childNodes[p.childNodes.indexOf(this) + 1] || null : null
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
- return p ? p.childNodes[p.childNodes.indexOf(this) - 1] || null : null
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
- for (const c of [...n.childNodes]) this.insertBefore(c, ref)
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.childNodes.indexOf(ref) : -1
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.childNodes.indexOf(n)
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 = /^(?:([\w-]+)|\.([\w-]+)|#([\w-]+)|\[([\w-]+)(?:=["']?([^"'\]]*)["']?)?\]|:not\(([^)]*)\))/
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 BUILD = 'brain-' + B.hash, META = 'brain:meta'
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: 'brain:route:' + r.match + (r.prefix ? '*' : '') }))
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('brain-') && !keep.includes(k)) || (k.startsWith('brain:route:') && !ROUTES.some(r => r.cache === k))) await caches.delete(k)
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(req))
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
- async function hashed(req) {
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)) (await caches.open(BUILD)).put(req, r.clone())
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
  }
@@ -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
  }