@christianirack/brainllm-runtime 0.3.1 → 0.5.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-form.js CHANGED
@@ -15,7 +15,7 @@
15
15
  // Unsaved changes (§41.5): `f.dirty` is true while values differ from the last saved ones (init, reset() or a successful
16
16
  // submit); leaving the page (client navigation or closing the tab) then asks first. No API.
17
17
  import { sig, fx, batch, untrack, _rt } from './brain.js'
18
- import { rule, check, msg } from './brain-schema.js'
18
+ import { rule, check, checkText, checkScalar, msg } from './brain-schema.js'
19
19
 
20
20
  export { rule, check } // server.mjs validates posted forms with the same rules (§38)
21
21
 
@@ -41,9 +41,18 @@ if (globalThis.addEventListener && !globalThis.__brainDirtyHook) {
41
41
  for (const f of DIRTY) if (f.dirty && !f.busy) return void (e.preventDefault(), (e.returnValue = ''))
42
42
  })
43
43
  }
44
- const empty = v => v == null || v === '' || (Array.isArray(v) && !v.length)
44
+ const empty = v => v == null || typeof v === 'string' && v.trim() === '' || Array.isArray(v) && !v.length
45
45
  const init0 = r => (r.list ? [] : r.t === 'bool' ? false : '')
46
- const out = (r, v) => (r.list ? v || [] : r.t === 'num' ? (empty(v) ? null : Number(v)) : r.t === 'bool' ? !!v : v)
46
+ const out = (r, v) => {
47
+ if (r.list) {
48
+ if (!Array.isArray(v)) return []
49
+ if (r.t !== 'num') return v
50
+ const numbers = new Array(v.length)
51
+ for (let i = 0; i < v.length; i++) numbers[i] = Number(v[i])
52
+ return numbers
53
+ }
54
+ return r.t === 'num' ? (empty(v) ? null : Number(v)) : r.t === 'bool' ? !!v : v
55
+ }
47
56
 
48
57
  // state left by the server: globalThis.__brainForms while it renders, or the page's #brain-forms JSON
49
58
  let SEEN
@@ -62,6 +71,46 @@ function posted(id) {
62
71
  }
63
72
 
