@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/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 = /^(?:([\w-]+)|\.([\w-]+)|#([\w-]+)|\[([\w-]+)(?:=["']?([^"'\]]*)["']?)?\]|:not\(([^)]*)\))/
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 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
  }