@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.
- package/LICENSE +21 -0
- package/README.md +4 -2
- package/brain-anim.js +21 -0
- package/brain-ch.js +254 -0
- package/brain-consent.js +38 -0
- package/brain-data.js +547 -0
- package/brain-dev.js +100 -0
- package/brain-env.js +160 -0
- package/brain-err.js +242 -0
- package/brain-form.js +260 -0
- package/brain-guard.js +242 -0
- package/brain-hydrate.js +72 -0
- package/brain-i18n.js +111 -0
- package/brain-inspect.js +745 -0
- package/brain-island.js +106 -0
- package/brain-lazy.js +88 -0
- package/brain-mcp.js +58 -0
- package/brain-mock.js +80 -0
- package/brain-overlay.js +123 -0
- package/brain-persist.js +65 -0
- package/brain-router.js +353 -0
- package/brain-rpc.js +17 -0
- package/brain-schema.js +69 -0
- package/brain-wc.js +69 -0
- package/brain-why.js +198 -0
- package/brain-worker.js +66 -0
- package/brain.js +421 -0
- package/package.json +20 -4
- package/server/server.mjs +882 -0
- package/shim/dom.js +448 -0
- package/shim/probe.js +111 -0
- package/sw/brain-sw.js +103 -0
- package/themes/contrast.cssbrain +39 -0
- package/themes/dark.cssbrain +39 -0
- package/themes/glass.cssbrain +39 -0
- package/themes/paper.cssbrain +39 -0
- package/ui/button.brain +31 -0
- package/ui/center.brain +11 -0
- package/ui/checkbox.brain +13 -0
- package/ui/container.brain +11 -0
- package/ui/dialog.brain +29 -0
- package/ui/error-state.brain +53 -0
- package/ui/field.brain +34 -0
- package/ui/grid.brain +11 -0
- package/ui/input.brain +23 -0
- package/ui/lang-switch.brain +38 -0
- package/ui/menu.brain +64 -0
- package/ui/row.brain +12 -0
- package/ui/select.brain +29 -0
- package/ui/sidebar.brain +17 -0
- package/ui/spacer.brain +12 -0
- package/ui/stack.brain +10 -0
- package/ui/table.brain +73 -0
- package/ui/tabs.brain +33 -0
- package/ui/toast.brain +45 -0
package/brain-inspect.js
ADDED
|
@@ -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 => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[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 */
|