64
73
  export function form(schema, o = {}) {
74
+ if (rule(schema).t !== 'obj') throw Error('form: schema must be a nested field object')
75
+ return createForm(schema, o, rule, check, remoteCheck)
76
+ }
77
+ // Compiler-only flat literal schema bytecode. Dynamic/public form() keeps the full parser.
78
+ export function formStatic(schema, o = {}) {
79
+ return compiledForm(schema, o, ['str', 'email'], checkText, remoteCheck)
80
+ }
81
+ export function formScalars(schema, o = {}) {
82
+ return compiledForm(schema, o, ['str', 'email', 'bool', 'num'], checkScalar, remoteCheck)
83
+ }
84
+ // Compiler-only options proven not to configure or dynamically install async checks.
85
+ export function formStaticSync(schema, o = {}) {
86
+ return compiledForm(schema, o, ['str', 'email'], checkText, null)
87
+ }
88
+ export function formScalarsSync(schema, o = {}) {
89
+ return compiledForm(schema, o, ['str', 'email', 'bool', 'num'], checkScalar, null)
90
+ }
91
+ function compiledForm(schema, o, types, validate, remote) {
92
+ if (remote === null && ('check' in o || 'validate' in o)) throw Error('form: invalid compiled synchronous options')
93
+ if (!schema || Object.getPrototypeOf(schema) !== Object.prototype || Object.keys(schema).length > 4096) throw Error('form: invalid compiled schema')
94
+ return createForm(schema, o, (value, key) => {
95
+ if (['__proto__', 'prototype', 'constructor'].includes(key) || !Array.isArray(value) || value.length !== 2 || !types.includes(value[0]) || typeof value[1] !== 'boolean') throw Error('form: invalid compiled schema')
96
+ return { t: value[0], req: value[1], list: false, min: null, max: null }
97
+ }, validate, remote)
98
+ }
99
+ // Only public/dynamic or check-configured forms retain this optional capability.
100
+ function remoteCheck(o, x, checking) {
101
+ const c = o.check && o.check[x.path], v = untrack(() => x.s.v)
102
+ if (!c || untrack(() => x.e.v)) return Promise.resolve(untrack(() => x.e.v))
103
+ checking.v++
104
+ return Promise.resolve()
105
+ .then(() => c(v))
106
+ .then(m => m || '', e => (e && e.kind === 'aborted' ? '' : (e && e.message) || msg('form.check_failed', 'Could not check this value.')))
107
+ .then(m => {
108
+ if (untrack(() => x.s.v) === v) x.e.v = m
109
+ return untrack(() => x.e.v)
110
+ })
111
+ .finally(() => checking.v--)
112
+ }
113
+ function createForm(schema, o, readRule, validateRule, remote) {
65
114
  const id = 'f' + ++seq, owner = I.owner() // aria effects are created later (in handlers) but live with the component
66
115
  const busy = sig(false), error = sig(''), shown = sig(0) // shown: bumped when touched set changes
67
116
  const checking = sig(0), saved = sig('') // async checks running · JSON of the last saved values
@@ -72,7 +121,7 @@ export function form(schema, o = {}) {
72
121
  function build(sch, base, init) {
73
122
  const v = {}, err = {}
74
123
  for (const k of Object.keys(sch)) {
75
- const r = rule(sch[k]), path = base ? base + '.' + k : k
124
+ const r = readRule(sch[k], k), path = base ? base + '.' + k : k
76
125
  if (r.t === 'obj') {
77
126
  const sub = build(r.s, path, (init && init[k]) || {})
78
127
  Object.defineProperty(v, k, { enumerable: true, value: sub.v })
@@ -115,23 +164,9 @@ export function form(schema, o = {}) {
115
164
  const touch = x => {
116
165
  validate(x)
117
166
  if (!x.touched) (x.touched = true), shown.v++
118
- remote(x)
119
- }
120
- // async check of one field (only when its own rule passes); resolves to its message
121
- const remote = x => {
122
- const c = o.check && o.check[x.path], v = untrack(() => x.s.v)
123
- if (!c || untrack(() => x.e.v)) return Promise.resolve(untrack(() => x.e.v))
124
- checking.v++
125
- return Promise.resolve()
126
- .then(() => c(v))
127
- .then(m => m || '', e => (e && e.kind === 'aborted' ? '' : (e && e.message) || msg('form.check_failed', 'Could not check this value.')))
128
- .then(m => {
129
- if (untrack(() => x.s.v) === v) x.e.v = m // a stale answer (the value changed meanwhile) is ignored
130
- return untrack(() => x.e.v)
131
- })
132
- .finally(() => checking.v--)
167
+ if (remote) remote(o, x, checking)
133
168
  }
134
- const validate = x => (x.e.v = untrack(() => check(x.r, x.s.v)))
169
+ const validate = x => (x.e.v = untrack(() => validateRule(x.r, x.s.v)))
135
170
  const fid = p => `${id}-${p.replace(/\./g, '-')}-err`
136
171
 
137
172
  // A form posted without JS (§38) comes back from the server with its values and messages: start from them, already
@@ -196,7 +231,7 @@ export function form(schema, o = {}) {
196
231
  return checking.v > 0
197
232
  },
198
233
  get ok() {
199
- for (const x of F) if (check(x.r, x.s.v)) return false
234
+ for (const x of F) if (validateRule(x.r, x.s.v)) return false
200
235
  return true
201
236
  },
202
237
  id: path => fid(path),
@@ -244,8 +279,8 @@ export function form(schema, o = {}) {
244
279
  busy.v = true
245
280
  if (fm) fm.setAttribute('aria-busy', 'true') // assistive tech hears the form is working
246
281
  try {
247
- if (o.check) {
248
- await Promise.all(F.map(remote))
282
+ if (remote && o.check) {
283
+ await Promise.all(F.map(x => remote(o, x, checking)))
249
284
  first = F.find(x => untrack(() => x.e.v))
250
285
  if (first) return void (shown.v++, first.el && first.el.focus && first.el.focus())
251
286
  }
package/brain-hash.js ADDED
@@ -0,0 +1,17 @@
1
+ // Web Crypto owns cryptography in browsers and Node. No hashing implementation is bundled.
2
+ export async function hashText(value, algorithm = 'SHA-256') {
3
+ if (!['SHA-256', 'SHA-384', 'SHA-512'].includes(algorithm)) throw RangeError('hashText: SHA-256, SHA-384 or SHA-512 required')
4
+ if (typeof value !== 'string') throw TypeError('hashText: text input required')
5
+ if (value.length > 8388608) throw RangeError('hashText: input exceeds 8 MiB')
6
+ const bytes = new TextEncoder().encode(value)
7
+ if (bytes.byteLength > 8388608) throw RangeError('hashText: UTF-8 input exceeds 8 MiB')
8
+ if (!globalThis.crypto?.subtle) throw Error('hashText: Web Crypto requires a secure browser context or Node 22+')
9
+ const digest = new Uint8Array(await globalThis.crypto.subtle.digest(algorithm, bytes))
10
+ let hex = ''
11
+ for (let i = 0; i < digest.length; i++) hex += digest[i].toString(16).padStart(2, '0')
12
+ return hex
13
+ }
14
+ export async function shortHash(value, length = 32) {
15
+ if (!Number.isInteger(length) || length < 8 || length > 64) throw RangeError('shortHash: length must be 8..64 hex characters')
16
+ return (await hashText(value)).slice(0, length)
17
+ }
package/brain-inspect.js CHANGED
@@ -16,8 +16,8 @@ const now = () => Date.now()
16
16
  const pnow = () => (G.performance ? performance.now() : now())
17
17
  const syncHeap = () => (G.performance && performance.memory ? performance.memory.usedJSHeapSize : G.process && process.memoryUsage ? process.memoryUsage().heapUsed : null)
18
18
 
19
- let out = [], seq = 0, opts = {}, started = false
20
- const push = r => out.push(r)
19
+ let out = [], seq = 0, opts = {}, started = false, dropped = 0
20
+ const push = r => { if (out.length >= 2000) { out.shift(); dropped++ } out.push(r) }
21
21
 
22
22
  // ---------- instance identity and current owner ----------
23
23
  const ids = new WeakMap()
@@ -612,6 +612,8 @@ export function tick() {
612
612
  export function drain() {
613
613
  if (!out.length) return null
614
614
  const m = { t: 'm', v: 2, seq: seq++, at: now(), d: out }
615
+ if (dropped) m.dropped = dropped
616
+ dropped = 0
615
617
  out = []
616
618
  if (opts.send) opts.send(JSON.stringify(m))
617
619
  return m
@@ -629,7 +631,7 @@ export function inspect(o) {
629
631
  else i = instOf(x)
630
632
  if (!i) return null
631
633
  const chain = []
632
- for (let j = i; j; j = instOf(j.p)) chain.push(nameOf(j))
634
+ for (let j = i; j && chain.length < 33; j = instOf(j.p)) chain.push(nameOf(j))
633
635
  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
636
  },
635
637
  sigName,
@@ -643,7 +645,7 @@ export function inspect(o) {
643
645
  if (done) done.add(key)
644
646
  push({ k: 'chan', kind, name, role, id: i ? idOf(i) : null, h: i ? i.h : null, comp: i ? nameOf(i) : null })
645
647
  })
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 }))
648
+ 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, brainStack: r.brainStack || null, location: r.location, generatedLocation: r.generatedLocation, frames: r.frames, causes: r.causes, truncated: r.truncated, mapping: r.mapping, crumbs: r.crumbs, n: r.n, at: r.last }))
647
649
  patchTimers()
648
650
  patchGlobalListeners()
649
651
  patchHandlers()
@@ -0,0 +1,50 @@
1
+ // Explicit foreign-DOM ownership. The adapter, SDK and any React runtime belong to the app.
2
+ import { _rt, fx, untrack } from './brain.js'
3
+
4
+ const I = _rt()
5
+
6
+ // Call during component setup. Resolve the bound element only after insertion.
7
+ // mount must synchronously return a cleanup function or { update?, destroy }.
8
+ export function interop(element, adapter, props = {}, options = {}) {
9
+ const owner = I.owner()
10
+ if (!owner) throw Error('interop: call during component setup')
11
+ let alive = true, instance = null, pending = props, ready = false
12
+ const report = (error, phase) => {
13
+ if (typeof options.onError === 'function') options.onError(error, phase)
14
+ else I.oops(error, owner, 'interop:' + phase)
15
+ }
16
+ const destroy = () => {
17
+ if (!alive) return
18
+ alive = false
19
+ const old = instance
20
+ instance = null
21
+ if (old) try { old.destroy() } catch (error) { report(error, 'destroy') }
22
+ }
23
+ const controller = {
24
+ update(next) {
25
+ if (!alive) throw Error('interop: boundary has been destroyed')
26
+ pending = next
27
+ if (!ready) return
28
+ if (!instance || typeof instance.update !== 'function') throw Error('interop: adapter does not support updates')
29
+ try { untrack(() => instance.update(next)) } catch (error) { report(error, 'update') }
30
+ },
31
+ destroy,
32
+ }
33
+ // No SDK execution during SSR or prerender. Disposal still invalidates this controller.
34
+ fx(() => destroy)
35
+ if (globalThis.__brainSSR || globalThis.__brainRoute) return controller
36
+ queueMicrotask(() => {
37
+ if (!alive) return
38
+ try {
39
+ const el = typeof element === 'function' ? untrack(element) : element
40
+ if (!el || el.nodeType !== 1) throw Error('interop: expected a bound DOM element')
41
+ if (!adapter || typeof adapter.mount !== 'function') throw Error('interop: adapter.mount is required')
42
+ const result = untrack(() => adapter.mount(el, pending))
43
+ if (typeof result === 'function') instance = { destroy: result }
44
+ else if (result && typeof result.destroy === 'function' && typeof result.then !== 'function') instance = result
45
+ else throw Error('interop: mount must return cleanup or { update, destroy }; asynchronous mounts are unsupported')
46
+ ready = true
47
+ } catch (error) { report(error, 'mount') }
48
+ })
49
+ return controller
50
+ }
package/brain-live.js ADDED
@@ -0,0 +1,364 @@
1
+ // Native, same-origin invalidation metadata. HTTP resource reads remain authoritative.
2
+ import { sig, fx, untrack, _rt } from './brain.js'
3
+
4
+ const G = globalThis, I = _rt()
5
+ const MAX_OWNERS = 128, MAX_ROOMS = 32, FRAME = 4096, BUFFER = 32768
6
+ const COALESCE = 100, READY_MS = 8000, REFRESH_MS = 10000, RETRY_MS = 60000
7
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
8
+ const own = (value, key) => Object.prototype.hasOwnProperty.call(value, key)
9
+ const RESERVED = new Set(['constructor', 'prototype', '__proto__', 'id', 'owner', 'created_at', 'updated_at', 'search'])
10
+ let broker
11
+ const fault = fix => Object.assign(Error('B0918: ' + fix), { code: 'B0918', fix })
12
+ const fail = fix => { throw fault(fix) }
13
+ const exact = (value, keys) => value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).every(key => keys.includes(key))
14
+ const resourceName = name => typeof name === 'string' && /^[a-z][a-z0-9_]{0,47}$/.test(name) && !RESERVED.has(name)
15
+
16
+ function roomsOf(value) {
17
+ if (!Array.isArray(value) || value.length > MAX_ROOMS) fail('Use at most 32 native resource rooms')
18
+ const rooms = new Map()
19
+ for (const room of value) {
20
+ let command, key
21
+ const id = room && typeof room === 'object' && own(room, 'id') ? room.id : undefined
22
+ if (exact(room, ['room']) && own(room, 'room') && room.room === 'user:self') {
23
+ command = { room: 'user:self' }; key = 'user:self'
24
+ } else if (exact(room, ['resource', 'id']) && own(room, 'resource') && resourceName(room.resource) && (id === undefined || (typeof id === 'string' && UUID.test(id)))) {
25
+ command = id === undefined ? { resource: room.resource } : { resource: room.resource, id: id.toLowerCase() }
26
+ key = command.resource + ':' + (command.id || '')
27
+ } else fail('Use a declared resource, an optional UUID, or the user:self room')
28
+ rooms.set(key, Object.freeze(command))
29
+ }
30
+ return rooms
31
+ }
32
+
33
+ function endpoint() {
34
+ const location = G.location
35
+ if (!location || !/^https?:$/.test(location.protocol)) fail('Serve live resources from an HTTP(S) application origin')
36
+ const meta = G.document && document.querySelector('meta[name=brain-base]')
37
+ const base = meta && meta.getAttribute('content') || ''
38
+ if (typeof base !== 'string' || base.length > 512 || (base && (!base.startsWith('/') || base.startsWith('//') || /[\\?#\x00-\x20\x7f]/.test(base)))) fail('Use a normalized same-origin application base path')
39
+ const origin = new URL(location.origin)
40
+ if (origin.origin !== location.origin || origin.username || origin.password) fail('Use a canonical application origin')
41
+ const normalized = base.replace(/\/$/, '')
42
+ const url = new URL(normalized + '/_brain/socket', origin)
43
+ if (url.origin !== origin.origin || url.pathname !== normalized + '/_brain/socket') fail('Use a normalized same-origin application base path')
44
+ url.protocol = origin.protocol === 'https:' ? 'wss:' : 'ws:'
45
+ return url.href
46
+ }
47
+
48
+ function metadata(text) {
49
+ if (typeof text !== 'string' || text.length > FRAME || /[^\x09\x0a\x0d\x20-\x7e]/.test(text)) return null
50
+ let value
51
+ try {
52
+ value = JSON.parse(text, (key, item) => {
53
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') throw Error('Unsafe metadata')
54
+ return item
55
+ })
56
+ } catch { return null }
57
+ if (exact(value, ['type', 'protocol', 'durable', 'resyncRequired']) && value.type === 'ready' && value.protocol === 1 && value.durable === false && value.resyncRequired === true) return Object.freeze(value)
58
+ if (exact(value, ['type', 'reason']) && value.type === 'resync' && value.reason === 'joined') return Object.freeze(value)
59
+ if (exact(value, ['type']) && value.type === 'pong') return Object.freeze(value)
60
+ if (exact(value, ['type', 'code']) && value.type === 'error' && value.code === 'ROOM_FORBIDDEN') return Object.freeze(value)
61
+ if (exact(value, ['type', 'resource', 'id', 'operation', 'eventId']) && value.type === 'invalidate' && resourceName(value.resource) && typeof value.id === 'string' && UUID.test(value.id) && typeof value.eventId === 'string' && UUID.test(value.eventId) && ['create', 'update', 'delete'].includes(value.operation)) return Object.freeze(value)
62
+ return null
63
+ }
64
+
65
+ function createBroker() {
66
+ const members = new Set(), joined = new Map(), waiting = [], events = new Map()
67
+ let socket = null, epoch = 0, scope = null, terminal = '', ready = false
68
+ let reconcileQueued = false, retryTimer, readyTimer, heartbeat, pongTimer, flushTimer
69
+ let attempts = 0, retryStart = 0, lastConnect = 0
70
+
71
+ const update = () => {
72
+ for (const member of members) {
73
+ const active = [...member.rooms.keys()].some(key => joined.get(key) === 'ready')
74
+ const forbidden = member.rooms.size > 0 && [...member.rooms.keys()].every(key => joined.get(key) === 'forbidden')
75
+ member.up.v = !!(socket && ready && member.scope && member.scope === scope && active)
76
+ member.state.v = !member.scope || !member.rooms.size ? 'idle' : terminal || (member.refreshState || (forbidden ? 'forbidden' : member.up.v ? 'open' : retryTimer ? 'retrying' : 'connecting'))
77
+ }
78
+ }
79
+ const clearMember = member => {
80
+ member.generation++
81
+ member.dirty = false
82
+ member.pending = member.resync = null
83
+ member.refreshState = member.flight ? 'refresh-pending' : ''
84
+ if (member.flight) {
85
+ const flight = member.flight
86
+ clearTimeout(flight.deadline); clearTimeout(flight.tick)
87
+ flight.deadline = flight.tick = null
88
+ if (flight.resolveTick) { flight.resolveTick(); flight.resolveTick = null }
89
+ }
90
+ }
91
+ const stop = () => {
92
+ epoch++; ready = false
93
+ clearTimeout(retryTimer); retryTimer = null
94
+ clearTimeout(readyTimer); clearInterval(heartbeat); clearTimeout(pongTimer)
95
+ readyTimer = heartbeat = pongTimer = null
96
+ clearTimeout(flushTimer); flushTimer = null
97
+ joined.clear(); waiting.length = 0; events.clear(); currentRooms.clear()
98
+ const previous = socket; socket = null
99
+ if (previous) {
100
+ previous.onopen = previous.onmessage = previous.onclose = previous.onerror = null
101
+ try { previous.close(1000, 'Live resource lifecycle ended') } catch {}
102
+ }
103
+ for (const member of members) { clearMember(member); member.up.v = false }
104
+ }
105
+ const reject = state => { stop(); terminal = state; update() }
106
+ const send = value => {
107
+ if (!socket || socket.readyState !== 1 || socket.bufferedAmount > BUFFER) return false
108
+ const text = JSON.stringify(value)
109
+ if (text.length > FRAME) return false
110
+ try { socket.send(text); return true } catch { return false }
111
+ }
112
+ const refresh = member => {
113
+ if (!members.has(member) || !member.up.v || !member.dirty || member.flight) return
114
+ member.dirty = false; member.refreshState = ''; update()
115
+ const pending = member.pending, resync = member.resync
116
+ member.pending = member.resync = null
117
+ const flight = { generation: member.generation, epoch, deadline: null, tick: null, resolveTick: null }
118
+ member.flight = flight
119
+ flight.deadline = setTimeout(timeoutFlight.bind(null, member.cell, flight), REFRESH_MS)
120
+ try {
121
+ if (resync && own(member.options, 'onResync') && member.options.onResync) observeMetadata(member.options.onResync, resync)
122
+ if (pending && own(member.options, 'onInvalidate') && member.options.onInvalidate) observeMetadata(member.options.onInvalidate, pending)
123
+ untrack(() => member.node.refetch())
124
+ // Equivalent post-effect tick, with an owned cancellation handle. The
125
+ // standard tick() intentionally exposes no handle for owner cancellation.
126
+ const next = new Promise(resolve => {
127
+ flight.resolveTick = resolve
128
+ flight.tick = setTimeout(resolve, 0)
129
+ })
130
+ next.then(readResource.bind(null, member.cell, flight)).then(
131
+ settleFlight.bind(null, member.cell, flight, false),
132
+ settleFlight.bind(null, member.cell, flight, true),
133
+ )
134
+ } catch {
135
+ I.oops(fault('Keep live metadata observers synchronous and handle resource failures in the view'), member.owner, 'B0918')
136
+ dispose(member.cell)
137
+ }
138
+ }
139
+ const valid = (member, flight) => members.has(member) && member.generation === flight.generation && epoch === flight.epoch
140
+ function complete(member, flight, failed) {
141
+ if (member.flight !== flight) return
142
+ clearTimeout(flight.deadline); clearTimeout(flight.tick)
143
+ flight.deadline = flight.tick = flight.resolveTick = null
144
+ member.flight = null; member.refreshState = ''
145
+ if (valid(member, flight) && failed) member.refreshState = 'refresh-error'
146
+ if (members.has(member) && member.dirty) schedule()
147
+ if (members.has(member)) update()
148
+ }
149
+ function schedule() {
150
+ if (!flushTimer) flushTimer = setTimeout(() => {
151
+ flushTimer = null
152
+ for (const member of members) refresh(member)
153
+ }, COALESCE)
154
+ }
155
+ const invalidate = (value, roomKey) => {
156
+ for (const member of members) {
157
+ if (!member.scope || member.scope !== scope) continue
158
+ const interested = roomKey ? member.rooms.has(roomKey) : [...member.rooms].some(([key, room]) => joined.get(key) === 'ready' && (room.room === 'user:self' || (room.resource === value.resource && (!room.id || room.id === value.id))))
159
+ if (interested) { member.dirty = true; if (value.type === 'resync') member.resync = value; else member.pending = value; schedule() }
160
+ }
161
+ }
162
+ const retry = () => {
163
+ stop()
164
+ if (++attempts > 5 || (retryStart && Date.now() - retryStart >= RETRY_MS)) { terminal = 'exhausted'; update(); return }
165
+ if (!retryStart) retryStart = Date.now()
166
+ const delay = Math.floor(Math.min(8000, 250 * 2 ** (attempts - 1)) * (0.75 + Math.random() * 0.25))
167
+ retryTimer = setTimeout(() => { retryTimer = null; reconcile() }, delay)
168
+ update()
169
+ }
170
+ const desired = () => {
171
+ const rooms = new Map(), scopes = new Set()
172
+ for (const member of members) if (member.scope && member.rooms.size) {
173
+ scopes.add(member.scope)
174
+ for (const [key, value] of member.rooms) rooms.set(key, value)
175
+ }
176
+ return { rooms, scopes }
177
+ }
178
+ const syncRooms = rooms => {
179
+ const changed = [...rooms.keys()].some(key => !joined.has(key)) || [...joined.keys()].some(key => !rooms.has(key))
180
+ if (waiting.length && changed) { retry(); return }
181
+ for (const [key] of joined) if (!rooms.has(key)) {
182
+ const room = currentRooms.get(key)
183
+ if (!room || !send({ type: 'leave', ...room })) { retry(); return }
184
+ joined.delete(key)
185
+ }
186
+ currentRooms = rooms
187
+ for (const [key, room] of rooms) if (!joined.has(key)) {
188
+ if (waiting.length >= MAX_ROOMS || !send({ type: 'join', ...room })) { retry(); return }
189
+ joined.set(key, 'pending'); waiting.push(key)
190
+ }
191
+ update()
192
+ }
193
+ let currentRooms = new Map()
194
+ const connect = () => {
195
+ const captured = ++epoch
196
+ lastConnect = Date.now()
197
+ let address
198
+ try { address = endpoint() } catch { reject('invalid-config'); return }
199
+ try { socket = new G.WebSocket(address, 'brain.v1') } catch { retry(); return }
200
+ readyTimer = setTimeout(() => { if (epoch === captured) retry() }, READY_MS)
201
+ socket.onopen = () => {
202
+ if (epoch !== captured) return
203
+ if (socket.protocol !== 'brain.v1') reject('protocol-error')
204
+ }
205
+ socket.onerror = () => { /* close or the ready deadline owns retries; arbitrary browser errors are not retained. */ }
206
+ socket.onclose = event => {
207
+ if (epoch !== captured) return
208
+ if (event.code === 1008) reject(event.reason === 'Authentication expired' ? 'auth-required' : 'policy-closed')
209
+ else retry()
210
+ }
211
+ socket.onmessage = event => {
212
+ if (epoch !== captured) return
213
+ const value = metadata(event.data)
214
+ if (!value) { reject('protocol-error'); return }
215
+ if (value.type === 'ready') {
216
+ if (ready) { reject('protocol-error'); return }
217
+ clearTimeout(readyTimer); readyTimer = null; ready = true
218
+ syncRooms(desired().rooms)
219
+ heartbeat = setInterval(() => {
220
+ if (!send({ type: 'ping' })) { retry(); return }
221
+ clearTimeout(pongTimer)
222
+ pongTimer = setTimeout(() => { if (epoch === captured) retry() }, 12000)
223
+ }, 25000)
224
+ } else if (!ready) reject('protocol-error')
225
+ else if (value.type === 'pong') { clearTimeout(pongTimer); pongTimer = null }
226
+ else if (value.type === 'resync' || value.type === 'error') {
227
+ const key = waiting.shift()
228
+ if (!key) { reject('protocol-error'); return }
229
+ joined.set(key, value.type === 'resync' ? 'ready' : 'forbidden')
230
+ if (value.type === 'resync') { attempts = 0; retryStart = 0 }
231
+ update()
232
+ if (value.type === 'resync') invalidate(value, key)
233
+ } else {
234
+ const now = Date.now()
235
+ for (const [id, time] of events) if (now - time > 60000) events.delete(id)
236
+ if (events.has(value.eventId)) return
237
+ events.set(value.eventId, now)
238
+ while (events.size > 128) events.delete(events.keys().next().value)
239
+ invalidate(value)
240
+ }
241
+ }
242
+ update()
243
+ }
244
+ function reconcile() {
245
+ reconcileQueued = false
246
+ const { rooms, scopes } = desired()
247
+ if (!rooms.size) { stop(); scope = null; terminal = ''; attempts = 0; retryStart = 0; update(); return }
248
+ if (scopes.size !== 1) { reject('scope-conflict'); return }
249
+ const next = scopes.values().next().value
250
+ if (next !== scope || terminal === 'scope-conflict') { stop(); scope = next; terminal = ''; attempts = 0; retryStart = 0 }
251
+ if (rooms.size > MAX_ROOMS) { reject('room-limit'); return }
252
+ if (terminal || retryTimer) { update(); return }
253
+ if (socket && ready) syncRooms(rooms)
254
+ else if (!socket) {
255
+ const elapsed = Date.now() - lastConnect
256
+ const delay = elapsed < 0 ? 0 : Math.max(0, 250 - elapsed)
257
+ if (delay) retryTimer = setTimeout(() => { retryTimer = null; reconcile() }, delay)
258
+ else connect()
259
+ }
260
+ update()
261
+ }
262
+ const queue = () => { if (!reconcileQueued) { reconcileQueued = true; queueMicrotask(reconcile) } }
263
+ return {
264
+ valid, complete,
265
+ stats() { return { owners: members.size, rooms: joined.size, pendingJoins: waiting.length, dedupe: events.size, connection: socket ? 1 : 0, timers: [retryTimer, readyTimer, heartbeat, pongTimer, flushTimer].filter(Boolean).length + [...members].filter(member => member.flight && member.flight.deadline).length, refreshes: [...members].filter(member => member.flight).length, dirty: [...members].filter(member => member.dirty).length } },
266
+ add(member) {
267
+ if (!members.has(member) && members.size >= MAX_OWNERS) fail('Close unused live resource owners before adding another')
268
+ members.add(member)
269
+ if (scope && member.scope && member.scope !== scope) stop()
270
+ queue()
271
+ },
272
+ remove(member) {
273
+ members.delete(member); clearMember(member); member.up.v = false
274
+ if (!members.size) { stop(); scope = null; terminal = ''; attempts = 0; retryStart = 0 }
275
+ else queue()
276
+ },
277
+ reconnect(member) {
278
+ if (!members.has(member)) return
279
+ stop(); terminal = ''; attempts = 0; retryStart = 0; queue()
280
+ },
281
+ }
282
+ }
283
+
284
+ function observeMetadata(callback, value) {
285
+ const result = untrack(() => callback(value))
286
+ if (result && typeof result.then === 'function') {
287
+ // Observe rejection without retaining an owner or the user-provided error.
288
+ Promise.resolve(result).catch(() => {})
289
+ fail('Use synchronous metadata observers; resource owns asynchronous fetching')
290
+ }
291
+ }
292
+
293
+ // Promise reactions retain only a nullable cell and scalar flight metadata. An
294
+ // externally retained, never-settling Promise cannot keep a disposed resource alive.
295
+ function readResource(cell, flight) {
296
+ flight.tick = flight.resolveTick = null
297
+ const member = cell.record
298
+ if (member && broker.valid(member, flight)) return member.node.v
299
+ }
300
+ function settleFlight(cell, flight, failed) {
301
+ const member = cell.record
302
+ if (member) broker.complete(member, flight, failed)
303
+ }
304
+ function timeoutFlight(cell, flight) {
305
+ const member = cell.record
306
+ if (member && broker.valid(member, flight)) {
307
+ // Never release a pending fetch slot merely because its deadline elapsed.
308
+ member.refreshState = 'refresh-timeout'
309
+ member.state.v = 'refresh-timeout'
310
+ }
311
+ }
312
+ function detach(cell) {
313
+ const member = cell.record
314
+ if (member && broker) broker.remove(member)
315
+ }
316
+ function dispose(cell) {
317
+ const member = cell.record
318
+ if (!member) return
319
+ cell.record = null
320
+ if (member.stopWatch) member.stopWatch()
321
+ if (broker) broker.remove(member)
322
+ member.node = member.options = member.owner = member.stopWatch = null
323
+ member.scope = null; member.rooms.clear(); member.flight = null
324
+ member.state.v = 'closed'
325
+ }
326
+ function observe(cell) {
327
+ const member = cell.record
328
+ if (!member) return
329
+ const scope = member.options.scope()
330
+ if (scope !== null && (typeof scope !== 'string' || !scope.length || scope.length > 160 || /[\x00-\x20\x7f]/.test(scope))) fail('Use null or a bounded public user/revision string as scope; never credentials')
331
+ member.scope = scope
332
+ member.rooms = roomsOf(typeof member.options.rooms === 'function' ? member.options.rooms() : member.options.rooms)
333
+ broker = broker || createBroker()
334
+ broker.add(member)
335
+ return detach.bind(null, cell)
336
+ }
337
+ function ownerCleanup(cell) { return dispose.bind(null, cell) }
338
+ function reconnect(cell) { if (cell.record && broker) broker.reconnect(cell.record) }
339
+
340
+ // Internal, metadata-only capacity evidence; never scope identifiers, rooms or values.
341
+ export function _liveStats() {
342
+ return broker ? broker.stats() : { owners: 0, rooms: 0, pendingJoins: 0, dedupe: 0, connection: 0, timers: 0, refreshes: 0, dirty: 0 }
343
+ }
344
+
345
+ export function liveResource(node, options = {}) {
346
+ if (!node || typeof node !== 'object' || typeof node.refetch !== 'function' || !('v' in node) || !exact(options, ['rooms', 'scope', 'onInvalidate', 'onResync']) || !own(options, 'rooms') || !own(options, 'scope') || typeof options.scope !== 'function') fail('Use liveResource(rows, {rooms, scope: () => publicUserRevision}) with a resource handle')
347
+ if (own(options, 'onInvalidate') && options.onInvalidate !== undefined && typeof options.onInvalidate !== 'function') fail('Use a metadata callback for onInvalidate')
348
+ if (own(options, 'onResync') && options.onResync !== undefined && typeof options.onResync !== 'function') fail('Use a metadata callback for onResync')
349
+ const state = sig('idle'), up = sig(false), cell = { record: null }
350
+ const out = {
351
+ get connected() { return up.v },
352
+ get state() { return state.v },
353
+ close() { dispose(cell); up.v = false; state.v = 'closed' },
354
+ reconnect: reconnect.bind(null, cell),
355
+ }
356
+ if (G.__brainSSR || G.__brainRoute) return out
357
+ if (typeof G.WebSocket !== 'function') { state.v = 'unsupported'; return out }
358
+ const owner = I.owner()
359
+ if (!owner) fail('Create liveResource inside a component so unmount closes its subscription')
360
+ cell.record = { cell, node, options, state, up, owner, scope: null, rooms: new Map(), generation: 0, flight: null, dirty: false, pending: null, resync: null, refreshState: '', stopWatch: null }
361
+ fx(ownerCleanup.bind(null, cell))
362
+ cell.record.stopWatch = fx(observe.bind(null, cell))
363
+ return out
364
+ }
package/brain-overlay.js CHANGED
@@ -24,10 +24,21 @@ export function forLLM(d) {
24
24
  })
25
25
  if (d.fix) out.push(`fix: ${d.fix}`)
26
26
  if (d.brainStack) out.push('stack:', ...[].concat(d.brainStack))
27
+ if (d.location || d.frames || d.causes || d.rid) out.push('structured trace (development only):', JSON.stringify(traceOf(d)))
28
+ if (d.mapping?.status && d.mapping.status !== 'mapped') out.push('Location mapping is incomplete; do not treat generated coordinates as original source coordinates.')
29
+ out.push('Agent next steps: inspect the exact source revision, apply a focused change, run brain check --json, then reproduce the failing action. Record evidence with brain history and refresh brain quality.')
27
30
  if (d.file) out.push('brain check --json:', JSON.stringify((byFile.get(d.file) || [d]).map(({ snippet, ...x }) => x)))
28
31
  return out.join('\n')
29
32
  }
30
33
 
34
+ function traceOf(d) {
35
+ return { schemaVersion: 1, requestId: d.rid || d.requestId || null, component: d.comp || null,
36
+ location: d.location || null, generatedLocation: d.generatedLocation || null,
37
+ mapping: d.mapping || null, truncated: d.truncated || null,
38
+ frames: Array.isArray(d.frames) ? d.frames.slice(0, 24) : [],
39
+ causes: Array.isArray(d.causes) ? d.causes.slice(0, 6) : [] }
40
+ }
41
+
31
42
  function item(d, i) {
32
43
  const loc = d.file ? `${d.file}:${d.line || ''}${d.col ? ':' + d.col : ''}` : d.src || ''
33
44
  const snip = d.snippet && d.snippet.text
@@ -35,6 +46,7 @@ function item(d, i) {
35
46
  : ''
36
47
  return `<li class="${d.level === 'warning' || d.level === 'info' ? d.level : 'error'}"><p class="t"><b>${esc(d.code || 'Error')}</b> <span>${esc(loc)}</span></p>` +
37
48
  `<p>${esc(d.msg)}</p>${snip}${d.fix ? `<p class="fix">fix: ${esc(d.fix)}</p>` : ''}` +
49
+ `${d.location || d.frames || d.causes ? `<details><summary>Development trace and mapping</summary><pre>${esc(JSON.stringify(traceOf(d), null, 2))}</pre></details>` : ''}` +
38
50
  `<button type="button" data-copy="${i}">Copy for the LLM</button></li>`
39
51
  }
40
52