@christianirack/brainllm-runtime 0.0.0-stage → 0.3.1

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -2
  3. package/brain-ai.js +129 -0
  4. package/brain-anim.js +21 -0
  5. package/brain-ch.js +254 -0
  6. package/brain-consent.js +38 -0
  7. package/brain-data.js +549 -0
  8. package/brain-dev.js +100 -0
  9. package/brain-env.js +160 -0
  10. package/brain-err.js +242 -0
  11. package/brain-form.js +275 -0
  12. package/brain-guard.js +242 -0
  13. package/brain-hydrate.js +72 -0
  14. package/brain-i18n.js +111 -0
  15. package/brain-inspect.js +745 -0
  16. package/brain-island.js +106 -0
  17. package/brain-lazy.js +88 -0
  18. package/brain-mcp.js +73 -0
  19. package/brain-mock.js +80 -0
  20. package/brain-overlay.js +123 -0
  21. package/brain-persist.js +65 -0
  22. package/brain-router.js +503 -0
  23. package/brain-rpc.js +20 -0
  24. package/brain-schema.js +93 -0
  25. package/brain-wc.js +69 -0
  26. package/brain-why.js +198 -0
  27. package/brain-worker.js +66 -0
  28. package/brain.js +421 -0
  29. package/package.json +20 -4
  30. package/server/server.mjs +1046 -0
  31. package/shim/dom.js +448 -0
  32. package/shim/probe.js +111 -0
  33. package/sw/brain-sw.js +103 -0
  34. package/themes/contrast.cssbrain +39 -0
  35. package/themes/dark.cssbrain +39 -0
  36. package/themes/glass.cssbrain +39 -0
  37. package/themes/paper.cssbrain +39 -0
  38. package/ui/button.brain +31 -0
  39. package/ui/center.brain +11 -0
  40. package/ui/checkbox.brain +13 -0
  41. package/ui/container.brain +11 -0
  42. package/ui/dialog.brain +29 -0
  43. package/ui/error-state.brain +53 -0
  44. package/ui/field.brain +34 -0
  45. package/ui/grid.brain +11 -0
  46. package/ui/input.brain +23 -0
  47. package/ui/lang-switch.brain +38 -0
  48. package/ui/menu.brain +64 -0
  49. package/ui/row.brain +12 -0
  50. package/ui/select.brain +29 -0
  51. package/ui/sidebar.brain +17 -0
  52. package/ui/spacer.brain +12 -0
  53. package/ui/stack.brain +10 -0
  54. package/ui/table.brain +73 -0
  55. package/ui/tabs.brain +33 -0
  56. package/ui/toast.brain +45 -0
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) && !(k == 'value' && el.tagName == 'OPTION') // an option's value is its attribute (SSR, value="")
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 }))
package/package.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "name": "@christianirack/brainllm-runtime",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.1",
4
+ "description": "The BRAINLLM runtime for the Rust-built web compiler for LLMs: dependency-free ES modules (signals, DOM, router, data, forms, channels). The brain binary embeds this same version.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./brain.js",
8
+ "./*": "./*"
9
+ },
10
+ "sideEffects": false,
11
+ "license": "MIT",
12
+ "author": "Christian Irack Flores Maldonado <christian@irack.mx> (https://ingenieria.dev)",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/chrisifm/BRAINLLM.git"
16
+ },
17
+ "homepage": "https://chrisifm.github.io/BRAINLLM/",
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "registry": "https://registry.npmjs.org/"
21
+ }
22
+ }