@christianirack/brainllm-runtime 0.0.0-stage → 0.1.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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -2
  3. package/brain-anim.js +21 -0
  4. package/brain-ch.js +254 -0
  5. package/brain-consent.js +38 -0
  6. package/brain-data.js +547 -0
  7. package/brain-dev.js +100 -0
  8. package/brain-env.js +160 -0
  9. package/brain-err.js +242 -0
  10. package/brain-form.js +260 -0
  11. package/brain-guard.js +242 -0
  12. package/brain-hydrate.js +72 -0
  13. package/brain-i18n.js +111 -0
  14. package/brain-inspect.js +745 -0
  15. package/brain-island.js +106 -0
  16. package/brain-lazy.js +88 -0
  17. package/brain-mcp.js +58 -0
  18. package/brain-mock.js +80 -0
  19. package/brain-overlay.js +123 -0
  20. package/brain-persist.js +65 -0
  21. package/brain-router.js +353 -0
  22. package/brain-rpc.js +17 -0
  23. package/brain-schema.js +69 -0
  24. package/brain-wc.js +69 -0
  25. package/brain-why.js +198 -0
  26. package/brain-worker.js +66 -0
  27. package/brain.js +421 -0
  28. package/package.json +20 -4
  29. package/server/server.mjs +882 -0
  30. package/shim/dom.js +448 -0
  31. package/shim/probe.js +111 -0
  32. package/sw/brain-sw.js +103 -0
  33. package/themes/contrast.cssbrain +39 -0
  34. package/themes/dark.cssbrain +39 -0
  35. package/themes/glass.cssbrain +39 -0
  36. package/themes/paper.cssbrain +39 -0
  37. package/ui/button.brain +31 -0
  38. package/ui/center.brain +11 -0
  39. package/ui/checkbox.brain +13 -0
  40. package/ui/container.brain +11 -0
  41. package/ui/dialog.brain +29 -0
  42. package/ui/error-state.brain +53 -0
  43. package/ui/field.brain +34 -0
  44. package/ui/grid.brain +11 -0
  45. package/ui/input.brain +23 -0
  46. package/ui/lang-switch.brain +38 -0
  47. package/ui/menu.brain +64 -0
  48. package/ui/row.brain +12 -0
  49. package/ui/select.brain +29 -0
  50. package/ui/sidebar.brain +17 -0
  51. package/ui/spacer.brain +12 -0
  52. package/ui/stack.brain +10 -0
  53. package/ui/table.brain +73 -0
  54. package/ui/tabs.brain +33 -0
  55. package/ui/toast.brain +45 -0
