@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-why.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// BRAINLLM update provenance (INTERNALS §43.2-§43.1, #198). Development only: the dev client loads it next to
|
|
2
|
+
// brain-dev.js; nothing ships to production and the core carries no hook for it.
|
|
3
|
+
// - why(selector | node) → for a node that updated: which signal write caused each change (component, field,
|
|
4
|
+
// `file.brain:line` of the write) and the chain of drv between them. MCP `brain_why` (TOOLCHAIN #200) calls it
|
|
5
|
+
// through globalThis.__brainWhy.
|
|
6
|
+
// - interactions() → per user interaction (click, input, change, keydown, submit): how many DOM mutations it caused
|
|
7
|
+
// and on how many nodes, for the impact report (`brain_impact`, "updates per interaction").
|
|
8
|
+
// How: it wraps, in dev only, the signal setter (records the write and its stack), the node run (an effect run
|
|
9
|
+
// knows, before it re-runs, which of its sources were written: those are its causes) and the DOM write methods
|
|
10
|
+
// (each mutation made while an effect runs is attributed to that effect's causes).
|
|
11
|
+
import { sig } from './brain.js'
|
|
12
|
+
import { names, mounts as M } from './brain-dev.js'
|
|
13
|
+
|
|
14
|
+
const G = globalThis
|
|
15
|
+
const N = sig(0).constructor, P = N.prototype
|
|
16
|
+
const KEEP = 10, LAST = 50
|
|
17
|
+
const WR = new Map() // sig → write record, for the current flush (cleared once it settles)
|
|
18
|
+
const BY = new WeakMap() // node → [mutation records], newest first
|
|
19
|
+
const INT = [] // last interactions
|
|
20
|
+
let RUN = null // causes of the effect running now
|
|
21
|
+
let cur = null // interaction being measured
|
|
22
|
+
let clearing = 0
|
|
23
|
+
|
|
24
|
+
// ---------- signal writes ----------
|
|
25
|
+
const vd = Object.getOwnPropertyDescriptor(P, 'v')
|
|
26
|
+
Object.defineProperty(P, 'v', {
|
|
27
|
+
configurable: true,
|
|
28
|
+
get: vd.get,
|
|
29
|
+
set(x) {
|
|
30
|
+
if (!this.f && !Object.is(x, this._)) {
|
|
31
|
+
const info = names.get(this)
|
|
32
|
+
WR.set(this, { sig: this, name: (info && info.name) || null, comp: (info && info.comp) || null, from: this._, to: x, stack: new Error().stack || '' })
|
|
33
|
+
if (!clearing) (clearing = 1), setTimeout(() => ((clearing = 0), WR.clear()), 0) // after the flush microtasks
|
|
34
|
+
}
|
|
35
|
+
vd.set.call(this, x)
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// causes of a node's next run: its sources that were written, or reached through a drv chain
|
|
40
|
+
function causesOf(n, via = [], seen = new Set()) {
|
|
41
|
+
const out = [], S = n.S
|
|
42
|
+
if (!S) return out
|
|
43
|
+
for (let i = 0; i < S.length; i += 2) {
|
|
44
|
+
const s = S[i]
|
|
45
|
+
if (seen.has(s)) continue
|
|
46
|
+
seen.add(s)
|
|
47
|
+
const w = WR.get(s)
|
|
48
|
+
if (w) out.push({ w, via })
|
|
49
|
+
else if (s.f && !s.e) out.push(...causesOf(s, [...via, (names.get(s) && names.get(s).name) || 'drv'], seen))
|
|
50
|
+
}
|
|
51
|
+
return out
|
|
52
|
+
}
|
|
53
|
+
const r0 = P.r
|
|
54
|
+
P.r = function () {
|
|
55
|
+
if (!this.e || !WR.size) return r0.call(this)
|
|
56
|
+
const prev = RUN
|
|
57
|
+
RUN = causesOf(this)
|
|
58
|
+
try {
|
|
59
|
+
return r0.call(this)
|
|
60
|
+
} finally {
|
|
61
|
+
RUN = prev
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------- DOM writes ----------
|
|
66
|
+
function note(node, kind) {
|
|
67
|
+
if (!node || !RUN || !RUN.length) return
|
|
68
|
+
const rec = { kind, t: Date.now(), causes: RUN }
|
|
69
|
+
for (const n of node.nodeType === 3 && node.parentNode ? [node, node.parentNode] : [node]) {
|
|
70
|
+
const a = BY.get(n) || (BY.set(n, []), BY.get(n))
|
|
71
|
+
a.unshift(rec)
|
|
72
|
+
a.length = Math.min(a.length, KEEP)
|
|
73
|
+
}
|
|
74
|
+
if (cur) cur.mutations++, cur.nodes.add(node)
|
|
75
|
+
}
|
|
76
|
+
const owner = (o, k) => {
|
|
77
|
+
for (; o; o = Object.getPrototypeOf(o)) {
|
|
78
|
+
const d = Object.getOwnPropertyDescriptor(o, k)
|
|
79
|
+
if (d) return [o, d]
|
|
80
|
+
}
|
|
81
|
+
return []
|
|
82
|
+
}
|
|
83
|
+
function wrap(sample, k, kind) {
|
|
84
|
+
const [o, d] = owner(sample, k)
|
|
85
|
+
if (!o || (d.value && d.value.$why) || (d.set && d.set.$why)) return
|
|
86
|
+
if (typeof d.value === 'function') {
|
|
87
|
+
const f = d.value
|
|
88
|
+
const g = function (...a) {
|
|
89
|
+
note(kind.node ? kind.node(this, a) : this, typeof kind === 'function' ? kind(a) : kind.k(a))
|
|
90
|
+
return f.apply(this, a)
|
|
91
|
+
}
|
|
92
|
+
g.$why = 1
|
|
93
|
+
Object.defineProperty(o, k, { ...d, value: g })
|
|
94
|
+
} else if (d.set) {
|
|
95
|
+
const set = function (v) {
|
|
96
|
+
note(this, kind)
|
|
97
|
+
d.set.call(this, v)
|
|
98
|
+
}
|
|
99
|
+
set.$why = 1
|
|
100
|
+
Object.defineProperty(o, k, { ...d, set })
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function install() {
|
|
104
|
+
const d = G.document
|
|
105
|
+
if (!d || G.__brainWhy) return
|
|
106
|
+
const el = d.createElement('div'), tx = d.createTextNode(''), cm = d.createComment('')
|
|
107
|
+
wrap(el, 'setAttribute', a => 'attr:' + a[0])
|
|
108
|
+
wrap(el, 'removeAttribute', a => 'attr:' + a[0])
|
|
109
|
+
wrap(el, 'textContent', 'text')
|
|
110
|
+
wrap(tx, 'data', 'text')
|
|
111
|
+
wrap(tx, 'nodeValue', 'text')
|
|
112
|
+
wrap(el, 'insertBefore', { node: (_, a) => a[0], k: () => 'insert' })
|
|
113
|
+
wrap(el, 'appendChild', { node: (_, a) => a[0], k: () => 'insert' })
|
|
114
|
+
wrap(el, 'removeChild', { node: (_, a) => a[0], k: () => 'remove' })
|
|
115
|
+
for (const n of [el, tx, cm]) wrap(n, 'remove', () => 'remove')
|
|
116
|
+
for (const k of ['value', 'checked', 'selected']) wrap(d.createElement('input'), k, 'prop:' + k)
|
|
117
|
+
// classList / style have no link back to their element: map them through the element's getter (browsers)
|
|
118
|
+
for (const [k, m] of [['classList', 'toggle'], ['style', 'setProperty']]) {
|
|
119
|
+
const [o, dd] = owner(el, k)
|
|
120
|
+
if (!o || !dd.get) continue
|
|
121
|
+
const back = new WeakMap()
|
|
122
|
+
Object.defineProperty(o, k, { ...dd, get() { const v = dd.get.call(this); back.set(v, this); return v } })
|
|
123
|
+
const [po, pd] = owner(el[k], m)
|
|
124
|
+
if (po && typeof pd.value === 'function') {
|
|
125
|
+
const f = pd.value
|
|
126
|
+
Object.defineProperty(po, m, { ...pd, value(...a) { note(back.get(this), (k === 'style' ? 'style:' : 'class:') + a[0]); return f.apply(this, a) } })
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// interactions: everything a user event causes until the updates settle
|
|
130
|
+
for (const t of ['click', 'input', 'change', 'keydown', 'submit'])
|
|
131
|
+
d.addEventListener(t, e => {
|
|
132
|
+
if (cur) return
|
|
133
|
+
cur = { type: t, target: describe(e.target), mutations: 0, nodes: new Set(), t0: G.performance ? performance.now() : Date.now() }
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
const c = cur
|
|
136
|
+
cur = null
|
|
137
|
+
INT.unshift({ type: c.type, target: c.target, mutations: c.mutations, nodes: c.nodes.size, ms: Math.round(((G.performance ? performance.now() : Date.now()) - c.t0) * 10) / 10 })
|
|
138
|
+
INT.length = Math.min(INT.length, LAST)
|
|
139
|
+
}, 0)
|
|
140
|
+
}, true)
|
|
141
|
+
G.__brainWhy = { why, interactions, mounts, clear }
|
|
142
|
+
}
|
|
143
|
+
const describe = n => {
|
|
144
|
+
if (!n || n.nodeType !== 1) return n && n.parentNode ? describe(n.parentNode) : null
|
|
145
|
+
const id = n.getAttribute('id'), c = n.getAttribute('class')
|
|
146
|
+
return n.tagName.toLowerCase() + (id ? '#' + id : '') + (c ? '.' + c.trim().split(/\s+/).join('.') : '')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------- answers ----------
|
|
150
|
+
const RT = /\/(@brain|_b)\/brain[\w-]*\.js|\/runtime\/brain[\w-]*\.js|\/brain-why\.js/
|
|
151
|
+
const MAPS = new Map()
|
|
152
|
+
// the first app frame of the write's stack, as file.brain:line when the dev module carries its source map
|
|
153
|
+
async function where(stack) {
|
|
154
|
+
for (const l of String(stack).split('\n').slice(1)) {
|
|
155
|
+
const m = /\(?((?:https?|file):\/\/[^\s)]+?):(\d+):\d+\)?\s*$/.exec(l)
|
|
156
|
+
if (!m || RT.test(m[1])) continue
|
|
157
|
+
const url = m[1], line = +m[2]
|
|
158
|
+
if (!MAPS.has(url)) MAPS.set(url, load(url))
|
|
159
|
+
const lines = await MAPS.get(url)
|
|
160
|
+
const path = url.replace(/^\w+:\/\/[^/]*/, '').replace(/^\//, '').replace(/\?.*$/, '')
|
|
161
|
+
return lines && lines[line - 1] ? `${path}:${lines[line - 1]}` : `${path}:${line}`
|
|
162
|
+
}
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
165
|
+
async function load(url) {
|
|
166
|
+
try {
|
|
167
|
+
if (!/^https?:/.test(url) || !G.fetch) return null
|
|
168
|
+
const t = await (await fetch(url)).text()
|
|
169
|
+
const m = /sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+)/.exec(t)
|
|
170
|
+
if (!m) return null
|
|
171
|
+
const { decode } = await import('./brain-err.js')
|
|
172
|
+
return decode(JSON.parse(atob(m[1])).mappings)
|
|
173
|
+
} catch {
|
|
174
|
+
return null
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const val = v => (typeof v === 'function' ? 'fn' : v && typeof v === 'object' ? JSON.stringify(v).slice(0, 80) : v)
|
|
178
|
+
|
|
179
|
+
// why('#total') → [{ kind: 'text', at, causes: [{ signal: 'cart.total', comp: 'views/cart.brain', src:
|
|
180
|
+
// 'views/cart.brain:12', from, to, via: ['subtotal'] }] }], newest first; [] when that node never updated
|
|
181
|
+
export async function why(target) {
|
|
182
|
+
const n = typeof target === 'string' ? G.document && document.querySelector(target) : target
|
|
183
|
+
if (!n) return { error: `no element matches ${target}` }
|
|
184
|
+
const recs = [...(BY.get(n) || [])]
|
|
185
|
+
return Promise.all(
|
|
186
|
+
recs.map(async r => ({
|
|
187
|
+
kind: r.kind,
|
|
188
|
+
at: r.t,
|
|
189
|
+
causes: await Promise.all(r.causes.map(async c => ({ signal: c.w.name, comp: c.w.comp, src: await where(c.w.stack), from: val(c.w.from), to: val(c.w.to), via: c.via }))),
|
|
190
|
+
}))
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
export const interactions = () => INT.slice()
|
|
194
|
+
// last mounts, newest first: [{ comp: 'comp/x.brain', ms, at, hmr }] (brain-dev measures each one)
|
|
195
|
+
export const mounts = () => M.slice()
|
|
196
|
+
export const clear = () => (INT.length = 0)
|
|
197
|
+
|
|
198
|
+
install()
|
package/brain-worker.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// BRAINLLM Web Worker bridge (INTERNALS §35.5). The compiler turns `*.worker.jsbrain` into a worker chunk that ends in
|
|
2
|
+
// `serve({ parseCsv, default: $default })`, and every importer into `const w = worker(new URL('…', import.meta.url))`
|
|
3
|
+
// plus `const parseCsv = (...a) => w.call('parseCsv', a)`. Messages use structured clone: { id, name, args } →
|
|
4
|
+
// { id, ok } | { id, err: { message, name, stack } }. The worker starts on the first call; a remote error is rethrown
|
|
5
|
+
// in the page as an Error with the same message and name (its stack in `remoteStack`).
|
|
6
|
+
|
|
7
|
+
// Worker side. `name` may be a path into the map ('default.mean'); the result may be a promise.
|
|
8
|
+
export function serve(map, scope = globalThis) {
|
|
9
|
+
scope.onmessage = async e => {
|
|
10
|
+
const { id, name, args } = e.data || {}
|
|
11
|
+
try {
|
|
12
|
+
let f = map, self
|
|
13
|
+
for (const k of String(name).split('.')) (self = f), (f = f == null ? undefined : f[k])
|
|
14
|
+
if (typeof f !== 'function') throw new TypeError(`worker: \`${name}\` is not an exported function`)
|
|
15
|
+
const ok = await f.apply(self, args || [])
|
|
16
|
+
scope.postMessage({ id, ok }) // throws DataCloneError for a result that cannot be cloned → reported below
|
|
17
|
+
} catch (x) {
|
|
18
|
+
scope.postMessage({ id, err: { message: (x && x.message) || String(x), name: (x && x.name) || 'Error', stack: x && x.stack } })
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Page side.
|
|
24
|
+
export function worker(url) {
|
|
25
|
+
let w = null, n = 0
|
|
26
|
+
const wait = new Map()
|
|
27
|
+
const failAll = err => {
|
|
28
|
+
for (const [, p] of wait) p[1](err)
|
|
29
|
+
wait.clear()
|
|
30
|
+
w = null // the next call starts a fresh worker
|
|
31
|
+
}
|
|
32
|
+
const start = () => {
|
|
33
|
+
w = new Worker(url, { type: 'module' })
|
|
34
|
+
w.onmessage = e => {
|
|
35
|
+
const { id, ok, err } = e.data || {}
|
|
36
|
+
const p = wait.get(id)
|
|
37
|
+
if (!p) return
|
|
38
|
+
wait.delete(id)
|
|
39
|
+
if (err) p[1](Object.assign(new Error(err.message), { name: err.name, remoteStack: err.stack }))
|
|
40
|
+
else p[0](ok)
|
|
41
|
+
}
|
|
42
|
+
w.onerror = e => {
|
|
43
|
+
if (e && e.preventDefault) e.preventDefault()
|
|
44
|
+
failAll(new Error('worker failed: ' + ((e && e.message) || url)))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
call(name, args) {
|
|
49
|
+
if (!w) start()
|
|
50
|
+
const id = ++n
|
|
51
|
+
return new Promise((ok, fail) => {
|
|
52
|
+
wait.set(id, [ok, fail])
|
|
53
|
+
try {
|
|
54
|
+
w.postMessage({ id, name, args })
|
|
55
|
+
} catch (x) {
|
|
56
|
+
wait.delete(id)
|
|
57
|
+
fail(x) // DataCloneError: an argument cannot cross to the worker (functions, DOM nodes…)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
},
|
|
61
|
+
terminate() {
|
|
62
|
+
if (w) w.terminate()
|
|
63
|
+
failAll(new Error('worker terminated'))
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
}
|
package/brain.js
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
// BRAINLLM runtime (SPEC §6). Push-pull signals with coloring: a write marks, a read recomputes only what is dirty.
|
|
2
|
+
// N: _ value · f fn · e effect · s state (0 clean 1 check 2 dirty 3 dead) · S sources · o observers
|
|
3
|
+
// S and o hold pairs [node, index of the link on the other side] (O(1) unlink) · c children · x index in p.c
|
|
4
|
+
// k cleanup · p owner. S/o/c start null (RAM). Methods: m mark · u update
|
|
5
|
+
// · r run · q clean · n unlink · d dispose. Short names: brain-min does not rename properties.
|
|
6
|
+
|
|
7
|
+
// L tracker · O owner · Q pending effects · B batch depth · P flush scheduled · H dev hook · E error hook · V provider (brain-ch)
|
|
8
|
+
let L = null, O = null, Q = [], B = 0, P = 0, H = null, E = null, V, A = null
|
|
9
|
+
// Errors (INTERNALS §18): fx, drv and handlers are caught and the app keeps running. E (brain-err.js, _err) records
|
|
10
|
+
// them and applies per-component fallbacks; without it, console.error.
|
|
11
|
+
// Errors go to the rich catcher (brain-err) or the console, then to the nearest component boundary (§42, brain-guard.js):
|
|
12
|
+
// up the owner chain for fx/drv, up the DOM for handlers.
|
|
13
|
+
const oops = (e, x, w) => {
|
|
14
|
+
if (e && e.kind == 'aborted') return // a cancelled request (brain-data): never an error to report or show
|
|
15
|
+
E ? E(e, x, w) : globalThis.reportError ? reportError(e) : console.error(e) // reportError: window error listeners (the dev client) see it
|
|
16
|
+
for (let o = x; o; o = o.nodeType ? o.parentNode : o.p) {
|
|
17
|
+
const g = o.$g
|
|
18
|
+
if (g) return g(e, w)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const adopt = (p, n) => {
|
|
23
|
+
n.p = p
|
|
24
|
+
if (p) n.x = (p.c || (p.c = [])).push(n) - 1
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class N {
|
|
28
|
+
constructor(v, f, e) {
|
|
29
|
+
this._ = v
|
|
30
|
+
this.f = f
|
|
31
|
+
this.e = e
|
|
32
|
+
this.s = f ? 2 : 0
|
|
33
|
+
this.S = null
|
|
34
|
+
this.o = null
|
|
35
|
+
this.c = null
|
|
36
|
+
this.k = null
|
|
37
|
+
this.p = null
|
|
38
|
+
this.x = 0
|
|
39
|
+
if (f) adopt(O, this)
|
|
40
|
+
}
|
|
41
|
+
get v() {
|
|
42
|
+
const l = L
|
|
43
|
+
if (l && l != this) {
|
|
44
|
+
const s = l.S || (l.S = [])
|
|
45
|
+
if (s[s.length - 2] != this && !s.includes(this)) {
|
|
46
|
+
const o = this.o || (this.o = [])
|
|
47
|
+
s.push(this, o.length)
|
|
48
|
+
o.push(l, s.length - 2)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (this.f) this.u()
|
|
52
|
+
return this._
|
|
53
|
+
}
|
|
54
|
+
set v(x) {
|
|
55
|
+
if (this.f) throw Error('drv is read-only')
|
|
56
|
+
if (!Object.is(x, this._)) {
|
|
57
|
+
this._ = x
|
|
58
|
+
const o = this.o
|
|
59
|
+
if (o) for (let i = 0; i < o.length; i += 2) o[i].m(2)
|
|
60
|
+
if (!B && !P) (P = 1), queueMicrotask(flush)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
m(s) {
|
|
64
|
+
if (this.s < s) {
|
|
65
|
+
if (this.e && !this.s) Q.push(this)
|
|
66
|
+
this.s = s
|
|
67
|
+
const o = this.o
|
|
68
|
+
if (o) for (let i = 0; i < o.length; i += 2) o[i].m(1)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
u() {
|
|
72
|
+
const S = this.S
|
|
73
|
+
if (this.s == 1 && S) for (let i = 0; i < S.length; i += 2) if ((S[i].u(), this.s == 2)) break
|
|
74
|
+
if (this.s == 2) this.r()
|
|
75
|
+
if (this.s < 3) this.s = 0
|
|
76
|
+
}
|
|
77
|
+
r() {
|
|
78
|
+
const pl = L, po = O, old = this._
|
|
79
|
+
this.q()
|
|
80
|
+
this.n()
|
|
81
|
+
L = O = this
|
|
82
|
+
let r, x = 0
|
|
83
|
+
try {
|
|
84
|
+
r = this.f()
|
|
85
|
+
} catch (e) {
|
|
86
|
+
;(x = 1), (r = old), oops(e, this, this.e ? 'fx' : 'drv')
|
|
87
|
+
} finally {
|
|
88
|
+
L = pl
|
|
89
|
+
O = po
|
|
90
|
+
}
|
|
91
|
+
if (x) return
|
|
92
|
+
if (this.e) {
|
|
93
|
+
if (typeof r == 'function') this.k = r
|
|
94
|
+
} else if (r !== old) {
|
|
95
|
+
this._ = r
|
|
96
|
+
const o = this.o
|
|
97
|
+
if (o) for (let i = 0; i < o.length; i += 2) if (o[i].s < 3) o[i].s = 2
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
q() {
|
|
101
|
+
const c = this.c
|
|
102
|
+
if (c) {
|
|
103
|
+
this.c = null // children see c=null and skip removing themselves one by one
|
|
104
|
+
for (const n of c) n.d()
|
|
105
|
+
}
|
|
106
|
+
const f = this.k
|
|
107
|
+
if (f) (this.k = null), f()
|
|
108
|
+
}
|
|
109
|
+
n() {
|
|
110
|
+
const S = this.S
|
|
111
|
+
if (S) {
|
|
112
|
+
// swap-remove: the last pair of s.o fills the gap and its index in l.S is fixed
|
|
113
|
+
for (let j = 0; j < S.length; j += 2) {
|
|
114
|
+
const o = S[j].o, k = S[j + 1], ly = o.pop(), l = o.pop()
|
|
115
|
+
if (k < o.length) (o[k] = l), (o[k + 1] = ly), (l.S[ly + 1] = k)
|
|
116
|
+
}
|
|
117
|
+
S.length = 0 // reused on the next run
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
d() {
|
|
121
|
+
if (this.s < 3) {
|
|
122
|
+
this.s = 3
|
|
123
|
+
const c = this.p && this.p.c
|
|
124
|
+
if (c) {
|
|
125
|
+
const l = c.pop()
|
|
126
|
+
if (l != this) (c[this.x] = l), (l.x = this.x)
|
|
127
|
+
}
|
|
128
|
+
this.q()
|
|
129
|
+
this.n()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function flush() {
|
|
135
|
+
P = 0
|
|
136
|
+
B++
|
|
137
|
+
let c = null
|
|
138
|
+
try {
|
|
139
|
+
for (let i = 0; i < Q.length; i++) {
|
|
140
|
+
const n = Q[i]
|
|
141
|
+
if (i > 100) {
|
|
142
|
+
// B0902: an effect re-run >100 times in one flush is cut off (stays dirty, not requeued)
|
|
143
|
+
const k = (c || (c = new Map())).get(n) + 1 || 1
|
|
144
|
+
c.set(n, k)
|
|
145
|
+
if (k > 100) {
|
|
146
|
+
if (k < 102) oops(Object.assign(Error('B0902'), { code: 'B0902', fx: n }), n, 'B0902')
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
n.u()
|
|
151
|
+
}
|
|
152
|
+
} finally {
|
|
153
|
+
Q = []
|
|
154
|
+
B--
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export const sig = (v, k) => (k && H ? H(new N(v, null, 0), k) : new N(v, null, 0))
|
|
159
|
+
export const drv = f => new N(undefined, f, 0)
|
|
160
|
+
export const fx = f => {
|
|
161
|
+
const n = new N(undefined, f, 1)
|
|
162
|
+
n.u()
|
|
163
|
+
return () => n.d()
|
|
164
|
+
}
|
|
165
|
+
export const batch = f => {
|
|
166
|
+
B++
|
|
167
|
+
try {
|
|
168
|
+
return f()
|
|
169
|
+
} finally {
|
|
170
|
+
if (!--B) flush()
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
export const untrack = f => {
|
|
174
|
+
const p = L
|
|
175
|
+
L = null
|
|
176
|
+
try {
|
|
177
|
+
return f()
|
|
178
|
+
} finally {
|
|
179
|
+
L = p
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Root owner: isolates from tracking and returns what f builds; f receives its dispose.
|
|
184
|
+
function root(f, p) {
|
|
185
|
+
const r = new N(), pl = L, po = O
|
|
186
|
+
adopt(p, r)
|
|
187
|
+
L = null
|
|
188
|
+
O = r
|
|
189
|
+
try {
|
|
190
|
+
return f(() => r.d())
|
|
191
|
+
} finally {
|
|
192
|
+
L = pl
|
|
193
|
+
O = po
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Internals for brain-dev.js: installs the named-sig hook; returns root and the current owner.
|
|
197
|
+
export const _dev = h => ((H = h), _rt())
|
|
198
|
+
// RPC (INTERNALS §11.1): codegen stub `(...a) => rpc("<hash>/save", a)`; the client is brain-rpc.js, loaded on the
|
|
199
|
+
// first call (#226: only pages that call the server pay for it).
|
|
200
|
+
export const rpc = (id, a) => import('./brain-rpc.js').then(m => m.rpc(id, a, oops, base()))
|
|
201
|
+
// Internals for sibling modules (brain-err, brain-ch): root, current owner and node helpers; _err installs the hook;
|
|
202
|
+
// brain-ch passes its provider for `mount(C, t, props, { provide })`.
|
|
203
|
+
// Optimistic writes (brain-data) and the circuit breaker (brain-guard) wrap N's prototype instead of a core hook (#288).
|
|
204
|
+
export const _rt = p => ((V = V || p), { root, owner: () => O, nodes, rm, oops, guard: (o, g) => (o.$g = g), adopt: a => (A = a), dev: (a, b) => H && H(a, b) })
|
|
205
|
+
export const _err = h => ((E = h), _rt())
|
|
206
|
+
|
|
207
|
+
// ---------- DOM ----------
|
|
208
|
+
|
|
209
|
+
const mk = p => p.appendChild(document.createComment(''))
|
|
210
|
+
// Yield to the browser: scheduler.yield (Chrome 129+), else a MessageChannel task (no 4 ms setTimeout clamp)
|
|
211
|
+
const later = f => {
|
|
212
|
+
if (globalThis.scheduler && scheduler.yield) return scheduler.yield().then(f)
|
|
213
|
+
const c = new MessageChannel()
|
|
214
|
+
c.port1.onmessage = () => (c.port1.close(), f())
|
|
215
|
+
c.port2.postMessage(0)
|
|
216
|
+
}
|
|
217
|
+
// a node with an exit transition (brain-anim.js `$out` → Promise) leaves once it has played
|
|
218
|
+
const rm = a => {
|
|
219
|
+
for (const n of a) n.$out ? n.$out().then(() => n.remove()) : n.remove()
|
|
220
|
+
}
|
|
221
|
+
function nodes(v, a = []) {
|
|
222
|
+
if (v == null || v === true || v === false) return a
|
|
223
|
+
if (Array.isArray(v)) for (const x of v) nodes(x, a)
|
|
224
|
+
else if (typeof v == 'function') nodes(v(), a)
|
|
225
|
+
else if (v.nodeType == 11) a.push(...v.childNodes)
|
|
226
|
+
else a.push(v.nodeType ? v : document.createTextNode(v))
|
|
227
|
+
return a
|
|
228
|
+
}
|
|
229
|
+
function place(end, v) {
|
|
230
|
+
const a = nodes(v), p = end.parentNode
|
|
231
|
+
if (A) return A.p(end, a)
|
|
232
|
+
for (const n of a) p.insertBefore(n, end)
|
|
233
|
+
return a
|
|
234
|
+
}
|
|
235
|
+
// Writes reach the DOM only when the value changed (Object.is): a row whose object changed rewrites only its new fields.
|
|
236
|
+
const bind = (f, s) => {
|
|
237
|
+
if (typeof f != 'function') return s(f)
|
|
238
|
+
let last = bind
|
|
239
|
+
fx(() => {
|
|
240
|
+
const v = f()
|
|
241
|
+
if (!Object.is(v, last)) s((last = v))
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function tpl(h) {
|
|
246
|
+
let n
|
|
247
|
+
return () => {
|
|
248
|
+
if (!n) {
|
|
249
|
+
const t = document.createElement('template')
|
|
250
|
+
t.innerHTML = h
|
|
251
|
+
const c = t.content
|
|
252
|
+
n = c.childNodes.length == 1 ? c.firstChild : c
|
|
253
|
+
}
|
|
254
|
+
// A: SSR stamps the clone, hydration adopts the node the server rendered (#285)
|
|
255
|
+
return A ? A.t(n) : n.cloneNode(true)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
export function insert(p, f, end) {
|
|
259
|
+
end = end || mk(p)
|
|
260
|
+
if (typeof f != 'function') return void place(end, f)
|
|
261
|
+
let cur = [], last = insert
|
|
262
|
+
fx(() => {
|
|
263
|
+
const v = f()
|
|
264
|
+
if (Object.is(v, last)) return
|
|
265
|
+
last = v
|
|
266
|
+
if (cur.length == 1 && cur[0].nodeType == 3 && (typeof v == 'string' || typeof v == 'number')) cur[0].data = v
|
|
267
|
+
else
|
|
268
|
+
untrack(() => {
|
|
269
|
+
rm(cur)
|
|
270
|
+
cur = place(end, v)
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// aria-* and data-* are enumerated, not boolean: true and false render as text (k[4] is their dash)
|
|
276
|
+
export function attr(el, k, f) {
|
|
277
|
+
bind(
|
|
278
|
+
f,
|
|
279
|
+
k.startsWith('style:')
|
|
280
|
+
? ((k = k.slice(6)), v => (v == null || v === false ? el.style.removeProperty(k) : el.style.setProperty(k, v)))
|
|
281
|
+
: /^(value|checked|selected)$/.test(k)
|
|
282
|
+
? v => {
|
|
283
|
+
if (el[k] !== v) el[k] = v
|
|
284
|
+
}
|
|
285
|
+
: v => (v == null || (v === false && k[4] != '-') ? el.removeAttribute(k) : el.setAttribute(k, v === true && k[4] != '-' ? '' : /^(href|src|action|formaction|poster)$/.test(k) && typeof v == 'string' ? inBase(v) : v))
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
// Base path (§23 #148): <meta name="brain-base"> in the browser, globalThis.__brainBase while server.mjs renders.
|
|
289
|
+
// A computed app URL ('/posts/7') gets it; literal ones already have it from the build.
|
|
290
|
+
const base = (m = document.querySelector('meta[name=brain-base]')) => globalThis.__brainBase || (m ? m.getAttribute('content') : '/')
|
|
291
|
+
// javascript:, vbscript: and data: (other than data:image/) in a bound URL are dropped and reported (B0213, §31)
|
|
292
|
+
const inBase = (v, b = base()) =>
|
|
293
|
+
/^\s*(javascript|vbscript|data(?!:image\/)):/i.test(v)
|
|
294
|
+
? (oops(Error('B0213 blocked URL scheme')), '')
|
|
295
|
+
: v[0] == '/' && v[1] != '/' && !(v + '/').startsWith(b) ? b + v.slice(1) : v
|
|
296
|
+
|
|
297
|
+
export const cls = (el, k, f) => bind(f, v => el.classList.toggle(k, !!v))
|
|
298
|
+
|
|
299
|
+
// Delegated: walks composedPath (crosses shadow roots). brain-wc.js reuses it in shadow roots (_ev).
|
|
300
|
+
export function _ev(e) {
|
|
301
|
+
const k = '$' + e.type
|
|
302
|
+
let n
|
|
303
|
+
batch(() => {
|
|
304
|
+
for (n of e.composedPath()) {
|
|
305
|
+
const h = n[k]
|
|
306
|
+
if (h) {
|
|
307
|
+
// a data property, not a getter: V8 keeps accessors in the event's shared hidden class, so a getter closure
|
|
308
|
+
// would keep the first event (and its component) alive for the life of the page
|
|
309
|
+
Object.defineProperty(e, 'currentTarget', { configurable: true, value: n })
|
|
310
|
+
try {
|
|
311
|
+
// n moves on with the loop: keep the element for a later rejection
|
|
312
|
+
const r = h.call(n, e), el = n
|
|
313
|
+
if (r && r.then) r.then(null, x => oops(x, el, 'on:' + e.type)) // async handler: a rejected await too
|
|
314
|
+
} catch (x) {
|
|
315
|
+
oops(x, n, 'on:' + e.type)
|
|
316
|
+
}
|
|
317
|
+
if (e.cancelBubble) break
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
export function on(el, t, h) {
|
|
323
|
+
if (/^(focus|blur|mouseenter|mouseleave|load|error|scroll)$/.test(t)) return el.addEventListener(t, e => batch(() => h(e)))
|
|
324
|
+
el['$' + t] = h
|
|
325
|
+
document.addEventListener(t, _ev) // once per type: the DOM ignores the same listener added again
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Indices of array `a` (−1 = new) that form the longest increasing subsequence.
|
|
329
|
+
function lis(a) {
|
|
330
|
+
const p = a.slice(), r = []
|
|
331
|
+
for (let i = 0; i < a.length; i++) {
|
|
332
|
+
const x = a[i]
|
|
333
|
+
if (x < 0) continue
|
|
334
|
+
let lo = 0, hi = r.length
|
|
335
|
+
while (lo < hi) {
|
|
336
|
+
const m = (lo + hi) >> 1
|
|
337
|
+
a[r[m]] < x ? (lo = m + 1) : (hi = m)
|
|
338
|
+
}
|
|
339
|
+
p[i] = lo ? r[lo - 1] : -1
|
|
340
|
+
r[lo] = i
|
|
341
|
+
}
|
|
342
|
+
const f = new Uint8Array(a.length)
|
|
343
|
+
for (let i = r.length ? r[r.length - 1] : -1; i >= 0; i = p[i]) f[i] = 1
|
|
344
|
+
return f
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// render(item, i) receives signals (`item.v`, `i.v`): same key with a new object updates in place.
|
|
348
|
+
export function list(p, f, key, render, end, empty) {
|
|
349
|
+
end = end || mk(p)
|
|
350
|
+
const own = O
|
|
351
|
+
let rows = new Map(), order = [], emp = null
|
|
352
|
+
fx(() => {
|
|
353
|
+
const items = f() || [], me = O
|
|
354
|
+
untrack(() => {
|
|
355
|
+
// Large updates (>500 rows, some already on screen) create new rows in ≤16 ms slices (one frame; QA #302: 5 ms
|
|
356
|
+
// split a 1,000-row update into extra frames): the rest is skipped and this fx re-runs after a yield (INP). A first render (no rows yet) is built at once: slicing it only re-walks the list.
|
|
357
|
+
const first = !order.length, next = [], map = new Map(), pa = end.parentNode, big = items.length > 500 && !first, t0 = big && performance.now()
|
|
358
|
+
let cut = 0
|
|
359
|
+
order.forEach((r, j) => (r.o = j))
|
|
360
|
+
for (let i = 0; i < items.length; i++) {
|
|
361
|
+
const it = items[i], k = key(it, i)
|
|
362
|
+
if (map.has(k)) throw Error('duplicate key in each: ' + k)
|
|
363
|
+
let r = rows.get(k)
|
|
364
|
+
if (r) rows.delete(k), (r.s.v = it), (r.i.v = i)
|
|
365
|
+
else if (cut || (big && performance.now() - t0 > 16)) {
|
|
366
|
+
cut = 1
|
|
367
|
+
continue
|
|
368
|
+
} else r = root(d => {
|
|
369
|
+
const s = sig(it), x = sig(i)
|
|
370
|
+
return { s, i: x, n: nodes(render(s, x)), d, o: -1 }
|
|
371
|
+
}, own)
|
|
372
|
+
next.push(r)
|
|
373
|
+
map.set(k, r)
|
|
374
|
+
}
|
|
375
|
+
if (cut) later(() => (me.m(2), P || B || ((P = 1), queueMicrotask(flush))))
|
|
376
|
+
for (const r of rows.values()) r.d(), rm(r.n)
|
|
377
|
+
if (first && next.length && A) A.p(end, next.flatMap(r => r.n))
|
|
378
|
+
else if (first && next.length) {
|
|
379
|
+
// nothing to keep or move: one fragment, one insertion
|
|
380
|
+
const fr = document.createDocumentFragment()
|
|
381
|
+
for (const r of next) for (const n of r.n) fr.appendChild(n)
|
|
382
|
+
pa.insertBefore(fr, end)
|
|
383
|
+
} else {
|
|
384
|
+
const keep = lis(next.map(r => r.o)), mv = pa.moveBefore && pa.isConnected
|
|
385
|
+
for (let i = next.length - 1, ref = end; i >= 0; i--) {
|
|
386
|
+
const r = next[i]
|
|
387
|
+
// moveBefore keeps focus, iframes, animations and state of moved rows (Chrome 133+); new rows use insertBefore
|
|
388
|
+
if (!keep[i]) for (const n of r.n) mv && r.o >= 0 ? pa.moveBefore(n, ref) : pa.insertBefore(n, ref)
|
|
389
|
+
ref = r.n[0] || ref
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
rows = map
|
|
393
|
+
order = next
|
|
394
|
+
if (!items.length && empty && !emp) emp = root(d => ({ n: place(end, empty()), d }), own)
|
|
395
|
+
else if (items.length && emp) emp.d(), rm(emp.n), (emp = null)
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function when(p, c, a, b, end) {
|
|
401
|
+
end = end || mk(p)
|
|
402
|
+
const t = drv(() => !!c())
|
|
403
|
+
fx(() => {
|
|
404
|
+
const g = t.v ? a : b
|
|
405
|
+
const n = g ? untrack(() => place(end, g())) : []
|
|
406
|
+
return () => rm(n)
|
|
407
|
+
})
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Tests replace services/context: `mount(C, el, props, { provide: [[Api, fake]] })` (needs brain-ch.js).
|
|
411
|
+
export function mount(C, t, props = {}, o) {
|
|
412
|
+
return root(d => {
|
|
413
|
+
const n = nodes(o && o.provide ? V(o.provide, () => C(props)) : C(props))
|
|
414
|
+
for (const x of n) t.appendChild(x)
|
|
415
|
+
return () => (d(), rm(n))
|
|
416
|
+
}, null)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Islands (SPEC §6.1) live in brain-island.js: only pages with islands load it. It receives what it needs from the
|
|
420
|
+
// core instead of importing it (hashed chunks cannot import each other in a cycle).
|
|
421
|
+
export const hydrate = r => import('./brain-island.js').then(m => m.hydrate(r, { mount, _rt }))
|