@christianirack/brainllm-runtime 0.1.0 → 0.4.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/brain-worker.js CHANGED
@@ -3,17 +3,34 @@
3
3
  // plus `const parseCsv = (...a) => w.call('parseCsv', a)`. Messages use structured clone: { id, name, args } →
4
4
  // { id, ok } | { id, err: { message, name, stack } }. The worker starts on the first call; a remote error is rethrown
5
5
  // in the page as an Error with the same message and name (its stack in `remoteStack`).
6
+ const TRANSFER = Symbol('brain-transfer')
7
+ export function transfer(value, list) {
8
+ if (!Array.isArray(list) || list.length > 32 || new Set(list).size !== list.length) throw new TypeError('transfer: provide at most 32 distinct transferable objects')
9
+ return { [TRANSFER]:true, value, list }
10
+ }
6
11
 
7
12
  // Worker side. `name` may be a path into the map ('default.mean'); the result may be a promise.
8
13
  export function serve(map, scope = globalThis) {
14
+ if ('onconnect' in scope) {
15
+ scope.onconnect = event => {
16
+ for (const port of event.ports || []) { serve(map,port); port.start() }
17
+ }
18
+ return
19
+ }
9
20
  scope.onmessage = async e => {
10
21
  const { id, name, args } = e.data || {}
11
22
  try {
23
+ if (!Number.isSafeInteger(id) || id < 1 || typeof name !== 'string' || name.length > 256 || !Array.isArray(args)) throw new TypeError('worker: invalid RPC envelope')
12
24
  let f = map, self
13
- for (const k of String(name).split('.')) (self = f), (f = f == null ? undefined : f[k])
25
+ for (const k of name.split('.')) {
26
+ if (!k || /^(?:__proto__|prototype|constructor)$/.test(k)) throw new TypeError('worker: unsafe export path')
27
+ self = f
28
+ f = f == null ? undefined : Object.getOwnPropertyDescriptor(f, k)?.value
29
+ }
14
30
  if (typeof f !== 'function') throw new TypeError(`worker: \`${name}\` is not an exported function`)
15
31
  const ok = await f.apply(self, args || [])
16
- scope.postMessage({ id, ok }) // throws DataCloneError for a result that cannot be cloned → reported below
32
+ if (ok?.[TRANSFER]) scope.postMessage({id,ok:ok.value},ok.list)
33
+ else scope.postMessage({ id, ok }) // throws DataCloneError for a result that cannot be cloned → reported below
17
34
  } catch (x) {
18
35
  scope.postMessage({ id, err: { message: (x && x.message) || String(x), name: (x && x.name) || 'Error', stack: x && x.stack } })
19
36
  }
@@ -21,39 +38,109 @@ export function serve(map, scope = globalThis) {
21
38
  }
22
39
 
23
40
  // Page side.
24
- export function worker(url) {
25
- let w = null, n = 0
41
+ export function worker(url, options = {}) {
42
+ let w = null, n = 0, inFlight = 0, idle = null
26
43
  const wait = new Map()
44
+ const timeout = Math.max(10, Math.min(60000, Number(options.timeout) || 15000))
45
+ const capacity = Math.max(1, Math.min(256, Number(options.capacity) || 64))
46
+ const maxBytes = Math.max(1024, Math.min(67108864, Number(options.maxBytes) || 4194304))
47
+ const maxInFlight = Math.max(maxBytes, Math.min(67108864, Number(options.maxInFlightBytes) || 8388608))
48
+ function size(value) {
49
+ const seen = new Set(), stack = [[value, 0]]
50
+ let bytes = 0, nodes = 0
51
+ const push = (value, depth) => {
52
+ if (nodes + stack.length >= 4096) throw Object.assign(new Error('worker: payload structure exceeds its budget; transfer a bounded ArrayBuffer instead'), {code:'B0911'})
53
+ stack.push([value, depth])
54
+ }
55
+ while (stack.length) {
56
+ const [v, depth] = stack.pop()
57
+ if (++nodes > 4096 || depth > 24) throw Object.assign(new Error('worker: payload structure exceeds its budget; transfer a bounded ArrayBuffer instead'), { code: 'B0911' })
58
+ if (typeof v === 'string') bytes += v.length * 2
59
+ else if (v && typeof v === 'object' && !seen.has(v)) {
60
+ seen.add(v)
61
+ if (ArrayBuffer.isView(v)) push(v.buffer, depth + 1)
62
+ else if (v instanceof ArrayBuffer || (typeof SharedArrayBuffer === 'function' && v instanceof SharedArrayBuffer)) bytes += v.byteLength
63
+ else if (v instanceof Map) for (const [a,b] of v) { push(a,depth+1); push(b,depth+1) }
64
+ else if (v instanceof Set) for (const a of v) push(a,depth+1)
65
+ else for (const k in v) {
66
+ const d = Object.getOwnPropertyDescriptor(v,k)
67
+ if (!d) continue
68
+ if (!('value' in d)) throw new DOMException('Worker payload accessors are not supported', 'DataCloneError')
69
+ bytes += k.length * 2; push(d.value,depth+1)
70
+ }
71
+ } else bytes += 8
72
+ if (bytes > maxBytes) throw Object.assign(new Error('worker: payload byte budget exceeded; reduce the batch or configure a bounded larger budget'), { code: 'B0911' })
73
+ }
74
+ return bytes
75
+ }
27
76
  const failAll = err => {
28
- for (const [, p] of wait) p[1](err)
77
+ clearTimeout(idle); idle = null
78
+ const old = w
79
+ w = null
80
+ if (old) old.terminate()
81
+ for (const [, p] of wait) { p.clean(); p.fail(err) }
29
82
  wait.clear()
30
- w = null // the next call starts a fresh worker
31
83
  }
32
84
  const start = () => {
33
- w = new Worker(url, { type: 'module' })
85
+ if (globalThis.location) {
86
+ const resolved = new URL(url,location.href)
87
+ if (resolved.origin !== location.origin || resolved.username || resolved.password) throw new TypeError('worker: use a same-origin compiled module URL')
88
+ }
89
+ if (options.shared) {
90
+ if (!globalThis.SharedWorker) throw new Error('SharedWorker is unavailable; use a dedicated worker or explicit tab coordination fallback')
91
+ const shared = new SharedWorker(url, {type:'module',name:options.name}), port = shared.port
92
+ const bridge = { postMessage:(m,t) => port.postMessage(m,t), terminate:() => port.close() }
93
+ port.onmessage = e => bridge.onmessage?.(e)
94
+ port.onmessageerror = e => bridge.onmessageerror?.(e)
95
+ shared.onerror = e => bridge.onerror?.(e)
96
+ port.start()
97
+ w = bridge
98
+ } else w = new Worker(url, { type: 'module' })
99
+ const instance = w
34
100
  w.onmessage = e => {
101
+ if (w !== instance) return
35
102
  const { id, ok, err } = e.data || {}
36
103
  const p = wait.get(id)
37
104
  if (!p) return
38
105
  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)
106
+ p.clean()
107
+ if (!wait.size) {
108
+ idle = setTimeout(() => { if (w === instance && !wait.size) failAll(new Error('worker idle')) }, 30000)
109
+ idle.unref?.()
110
+ }
111
+ if (err) p.fail(Object.assign(new Error(err.message), { name: err.name, remoteStack: err.stack }))
112
+ else p.ok(ok)
41
113
  }
42
114
  w.onerror = e => {
115
+ if (w !== instance) return
43
116
  if (e && e.preventDefault) e.preventDefault()
44
117
  failAll(new Error('worker failed: ' + ((e && e.message) || url)))
45
118
  }
119
+ w.onmessageerror = () => { if (w === instance) failAll(new Error('worker: response could not be decoded')) }
46
120
  }
47
121
  return {
48
- call(name, args) {
49
- if (!w) start()
122
+ call(name, args = [], o = {}) {
123
+ if (o.signal?.aborted) return Promise.reject(Object.assign(new Error('worker call aborted'), { name: 'AbortError', kind: 'aborted' }))
124
+ if (wait.size >= capacity) return Promise.reject(Object.assign(new Error('worker: pending call capacity exceeded; await existing work'), { code: 'B0911' }))
125
+ if (typeof name !== 'string' || name.length > 256 || !Array.isArray(args)) return Promise.reject(new TypeError('worker: invalid call'))
126
+ let bytes
127
+ try { bytes = size(args) } catch (e) { return Promise.reject(e) }
128
+ if (inFlight + bytes > maxInFlight) return Promise.reject(Object.assign(new Error('worker: in-flight byte capacity exceeded; await pending work'), { code:'B0911' }))
129
+ try { if (!w) start() } catch (e) { return Promise.reject(e) }
130
+ clearTimeout(idle); idle = null
50
131
  const id = ++n
51
132
  return new Promise((ok, fail) => {
52
- wait.set(id, [ok, fail])
133
+ const abort = () => failAll(Object.assign(new Error('worker call aborted; shared pending calls cancelled'), { name: 'AbortError', kind: 'aborted' }))
134
+ const timer = setTimeout(() => failAll(Object.assign(new Error('worker: deadline exceeded; move bounded work into smaller batches'), { code: 'B0910' })), timeout)
135
+ inFlight += bytes
136
+ const clean = () => { inFlight -= bytes; clearTimeout(timer); o.signal?.removeEventListener('abort', abort) }
137
+ wait.set(id, { ok, fail, clean })
138
+ o.signal?.addEventListener('abort', abort, { once: true })
53
139
  try {
54
- w.postMessage({ id, name, args })
140
+ w.postMessage({ id, name, args }, o.transfer || [])
55
141
  } catch (x) {
56
142
  wait.delete(id)
143
+ clean()
57
144
  fail(x) // DataCloneError: an argument cannot cross to the worker (functions, DOM nodes…)
58
145
  }
59
146
  })
@@ -64,3 +151,8 @@ export function worker(url) {
64
151
  },
65
152
  }
66
153
  }
154
+
155
+ export function sharedWorker(url, options = {}) {
156
+ if (typeof options.name !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,79}$/.test(options.name)) throw new TypeError('sharedWorker: provide a stable public name of at most 80 characters')
157
+ return worker(url, { ...options, shared:true })
158
+ }
package/brain.js CHANGED
@@ -5,14 +5,14 @@
5
5
  // · r run · q clean · n unlink · d dispose. Short names: brain-min does not rename properties.
6
6
 
7
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
8
+ let L = null, O = null, Q = [], B = 0, P = 0, H = null, E = null, V, A = null, U
9
9
  // Errors (INTERNALS §18): fx, drv and handlers are caught and the app keeps running. E (brain-err.js, _err) records
10
10
  // them and applies per-component fallbacks; without it, console.error.
11
11
  // Errors go to the rich catcher (brain-err) or the console, then to the nearest component boundary (§42, brain-guard.js):
12
12
  // up the owner chain for fx/drv, up the DOM for handlers.
13
13
  const oops = (e, x, w) => {
14
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
15
+ E ? E(e, x, w) : H ? globalThis.reportError ? reportError(e) : console.error(e) : console.error('BRAIN_ERROR: Operation failed. Try again.')
16
16
  for (let o = x; o; o = o.nodeType ? o.parentNode : o.p) {
17
17
  const g = o.$g
18
18
  if (g) return g(e, w)
@@ -155,7 +155,7 @@ function flush() {
155
155
  }
156
156
  }
157
157
 
158
- export const sig = (v, k) => (k && H ? H(new N(v, null, 0), k) : new N(v, null, 0))
158
+ export const sig = (v, k) => (U && U('signals', O, 1), k && H ? H(new N(v, null, 0), k) : new N(v, null, 0))
159
159
  export const drv = f => new N(undefined, f, 0)
160
160
  export const fx = f => {
161
161
  const n = new N(undefined, f, 1)
@@ -201,7 +201,7 @@ export const rpc = (id, a) => import('./brain-rpc.js').then(m => m.rpc(id, a, oo
201
201
  // Internals for sibling modules (brain-err, brain-ch): root, current owner and node helpers; _err installs the hook;
202
202
  // brain-ch passes its provider for `mount(C, t, props, { provide })`.
203
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) })
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), development: () => !!H, budget: f => (U = f) })
205
205
  export const _err = h => ((E = h), _rt())
206
206
 
207
207
  // ---------- DOM ----------
@@ -278,7 +278,7 @@ export function attr(el, k, f) {
278
278
  f,
279
279
  k.startsWith('style:')
280
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)
281
+ : /^(value|checked|selected)$/.test(k) && !(k == 'value' && el.tagName == 'OPTION') // an option's value is its attribute (SSR, value="")
282
282
  ? v => {
283
283
  if (el[k] !== v) el[k] = v
284
284
  }
@@ -351,6 +351,7 @@ export function list(p, f, key, render, end, empty) {
351
351
  let rows = new Map(), order = [], emp = null
352
352
  fx(() => {
353
353
  const items = f() || [], me = O
354
+ if (U) U('rows', me, items.length)
354
355
  untrack(() => {
355
356
  // Large updates (>500 rows, some already on screen) create new rows in ≤16 ms slices (one frame; QA #302: 5 ms
356
357
  // 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.
package/package.json CHANGED
@@ -1,13 +1,57 @@
1
1
  {
2
2
  "name": "@christianirack/brainllm-runtime",
3
- "version": "0.1.0",
4
- "description": "The BRAINLLM runtime: dependency-free ES modules (signals, DOM, router, data, forms, channels). The brain binary embeds this same version.",
3
+ "version": "0.4.0",
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
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./brain.js",
8
8
  "./*": "./*"
9
9
  },
10
10
  "sideEffects": false,
11
+ "keywords": [
12
+ "runtime",
13
+ "zero-dependency",
14
+ "dom",
15
+ "router",
16
+ "llm",
17
+ "ai",
18
+ "ai-agents",
19
+ "coding-agent",
20
+ "agentic",
21
+ "claude-code",
22
+ "cursor",
23
+ "mcp",
24
+ "model-context-protocol",
25
+ "frontend",
26
+ "front-end",
27
+ "backend",
28
+ "back-end",
29
+ "fullstack",
30
+ "full-stack",
31
+ "cli",
32
+ "command-line",
33
+ "developer-tools",
34
+ "design-system",
35
+ "css",
36
+ "testing",
37
+ "e2e",
38
+ "postgresql",
39
+ "jwt",
40
+ "web-framework",
41
+ "framework",
42
+ "compiler",
43
+ "rust",
44
+ "fast",
45
+ "performance",
46
+ "react-alternative",
47
+ "vite-alternative",
48
+ "signals",
49
+ "reactive",
50
+ "components",
51
+ "ssr",
52
+ "spa",
53
+ "web"
54
+ ],
11
55
  "license": "MIT",
12
56
  "author": "Christian Irack Flores Maldonado <christian@irack.mx> (https://ingenieria.dev)",
13
57
  "repository": {