@@ -0,0 +1,745 @@
1
+ // BRAINLLM inspector (SPEC §9 and §9.1). Dev only; absent from builds. Emits data, does not draw the debugger.
2
+ // Usage (brain-serve HMR client): inspect({ send: s => ws.send(s), inspectPort }) → every 100 ms, if there is data,
3
+ // sends `{"t":"m","v":2,"seq":n,"at":ms,"d":[records…]}`. Record format: see FORMAT at the end.
4
+ // Does not touch the core: uses the brain-dev.js registry and patches the signal prototype in dev (`set v`, `r`).
5
+ import { live, inst, bus, owner } from './brain-dev.js'
6
+ import { capture, sub } from './brain-err.js'
7
+ import { seen } from './brain-ch.js'
8
+
9
+ const G = globalThis
10
+ const RT = new URL('.', import.meta.url).href // runtime frames: do not count as an app line
11
+ const O = {
12
+ si: G.setInterval, ci: G.clearInterval, st: G.setTimeout, ct: G.clearTimeout,
13
+ raf: G.requestAnimationFrame, caf: G.cancelAnimationFrame, fetch: G.fetch,
14
+ }
15
+ const now = () => Date.now()
16
+ const pnow = () => (G.performance ? performance.now() : now())
17
+ const syncHeap = () => (G.performance && performance.memory ? performance.memory.usedJSHeapSize : G.process && process.memoryUsage ? process.memoryUsage().heapUsed : null)
18
+
19
+ let out = [], seq = 0, opts = {}, started = false
20
+ const push = r => out.push(r)
21
+
22
+ // ---------- instance identity and current owner ----------
23
+ const ids = new WeakMap()
24
+ let nextId = 1
25
+ const idOf = i => ids.get(i) || (ids.set(i, nextId++), ids.get(i))
26
+ // the component's own name: under guard() (every compiled component) i.C is its wrapper `W`, the component is i.C.C
27
+ const nameOf = i => (i.C && ((i.C.C && i.C.C.name) || i.C.name)) || i.h
28
+ function instOf(n) {
29
+ for (; n; n = n.p) {
30
+ const i = inst.get(n)
31
+ if (i) return i
32
+ }
33
+ return null
34
+ }
35
+ let handler = null // { i, o:'on:click' } while a delegated handler runs
36
+ // Current owner: component + origin (fx · setup · on:<event>). load runs on the server: brain-serve attributes it.
37
+ function ctx() {
38
+ const n = owner()
39
+ if (n && n.e) {
40
+ const i = instOf(n)
41
+ if (i) return { i, o: 'fx', n }
42
+ }
43
+ if (handler) return handler
44
+ const i = n && instOf(n)
45
+ return i ? { i, o: 'setup', n } : null
46
+ }
47
+ const own = c => (c ? { id: idOf(c.i), h: c.i.h, name: nameOf(c.i), o: c.o } : null)
48
+ // First frame outside the runtime: "url:line:col". No app frame → foreign (extension, third party, browser).
49
+ function site() {
50
+ const lim = Error.stackTraceLimit
51
+ Error.stackTraceLimit = 50 // with 10, the runtime stack hides the app frame
52
+ const st = String(new Error().stack || '').split('\n')
53
+ Error.stackTraceLimit = lim
54
+ for (const l of st) {
55
+ const m = l.match(/((?:https?|file|chrome-extension|moz-extension):\/\/[^\s)]+):(\d+):(\d+)/)
56
+ if (!m || (m[1].startsWith(RT) && !m[1].slice(RT.length).includes('/'))) continue // only root-level brain*.js
57
+ const app = G.location && /^https?:/.test(G.location.href) ? m[1].startsWith(G.location.origin) && !m[1].includes('/@brain/') : m[1].startsWith('file:')
58
+ return { line: m[1].replace(G.location ? G.location.origin : '', '') + ':' + m[2], foreign: !app }
59
+ }
60
+ return { line: null, foreign: true }
61
+ }
62
+ // Safe preview of a value (depth and length limits).
63
+ function prev(v, d = 0) {
64
+ if (v == null || typeof v === 'number' || typeof v === 'boolean') return v
65
+ if (typeof v === 'string') return v.length > 120 ? v.slice(0, 117) + '…' : v
66
+ if (typeof v === 'function') return 'ƒ'
67
+ if (v.nodeType) return '<' + (v.nodeName || 'node').toLowerCase() + '>'
68
+ if (d > 1) return Array.isArray(v) ? `[… ${v.length}]` : '{…}'
69
+ if (Array.isArray(v)) return v.slice(0, 10).map(x => prev(x, d + 1)).concat(v.length > 10 ? [`… +${v.length - 10}`] : [])
70
+ const o = {}
71
+ let k = 0
72
+ for (const key in v) if (k++ < 12) o[key] = prev(v[key], d + 1)
73
+ return o
74
+ }
75
+
76
+ // ---------- signals: last writer, count and update latency ----------
77
+ const writes = new WeakMap() // N → { w, c }
78
+ let upd = null // update in progress: { at, t0, sig, w, fx, ms }
79
+ let mo = null, touched = 0
80
+ function patchSignals(sample) {
81
+ const P = Object.getPrototypeOf(sample)
82
+ if (P.$bi) return
83
+ P.$bi = 1
84
+ const d = Object.getOwnPropertyDescriptor(P, 'v')
85
+ Object.defineProperty(P, 'v', {
86
+ configurable: true,
87
+ get: d.get,
88
+ set(x) {
89
+ const before = this._
90
+ d.set.call(this, x)
91
+ if (this._ === before) return
92
+ const c = ctx(), s = site(), w = c ? `${nameOf(c.i)} · ${c.o}` : s.line || '?'
93
+ const r = writes.get(this) || (writes.set(this, { w, c: 0 }), writes.get(this))
94
+ r.w = w
95
+ r.c++
96
+ if (!upd) {
97
+ upd = { at: now(), t0: pnow(), sig: sigName(this), w, fx: 0, ms: 0 }
98
+ if (mo) mo.takeRecords()
99
+ touched = 0
100
+ queueMicrotask(closeUpd) // queued after the core flush
101
+ }
102
+ },
103
+ })
104
+ const run = P.r
105
+ P.r = function () {
106
+ if (!this.e) return run.call(this)
107
+ const t = pnow()
108
+ try {
109
+ return run.call(this)
110
+ } finally {
111
+ if (upd) upd.fx++
112
+ const i = instOf(this)
113
+ if (i) (i.$fx = (i.$fx || 0) + 1), (i.$ms = (i.$ms || 0) + pnow() - t)
114
+ }
115
+ }
116
+ }
117
+ function closeUpd() {
118
+ if (!upd) return
119
+ const u = upd
120
+ upd = null
121
+ if (mo) touched += mo.takeRecords().length
122
+ push({ k: 'upd', at: u.at, sig: u.sig, w: u.w, fx: u.fx, nodes: touched, ms: +(pnow() - u.t0).toFixed(3) })
123
+ }
124
+ function sigName(n) {
125
+ for (const set of live.values()) for (const i of set) if (i.m) for (const [k, v] of i.m) if (v === n) return `${nameOf(i)}.${k}`
126
+ return 'sig'
127
+ }
128
+
129
+ // ---------- delegated handlers: capture on document, close on window ----------
130
+ function patchHandlers() {
131
+ const D = G.document, W = G.window
132
+ if (!D || !W || D.$bi) return
133
+ D.$bi = 1
134
+ let t0 = 0
135
+ const close = t => {
136
+ if (handler) push({ k: 'handler', at: now(), id: idOf(handler.i), h: handler.i.h, name: nameOf(handler.i), type: t, ms: +(pnow() - t0).toFixed(3) })
137
+ handler = null
138
+ }
139
+ const types = ['click', 'input', 'change', 'submit', 'keydown', 'keyup', 'pointerdown', 'pointerup', 'focusin', 'focusout']
140
+ for (const t of types) {
141
+ D.addEventListener.call(D, t, e => {
142
+ let n = e.composedPath ? e.composedPath()[0] : e.target
143
+ for (; n && !n.$i; n = n.parentNode || n.host);
144
+ handler = n ? { i: n.$i, o: 'on:' + t } : null
145
+ t0 = pnow()
146
+ if (handler) O.st.call(G, () => close(t), 0) // fallback if a handler stops propagation
147
+ }, true)
148
+ W.addEventListener.call(W, t, () => close(t))
149
+ }
150
+ }
151
+
152
+ // ---------- timers ----------
153
+ const timers = new Map() // id → active public record
154
+ const tmeta = new Map() // id → { h: handle, n: owning fx } (internal: never serialized)
155
+ let tid = 1
156
+ function timer(kind, ms) {
157
+ const c = ctx(), s = site(), id = tid++
158
+ const r = { k: 'timer', id, kind, ms: ms || 0, own: own(c), line: s.line, foreign: s.foreign, cleanup: null, st: 'on' }
159
+ timers.set(id, r)
160
+ tmeta.set(id, { h: null, n: c && c.o === 'fx' ? c.n : null })
161
+ push(r)
162
+ return r
163
+ }
164
+ function patchTimers() {
165
+ const mk = (name, kind, orig, once) =>
166
+ function (cb, ms, ...a) {
167
+ const r = timer(kind, kind === 'raf' ? 16 : ms)
168
+ const h = orig.call(G, typeof cb === 'function' && once ? (...x) => (end(r), cb(...x)) : cb, ms, ...a)
169
+ tmeta.get(r.id).h = h
170
+ byHandle.set(h, r)
171
+ if (tmeta.get(r.id).n) O.st.call(G, () => fxCleanup(r), 0)
172
+ return h
173
+ }
174
+ const byHandle = new Map()
175
+ const end = r => {
176
+ if (r.st === 'off') return
177
+ r.st = 'off'
178
+ timers.delete(r.id)
179
+ const m = tmeta.get(r.id)
180
+ if (m) byHandle.delete(m.h), tmeta.delete(r.id)
181
+ push({ k: 'timer', id: r.id, st: 'off' })
182
+ }
183
+ const clr = orig =>
184
+ function (h) {
185
+ const r = byHandle.get(h)
186
+ if (r) end(r)
187
+ return orig.call(G, h)
188
+ }
189
+ if (G.setTimeout.$bi) return
190
+ G.setTimeout = mk('setTimeout', 'timeout', O.st, true)
191
+ G.setInterval = mk('setInterval', 'interval', O.si, false)
192
+ G.clearTimeout = clr(O.ct)
193
+ G.clearInterval = clr(O.ci)
194
+ if (O.raf) (G.requestAnimationFrame = mk('requestAnimationFrame', 'raf', O.raf, true)), (G.cancelAnimationFrame = clr(O.caf))
195
+ G.setTimeout.$bi = 1
196
+ }
197
+ // Timer created in an fx: cleanup = the fx returned a function. An interval without cleanup is B0602.
198
+ function fxCleanup(r) {
199
+ const m = tmeta.get(r.id), n = m && m.n
200
+ if (m) m.n = null
201
+ r.cleanup = !!(n && n.k)
202
+ push({ k: 'timer', id: r.id, cleanup: r.cleanup })
203
+ if (!r.cleanup && r.kind === 'interval' && r.st === 'on') b602(n, 'setInterval', r.line)
204
+ }
205
+
206
+ // ---------- B0602 and B0601 ----------
207
+ const leaks = []
208
+ const flagged = new WeakSet()
209
+ const b602ByInst = new WeakMap()
210
+ function leak(r) {
211
+ if (leaks.some(l => l.code === r.code && l.h === r.h && l.msg === r.msg)) return
212
+ r.k = 'leak'
213
+ r.at = now()
214
+ leaks.push(r)
215
+ push(r)
216
+ }
217
+ function b602(n, what, line) {
218
+ if (!n || flagged.has(n) || n.s >= 3) return
219
+ flagged.add(n)
220
+ const i = instOf(n), name = i ? nameOf(i) : '?'
221
+ const fx = `fx in ${name} (${what})${line ? ' ' + line : ''}`
222
+ if (i) (b602ByInst.get(i.r) || b602ByInst.set(i.r, []).get(i.r)).push(fx)
223
+ leak({ code: 'B0602', h: i ? i.h : null, name, file: i && i.C.f, line, msg: `global ${what} inside an fx without cleanup`, fix: 'return a cleanup function from the fx: `fx(() => { …; return () => remove() })`', fx: [fx] })
224
+ }
225
+ function patchGlobalListeners() {
226
+ for (const t of [G.window, G.document].filter(Boolean)) {
227
+ const add = t.addEventListener
228
+ if (!add || add.$bi) continue
229
+ t.addEventListener = function (type, ...a) {
230
+ const c = ctx()
231
+ if (c && c.o === 'fx') {
232
+ const line = site().line
233
+ O.st.call(G, () => !c.n.k && b602(c.n, `addEventListener('${type}')`, line), 0)
234
+ }
235
+ return add.call(this, type, ...a)
236
+ }
237
+ t.addEventListener.$bi = 1
238
+ }
239
+ }
240
+ // GC cycles that can collect an old object (#295). A fresh object dies in any GC, a minor one included, and minor GCs
241
+ // never touch the promoted nodes an unmounted instance leaves behind: counting them flagged clean instances as leaks
242
+ // (B0601) whenever a page allocated between two checks. So each GC (seen by a fresh probe) ages a few strongly held
243
+ // sentinels; one that survived 2 GCs is promoted (V8 promotes on the second) and is then released: only a major GC collects it. Every release
244
+ // dying in the same GC counts once.
245
+ let gcs = 0, counted = false
246
+ const held = [[{}, 0]] // [object, GCs survived]; one from the start, so the first GCs already count
247
+ const major = G.FinalizationRegistry
248
+ ? new FinalizationRegistry(() => {
249
+ if (!counted) (counted = true), queueMicrotask(() => ((counted = false), gcs++))
250
+ })
251
+ : null
252
+ const probe = G.FinalizationRegistry
253
+ ? new FinalizationRegistry(() => {
254
+ for (let i = held.length - 1; i >= 0; i--) if (++held[i][1] >= 2) major.register(held.splice(i, 1)[0][0], 0)
255
+ held.push([{}, 0])
256
+ arm()
257
+ })
258
+ : null
259
+ const arm = () => probe && probe.register({}, 0)
260
+ const watched = []
261
+ // No deref(): reading a WeakRef keeps the target alive for the job (KeepDuringJob) and, checking every
262
+ // second, the inspector would cause the leak it measures. The FinalizationRegistry marks the record on collection.
263
+ const fin = G.FinalizationRegistry ? new FinalizationRegistry(w => (w.dead = 1)) : null
264
+ function watch(d) {
265
+ if (!fin) return
266
+ // its first DOM node: an app that keeps a detached node (a module array, a cache) is the leak to catch. The browser
267
+ // also keeps the last hovered or focused node for a while: `brain test --heap` moves the pointer away and blurs before
268
+ // collecting. Unmounted nodes no longer point at their instance (brain-dev, brain-guard), so a kept node is small.
269
+ const target = (d.n && d.n.find(x => x.nodeType === 1)) || d.r
270
+ if (!target) return
271
+ const w = { dead: 0, h: d.h, name: nameOf(d), file: (d.C && d.C.f) || null, at: gcs, fx: (d.r && b602ByInst.get(d.r)) || [] }
272
+ fin.register(target, w)
273
+ watched.push(w)
274
+ }
275
+ function checkLeaks() {
276
+ for (let j = watched.length - 1; j >= 0; j--) {
277
+ const w = watched[j]
278
+ if (w.dead) watched.splice(j, 1)
279
+ else if (gcs - w.at >= 2) {
280
+ watched.splice(j, 1)
281
+ leak({ code: 'B0601', h: w.h, name: w.name, file: w.file, msg: `unmounted ${w.name} instance still alive after 2 GC cycles`, fix: w.fx.length ? 'add cleanup to the listed fx: they retain the instance' : 'look for external references to its nodes or signals (global listeners, caches, module closures)', fx: w.fx })
282
+ }
283
+ }
284
+ }
285
+
286
+ // ---------- network: fetch and XHR ----------
287
+ let rid = 1
288
+ function patchNet() {
289
+ if (O.fetch && !G.fetch.$bi) {
290
+ G.fetch = function (input, init) {
291
+ const c = ctx(), s = site(), t0 = pnow()
292
+ const r = { k: 'net', id: rid++, kind: 'fetch', m: (init && init.method) || (input && input.method) || 'GET', url: String((input && input.url) || input), own: own(c), line: s.line, foreign: s.foreign, at: now(), st: null, ms: null, b: null }
293
+ push(r)
294
+ return O.fetch.call(this, input, init).then(
295
+ res => {
296
+ const len = res.headers && res.headers.get && res.headers.get('content-length')
297
+ push({ k: 'net', id: r.id, st: res.status, ms: +(pnow() - t0).toFixed(1), b: len == null ? null : +len })
298
+ return res
299
+ },
300
+ err => (push({ k: 'net', id: r.id, st: 0, ms: +(pnow() - t0).toFixed(1), err: String(err) }), Promise.reject(err))
301
+ )
302
+ }
303
+ G.fetch.$bi = 1
304
+ }
305
+ const X = G.XMLHttpRequest && G.XMLHttpRequest.prototype
306
+ if (X && !X.$bi) {
307
+ X.$bi = 1
308
+ const open = X.open, send = X.send
309
+ X.open = function (m, url, ...a) {
310
+ this.$r = { m, url: String(url) }
311
+ return open.call(this, m, url, ...a)
312
+ }
313
+ X.send = function (body) {
314
+ const c = ctx(), s = site(), t0 = pnow(), r = { k: 'net', id: rid++, kind: 'xhr', ...(this.$r || {}), own: own(c), line: s.line, foreign: s.foreign, at: now(), st: null, ms: null, b: null }
315
+ push(r)
316
+ this.addEventListener('loadend', () => push({ k: 'net', id: r.id, st: this.status, ms: +(pnow() - t0).toFixed(1), b: typeof this.responseText === 'string' ? this.responseText.length : null }))
317
+ return send.call(this, body)
318
+ }
319
+ }
320
+ }
321
+
322
+ // ---------- storage: cookie, Storage, IndexedDB, Cache Storage ----------
323
+ const storeKey = new Map() // `${area}:${key}` → last record (avoids repeating reads)
324
+ function store(area, key, op, extra) {
325
+ const c = ctx(), s = site(), id = area + ':' + key
326
+ if (op === 'get' && storeKey.has(id) && storeKey.get(id).own && storeKey.get(id).own.id === (c && idOf(c.i))) return
327
+ const r = { k: 'store', area, key, op, own: own(c), line: s.line, foreign: s.foreign, at: now(), ...extra }
328
+ storeKey.set(id, r)
329
+ push(r)
330
+ }
331
+ function patchStorage() {
332
+ const D = G.Document && G.Document.prototype
333
+ const cd = D && Object.getOwnPropertyDescriptor(D, 'cookie')
334
+ if (cd && !cd.set.$bi) {
335
+ const set = function (v) {
336
+ const [pair, ...attrs] = String(v).split(';')
337
+ const eq = pair.indexOf('='), name = pair.slice(0, eq).trim(), val = pair.slice(eq + 1).trim()
338
+ const fl = {}
339
+ for (const a of attrs) {
340
+ const [k, x] = a.split('=').map(s => s.trim())
341
+ fl[k.toLowerCase()] = x ?? true
342
+ }
343
+ store('cookie', name, 'set', { v: val, flags: { secure: !!fl.secure, sameSite: fl.samesite || null, path: fl.path || null }, exp: fl.expires || (fl['max-age'] != null ? `max-age=${fl['max-age']}` : 'session') })
344
+ cd.set.call(this, v)
345
+ }
346
+ set.$bi = 1
347
+ Object.defineProperty(D, 'cookie', {
348
+ configurable: true,
349
+ get() {
350
+ const v = cd.get.call(this)
351
+ for (const p of v ? v.split('; ') : []) store('cookie', p.slice(0, p.indexOf('=')), 'get')
352
+ return v
353
+ },
354
+ set,
355
+ })
356
+ }
357
+ const S = G.Storage && G.Storage.prototype
358
+ if (S && !S.$bi) {
359
+ S.$bi = 1
360
+ const area = st => (st === G.localStorage ? 'local' : 'session')
361
+ for (const [m, op] of [['setItem', 'set'], ['getItem', 'get'], ['removeItem', 'remove']]) {
362
+ const f = S[m]
363
+ S[m] = function (k, v) {
364
+ store(area(this), String(k), op, op === 'set' ? { v: prev(String(v)) } : {})
365
+ return f.apply(this, arguments)
366
+ }
367
+ }
368
+ const clear = S.clear
369
+ S.clear = function () {
370
+ store(area(this), '*', 'remove')
371
+ return clear.call(this)
372
+ }
373
+ }
374
+ if (G.indexedDB && !G.indexedDB.$bi) {
375
+ G.indexedDB.$bi = 1
376
+ const open = G.indexedDB.open
377
+ G.indexedDB.open = function (name, ver) {
378
+ store('idb', String(name), 'open', { ver: ver ?? null })
379
+ dbs.add(String(name))
380
+ return open.apply(this, arguments)
381
+ }
382
+ }
383
+ if (G.caches && !G.caches.$bi) {
384
+ G.caches.$bi = 1
385
+ const open = G.caches.open
386
+ G.caches.open = function (name) {
387
+ store('cache', String(name), 'open')
388
+ cacheNames.add(String(name))
389
+ return open.apply(this, arguments)
390
+ }
391
+ }
392
+ }
393
+ const dbs = new Set(), cacheNames = new Set()
394
+ // Every 5 s: stores, count and a sample of records from the databases and caches the app opened.
395
+ async function scanStores() {
396
+ for (const name of dbs) {
397
+ const req = G.indexedDB.open.call(G.indexedDB, name) // no version: does not trigger upgrade
398
+ const db = await new Promise(r => ((req.onsuccess = () => r(req.result)), (req.onerror = () => r(null))))
399
+ if (!db) continue
400
+ for (const sname of db.objectStoreNames) {
401
+ const os = db.transaction(sname, 'readonly').objectStore(sname)
402
+ const [count, sample] = await Promise.all([
403
+ new Promise(r => (os.count().onsuccess = e => r(e.target.result))),
404
+ new Promise(r => (os.getAll(undefined, 3).onsuccess = e => r(e.target.result))),
405
+ ])
406
+ push({ k: 'idb', db: name, store: sname, count, sample: prev(sample) })
407
+ }
408
+ db.close()
409
+ }
410
+ for (const name of cacheNames) {
411
+ const c = await G.caches.open.call(G.caches, name)
412
+ push({ k: 'cachestore', name, count: (await c.keys()).length })
413
+ }
414
+ }
415
+
416
+ // ---------- connections: WebSocket, EventSource, BroadcastChannel, Worker ----------
417
+ let cid = 1
418
+ function wrapCtor(name, kind, urlOf) {
419
+ const C = G[name]
420
+ if (!C || C.$bi) return
421
+ const W = new Proxy(C, {
422
+ construct(T, args, NT) {
423
+ const o = Reflect.construct(T, args, NT)
424
+ const c = ctx(), s = site()
425
+ const r = { k: 'conn', id: cid++, kind, url: urlOf(args), own: own(c), line: s.line, foreign: s.foreign, st: 'opening', in: 0, out: 0, bin: 0, bout: 0 }
426
+ push(r)
427
+ const upd = patch => push({ k: 'conn', id: r.id, ...patch })
428
+ const size = d => (typeof d === 'string' ? d.length : d && (d.byteLength ?? d.size)) || 0
429
+ o.addEventListener('open', () => upd({ st: 'open' }))
430
+ o.addEventListener('message', e => ((r.in++), (r.bin += size(e.data)), upd({ in: r.in, bin: r.bin })))
431
+ o.addEventListener('error', () => upd({ st: 'error' }))
432
+ o.addEventListener('close', () => upd({ st: 'closed' }))
433
+ const send = o.send || o.postMessage
434
+ const m = o.send ? 'send' : 'postMessage'
435
+ if (send) o[m] = function (d, ...a) {
436
+ r.out++
437
+ r.bout += size(d)
438
+ upd({ out: r.out, bout: r.bout })
439
+ return send.call(this, d, ...a)
440
+ }
441
+ const close = o.close || o.terminate
442
+ const cm = o.close ? 'close' : 'terminate'
443
+ if (close) o[cm] = function () {
444
+ upd({ st: 'closed' })
445
+ return close.apply(this, arguments)
446
+ }
447
+ if (kind === 'bc' || kind === 'worker') upd({ st: 'open' })
448
+ return o
449
+ },
450
+ })
451
+ W.$bi = 1
452
+ G[name] = W
453
+ }
454
+ function patchConns() {
455
+ wrapCtor('WebSocket', 'ws', a => String(a[0]))
456
+ wrapCtor('EventSource', 'es', a => String(a[0]))
457
+ wrapCtor('BroadcastChannel', 'bc', a => String(a[0]))
458
+ wrapCtor('Worker', 'worker', a => String(a[0]))
459
+ }
460
+
461
+ // ---------- tree, state, counts and graph per component ----------
462
+ const sent = new WeakMap() // instance → signature of the last sent state
463
+ function walk(r, self, f) {
464
+ for (const c of r.c || []) {
465
+ if (inst.has(c) && inst.get(c) !== self) continue
466
+ f(c)
467
+ walk(c, self, f)
468
+ }
469
+ }
470
+ function countDom(n, acc, self) {
471
+ if (n.nodeType === 1 && n.$i && n.$i !== self) return
472
+ acc.nodes++
473
+ if (n.nodeType === 1) for (const k in n) if (k[0] === '$' && k.length > 2 && typeof n[k] === 'function') acc.ev++
474
+ for (let c = n.firstChild; c; c = c.nextSibling) countDom(c, acc, self)
475
+ }
476
+ // Estimated bytes from runtime/perf/heap.js measurements (sig 112 B, child fx 170 B) plus an approximate DOM cost.
477
+ const EST = { sig: 112, drv: 230, fx: 170, nodes: 160, ev: 40 }
478
+ // ---------- Jank (§19): Long Animation Frames (Chrome 123+) and Event Timing (INP per component) ----------
479
+ const inpBy = new Map() // component hash → worst interaction latency (ms)
480
+ const pathOf = u => {
481
+ try {
482
+ return new URL(u, G.location && G.location.href).pathname
483
+ } catch {
484
+ return String(u || '')
485
+ }
486
+ }
487
+ // Component whose source file is served at this URL (dev serves `.brain` files at their path).
488
+ function compOfUrl(u) {
489
+ const p = pathOf(u)
490
+ for (const set of live.values())
491
+ for (const i of set) {
492
+ const f = i.C && (i.C.f || (i.C.C && i.C.C.f))
493
+ if (f && p.endsWith(f)) return { h: i.h, name: nameOf(i) }
494
+ }
495
+ return null
496
+ }
497
+ export function onLoaf(list) {
498
+ for (const e of list)
499
+ push({
500
+ k: 'jank', at: now(), ms: +e.duration.toFixed(1), block: +(e.blockingDuration || 0).toFixed(1),
501
+ scripts: (e.scripts || []).map(x => ({ src: pathOf(x.sourceURL), fn: x.sourceFunctionName || null, inv: x.invoker || null, ms: +x.duration.toFixed(1), comp: compOfUrl(x.sourceURL) })),
502
+ })
503
+ }
504
+ export function onEvents(list) {
505
+ for (const e of list) {
506
+ let i = null
507
+ for (let x = e.target; x && !i; x = x.parentNode) i = x.$i || null
508
+ const ms = Math.round(e.duration)
509
+ if (i) inpBy.set(i.h, Math.max(inpBy.get(i.h) || 0, ms))
510
+ push({ k: 'inp', at: now(), type: e.name, ms, delay: Math.round(e.processingStart - e.startTime), proc: Math.round(e.processingEnd - e.processingStart), comp: i ? { id: idOf(i), h: i.h, name: nameOf(i) } : null })
511
+ }
512
+ }
513
+
514
+ export function comps() {
515
+ const res = []
516
+ for (const [h, set] of live) {
517
+ if (!set.size) continue
518
+ const a = { k: 'comp', h, name: null, f: null, inst: 0, sig: 0, drv: 0, fx: 0, ev: 0, nodes: 0, est: 0, runs: 0, ms: 0, inp: inpBy.get(h) || null }
519
+ for (const i of set) {
520
+ a.name = a.name || nameOf(i)
521
+ a.f = a.f || i.C.f || null
522
+ a.inst++
523
+ a.sig += i.m ? i.m.size : 0
524
+ if (i.r) walk(i.r, i, c => c.f && (c.e ? a.fx++ : a.drv++))
525
+ for (const n of i.n || []) countDom(n, a, i)
526
+ a.runs += i.$fx || 0
527
+ a.ms += i.$ms || 0
528
+ }
529
+ a.fx = Math.max(0, a.fx - a.inst) // brain-dev internal fx (unregistration)
530
+ a.ms = +a.ms.toFixed(2)
531
+ a.est = a.sig * EST.sig + a.drv * EST.drv + a.fx * EST.fx + a.nodes * EST.nodes + a.ev * EST.ev
532
+ res.push(a)
533
+ }
534
+ return res
535
+ }
536
+ function states() {
537
+ for (const set of live.values())
538
+ for (const i of set) {
539
+ const sigs = [...(i.m || [])].map(([n, s]) => ({ n, v: prev(s._), w: (writes.get(s) || {}).w || null, c: (writes.get(s) || {}).c || 0 }))
540
+ const drv = []
541
+ if (i.r) walk(i.r, i, c => c.f && !c.e && drv.push(prev(c._)))
542
+ let props = {}
543
+ try {
544
+ for (const k of Object.keys(i.$p || {})) props[k] = prev(i.$p[k])
545
+ } catch {
546
+ props = '?'
547
+ }
548
+ const r = { k: 'state', id: idOf(i), sigs, drv, props }
549
+ const sig = JSON.stringify(r)
550
+ if (sent.get(i) !== sig) sent.set(i, sig), push(r)
551
+ }
552
+ }
553
+ function graphs() {
554
+ for (const set of live.values()) {
555
+ const i = set.values().next().value
556
+ if (!i || !i.r) continue
557
+ const nodes = [], idx = new Map(), edges = []
558
+ const add = (n, k, name) => (idx.has(n) || (idx.set(n, nodes.length), nodes.push({ k, name })), idx.get(n))
559
+ for (const [name, s] of i.m || []) add(s, 'sig', name)
560
+ walk(i.r, i, c => c.f && add(c, c.e ? 'fx' : 'drv', null))
561
+ for (const [n, j] of idx) {
562
+ const S = n.S || []
563
+ for (let x = 0; x < S.length; x += 2) if (idx.has(S[x])) edges.push([idx.get(S[x]), j])
564
+ if (nodes.length > 200) break
565
+ }
566
+ push({ k: 'graph', h: i.h, nodes: nodes.slice(0, 200), edges })
567
+ }
568
+ }
569
+
570
+ // ---------- heap ----------
571
+ let measuring = false, lastHeap = null
572
+ function sample() {
573
+ let used = syncHeap(), src = used == null ? 'none' : G.performance && performance.memory ? 'memory' : 'node'
574
+ if (!measuring && G.crossOriginIsolated && G.performance && performance.measureUserAgentSpecificMemory) {
575
+ measuring = true
576
+ performance.measureUserAgentSpecificMemory().then(
577
+ m => ((measuring = false), push({ k: 'heap', at: now(), used: m.bytes, src: 'uasm' })),
578
+ () => (measuring = false)
579
+ )
580
+ }
581
+ if (used != null) push({ k: 'heap', at: now(), used, src })
582
+ return used
583
+ }
584
+ function event(e, data) {
585
+ const u = syncHeap()
586
+ push({ k: 'ev', at: now(), e, dh: u != null && lastHeap != null ? u - lastHeap : null, ...data })
587
+ if (u != null) lastHeap = u
588
+ }
589
+
590
+ // ---------- cycle ----------
591
+ let ticks = 0, lastTop = null
592
+ // SSR islands still inert (no first event): count per hash.
593
+ function islands() {
594
+ if (!G.document || !G.document.querySelectorAll) return
595
+ const inert = {}
596
+ for (const s of G.document.querySelectorAll('script[type="b/state"]')) {
597
+ const el = s.previousSibling
598
+ const h = el && el.getAttribute && el.getAttribute('data-b')
599
+ if (h) inert[h] = (inert[h] || 0) + 1
600
+ }
601
+ push({ k: 'islands', inert })
602
+ }
603
+ export function tick() {
604
+ ticks++
605
+ lastHeap = sample() ?? lastHeap
606
+ checkLeaks()
607
+ for (const c of comps()) push(c)
608
+ states()
609
+ if (ticks % 5 === 1) graphs(), islands(), dbs.size + cacheNames.size && scanStores().catch(() => {})
610
+ return drain()
611
+ }
612
+ export function drain() {
613
+ if (!out.length) return null
614
+ const m = { t: 'm', v: 2, seq: seq++, at: now(), d: out }
615
+ out = []
616
+ if (opts.send) opts.send(JSON.stringify(m))
617
+ return m
618
+ }
619
+
620
+ export function inspect(o) {
621
+ if (started) return api // inspect() without args is read-only: does not overwrite send/options of the first call
622
+ opts = o = o || {}
623
+ started = true
624
+ // Errors (§18): brain-err with dev attribution (instance → name, file and parent chain).
625
+ capture({
626
+ who(x) {
627
+ let i = null
628
+ if (x && x.nodeType) for (; x && !i; x = x.parentNode) i = x.$i || null
629
+ else i = instOf(x)
630
+ if (!i) return null
631
+ const chain = []
632
+ for (let j = i; j; j = instOf(j.p)) chain.push(nameOf(j))
633
+ return { id: idOf(i), h: i.h, name: nameOf(i), file: (i.C && (i.C.f || (i.C.C && i.C.C.f))) || null, chain }
634
+ },
635
+ sigName,
636
+ maps: true, // dev: errors point at file.brain:line through each module's inline source map
637
+ })
638
+ // Channels (§12): one record per new (component, kind, name, role) edge, for the Graph view. No owner is retained.
639
+ const chans = new WeakMap()
640
+ seen.add((kind, name, role, o) => {
641
+ const i = instOf(o), key = `${kind}|${name}|${role}`, done = i && (chans.get(i) || (chans.set(i, new Set()), chans.get(i)))
642
+ if (done && done.has(key)) return
643
+ if (done) done.add(key)
644
+ push({ k: 'chan', kind, name, role, id: i ? idOf(i) : null, h: i ? i.h : null, comp: i ? nameOf(i) : null })
645
+ })
646
+ sub.add(r => push({ k: 'err', fp: r.fp, code: r.code, msg: r.msg, where: r.where, comp: r.comp, line: r.line, src: r.src || null, stack: r.brainStack || r.stack, crumbs: r.crumbs, n: r.n, at: r.last }))
647
+ patchTimers()
648
+ patchGlobalListeners()
649
+ patchHandlers()
650
+ patchNet()
651
+ patchStorage()
652
+ patchConns()
653
+ arm()
654
+ if (G.MutationObserver && G.document) (mo = new MutationObserver(() => {})).observe(G.document, { subtree: true, childList: true, attributes: true, characterData: true })
655
+ bus.add((k, d) => {
656
+ if (k === 'mount') {
657
+ if (d.m && d.m.size) patchSignals(d.m.values().next().value)
658
+ const pi = instOf(d.p)
659
+ if (!pi) lastTop = d
660
+ push({ k: 'inst', id: idOf(d), h: d.h, name: nameOf(d), f: d.C.f || null, parent: pi ? idOf(pi) : null, island: null })
661
+ event('mount', { id: idOf(d), h: d.h, ms: d.$mt == null ? null : +d.$mt.toFixed(2) })
662
+ if (O.raf) {
663
+ const t = pnow(), id = idOf(d)
664
+ O.raf.call(G, () => event('paint', { id, h: d.h, ms: +(pnow() - t).toFixed(1) }))
665
+ }
666
+ } else if (k === 'dispose') {
667
+ if (lastTop === d) lastTop = null // the inspector must not retain what it watches
668
+ if (handler && handler.i === d) handler = null
669
+ watch(d)
670
+ if (ids.has(d)) push({ k: 'gone', id: idOf(d) })
671
+ event('dispose', { h: d.h })
672
+ } else if (k === 'hydrate') {
673
+ if (lastTop) push({ k: 'inst', id: idOf(lastTop), island: 'hydrated' })
674
+ event('hydrate', { url: d.url, id: lastTop ? idOf(lastTop) : null })
675
+ }
676
+ else event('hmr', { h: d.h, n: d.n })
677
+ })
678
+ if (G.PerformanceObserver)
679
+ try {
680
+ new PerformanceObserver(l => {
681
+ for (const e of l.getEntries())
682
+ if (/\/(_b|@brain)\//.test(e.name)) event('module', { url: new URL(e.name).pathname, ms: +e.duration.toFixed(1), b: e.encodedBodySize || null })
683
+ }).observe({ type: 'resource', buffered: true })
684
+ } catch {}
685
+ const types = (G.PerformanceObserver && PerformanceObserver.supportedEntryTypes) || []
686
+ if (types.includes('long-animation-frame')) new PerformanceObserver(l => onLoaf(l.getEntries())).observe({ type: 'long-animation-frame', buffered: true })
687
+ if (types.includes('event')) new PerformanceObserver(l => onEvents(l.getEntries())).observe({ type: 'event', durationThreshold: 16, buffered: true })
688
+ const ms = (v, d) => Math.min(v || d, 2147483647) // >int32: Node turns it into 1 ms
689
+ const t1 = O.si.call(G, tick, ms(o.sample, 1000))
690
+ const t2 = O.si.call(G, drain, ms(o.batch, 100))
691
+ if (t1.unref) t1.unref(), t2.unref()
692
+ if (G.document && G.document.addEventListener && !o.noPanel)
693
+ G.document.addEventListener.call(G.document, 'keydown', e => e.altKey && e.shiftKey && e.code === 'KeyB' && toggle())
694
+ return api
695
+ }
696
+ const api = { tick, drain, comps, onLoaf, onEvents, leaks: () => leaks.slice(), timers: () => [...timers.values()], toggle: () => toggle() }
697
+
698
+ // ---------- mini panel (Alt+Shift+B): heap, leaks and link to the full debugger (§9.1) ----------
699
+ let panel = null
700
+ const CSS = `:host{all:initial;position:fixed;right:16px;bottom:16px;z-index:2147483647;font:12px/1.45 system-ui,-apple-system,sans-serif;color:#1a1a1a}
701
+ .p{width:min(340px,calc(100vw - 32px));border-radius:16px;padding:12px 14px;background:rgba(255,255,255,.72);
702
+ -webkit-backdrop-filter:blur(20px) saturate(1.6);backdrop-filter:blur(20px) saturate(1.6);border:1px solid rgba(0,0,0,.08);
703
+ box-shadow:0 1px 3px rgba(16,24,40,.08),0 12px 32px rgba(16,24,40,.14)}@supports not (backdrop-filter:blur(1px)){.p{background:#fff}}
704
+ @media (prefers-color-scheme:dark){:host{color:#f2f2f2}.p{background:rgba(28,28,30,.8);border-color:rgba(255,255,255,.1)}}
705
+ .h{display:flex;justify-content:space-between;font-weight:600}.k{font-variant-numeric:tabular-nums;font-weight:400;opacity:.65}
706
+ .l{margin-top:8px;padding:6px 8px;border-radius:10px;background:rgba(255,59,48,.1)}.l b{color:#d70015}.ok{margin-top:8px;opacity:.6}
707
+ a{display:inline-block;margin-top:10px;color:#0071e3;text-decoration:none;font-weight:500}`
708
+ function toggle() {
709
+ if (!panel) {
710
+ const host = document.createElement('brain-inspect')
711
+ const sr = host.attachShadow({ mode: 'open' })
712
+ sr.innerHTML = `<style>${CSS}</style><div class="p"></div>`
713
+ panel = { host, box: sr.lastChild, on: false }
714
+ }
715
+ panel.on = !panel.on
716
+ if (!panel.on) return panel.host.remove()
717
+ document.body.appendChild(panel.host)
718
+ const u = syncHeap(), port = opts.inspectPort || (location.port ? +location.port + 1 : 5181)
719
+ const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c])
720
+ panel.box.innerHTML = `<div class="h">brain-inspect <span class="k">${u == null ? '—' : (u / 1048576).toFixed(1) + ' MB'} · GC ${gcs}</span></div>` +
721
+ (leaks.length ? leaks.map(l => `<div class="l"><b>${l.code}</b> ${esc(l.msg)}</div>`).join('') : '<div class="ok">No leaks detected.</div>') +
722
+ `<a href="${location.protocol}//${location.hostname}:${port}/" target="_blank" rel="noopener">Open full debugger →</a>`
723
+ }
724
+
725
+ /* FORMAT v2 (every record in `d` has `k`; records with `id` are updated by patches with the same id):
726
+ heap {at, used, src:'uasm'|'memory'|'node'|'none'}
727
+ ev {at, e:'mount'|'dispose'|'hmr'|'hydrate'|'module', h?, id?, url?, ms?, b?, dh}
728
+ inst {id, h, name, f, parent, island:null|'hydrated'} gone {id} islands {inert:{hash: n}} (every 5 s)
729
+ ev.e also 'paint' (mount → first frame, ms)
730
+ comp {h, name, f, inst, sig, drv, fx, ev, nodes, est, runs, ms, inp} (every 1 s; inp = worst interaction ms or null)
731
+ jank {at, ms, block, scripts:[{src, fn, inv, ms, comp:{h,name}|null}]} (Long Animation Frame ≥50 ms)
732
+ inp {at, type, ms, delay, proc, comp:{id,h,name}|null} (Event Timing, interactions ≥16 ms)
733
+ state {id, sigs:[{n, v, w, c}], drv:[v], props} (only if changed)
734
+ graph {h, nodes:[{k:'sig'|'drv'|'fx', name}], edges:[[from, to]]} (every 5 s)
735
+ upd {at, sig, w, fx, nodes, ms} (signal written → effects → nodes touched)
736
+ handler {at, id, h, name, type, ms}
737
+ net {id, kind:'fetch'|'xhr', m, url, own, line, foreign, at, st, ms, b}
738
+ store {area:'cookie'|'local'|'session'|'idb'|'cache', key, op:'set'|'get'|'remove'|'open', v?, flags?, exp?:'session'|'max-age=N'|<expires>, own, line, foreign, at}
739
+ idb {db, store, count, sample} cachestore {name, count}
740
+ conn {id, kind:'ws'|'es'|'bc'|'worker', url, own, line, foreign, st:'opening'|'open'|'error'|'closed', in, out, bin, bout}
741
+ timer {id, kind:'timeout'|'interval'|'raf', ms, own, line, foreign, cleanup:true|false|null, st:'on'|'off'}
742
+ leak {code:'B0601'|'B0602', h, name, file, line?, msg, fix, fx:[…], at}
743
+ chan {kind:'ctx'|'store'|'bus'|'api', name, role:'provide'|'use'|'read'|'write'|'send'|'on'|'expose', id, h, comp} (once per component edge; id null = module scope)
744
+ err {fp, code, msg, where:'fx'|'drv'|'on:<ev>'|'render'|'B0902'|'onerror'|'promise'|'rpc', comp:{id,h,name,file,chain}, line, stack, crumbs, n, at}
745
+ own = {id, h, name, o:'fx'|'setup'|'on:<event>'} | null · line = "url:line" of the first app frame · foreign = no app frame */