@christianirack/brainllm-runtime 0.4.0 → 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-ch.js CHANGED
@@ -60,10 +60,10 @@ const safe = (k, v) => (k === '__proto__' || k === 'constructor' || k === 'proto
60
60
  // global instance) and inside `provide(K, store(…))` (one instance per subtree, §41.2). sync: 'tab' (BroadcastChannel) | 'storage'
61
61
  // (localStorage + storage event) | 'ws' (WebSocket at `url`, see ws() below). `id` is set by the codegen (channel
62
62
  // path); if missing, it is derived from init.
63
- export function store(init, o = {}, id) {
64
- const name = id || o.name || 'store:' + Object.keys(init).join(',')
63
+ // Shared signal/descriptor construction. Compiler-selected plain stores retain the
64
+ // same reset, serialization and inspector contracts without reaching transport code.
65
+ function storeState(init, name, changed) {
65
66
  const S = {}, s = {}
66
- let last = '' // JSON of the last state sent or received: cuts the echo between tabs
67
67
  for (const k of Object.keys(init)) {
68
68
  S[k] = sig(init[k], name + '.' + k) // named: dev tools (brain_why, HMR) see `cart.total`
69
69
  Object.defineProperty(s, k, {
@@ -80,7 +80,8 @@ export function store(init, o = {}, id) {
80
80
  const apply = d => {
81
81
  const n = snap()
82
82
  for (const k in d) if (k in S) n[k] = d[k]
83
- last = JSON.stringify(n) // before the batch: the sync fx runs when it closes and must see the echo
83
+ const encoded = JSON.stringify(n) // Preserve serialization errors and toJSON effects, even without sync.
84
+ if (changed) changed(encoded) // Before the batch: a sync effect must see the new echo marker.
84
85
  batch(() => {
85
86
  for (const k in n) S[k].v = n[k]
86
87
  })
@@ -90,6 +91,19 @@ export function store(init, o = {}, id) {
90
91
  $snap: { value: snap },
91
92
  $reset: { value: () => apply({ ...init }) }, // back to the initial state (persisted and synced like any write)
92
93
  })
94
+ return { S, s, snap, apply }
95
+ }
96
+ // Compiler-only: public store() still accepts all options and retains its full fallback.
97
+ export function plainStore(init, id) {
98
+ const name = id || 'store:' + Object.keys(init).join(',')
99
+ const state = storeState(init, name, null)
100
+ JSON.stringify(state.snap()) // The public plain path performs this validation too.
101
+ return state.s
102
+ }
103
+ export function store(init, o = {}, id) {
104
+ const name = id || o.name || 'store:' + Object.keys(init).join(',')
105
+ let last = '' // JSON of the last state sent or received: cuts the echo between tabs
106
+ const { S, s, snap, apply } = storeState(init, name, value => { last = value })
93
107
  const K = 'brain:' + name
94
108
  if (o.persist) {
95
109
  // Persistence (INTERNALS §35.4), versioned by the shape of init. Small states live in localStorage and are restored
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@christianirack/brainllm-runtime",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
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": {
@@ -0,0 +1,156 @@
1
+ // Fixed loopback Upgrade transport. Rust owns WebSocket frames, authentication and per-delivery policy.
2
+ import {request} from 'node:http'
3
+ import {createHash, randomUUID} from 'node:crypto'
4
+
5
+ const PROTOCOL = 'brain.v1'
6
+ const LIMITS = Object.freeze({connections:32, headers:16384, credentials:16384, head:4096, upstreamHead:8192, queued:262144, bytes:64*1024*1024, handshake:5000, write:2000, idle:70000, lifetime:15*60*1000, close:1000})
7
+ const SINGLE = new Set(['host','origin','authorization','cookie','upgrade','sec-websocket-key','sec-websocket-version','sec-websocket-protocol'])
8
+ const installed = new WeakSet()
9
+ const connectionUpgrade = value => typeof value === 'string' && value.toLowerCase().split(/\s*,\s*/).includes('upgrade')
10
+ const unique = (headers, names) => {
11
+ const seen = new Set()
12
+ for (let i=0;i<headers.length;i+=2) {
13
+ const name = headers[i].toLowerCase()
14
+ if (names.has(name) && seen.has(name)) return false
15
+ seen.add(name)
16
+ }
17
+ return true
18
+ }
19
+
20
+ export function installNativeSockets(server, {target, base='/', cookie, log}) {
21
+ if (installed.has(server)) throw Error('The native socket bridge is already installed')
22
+ if (typeof target !== 'string' || target.length>2048) throw Error('Native sockets require a fixed loopback HTTP backend')
23
+ let origin
24
+ try { origin = new URL(target) } catch { throw Error('Native sockets require a fixed loopback HTTP backend') }
25
+ if (origin.protocol !== 'http:' || !['127.0.0.1','localhost','[::1]'].includes(origin.hostname) || origin.username || origin.password || origin.pathname !== '/' || origin.search || origin.hash) throw Error('Native sockets require a fixed loopback HTTP backend')
26
+ if (origin.hostname === 'localhost') origin.hostname = '127.0.0.1'
27
+ if (typeof base !== 'string' || base.length>2048 || !base.startsWith('/') || !base.endsWith('/') || /[\\?#\s]/.test(base) || base.includes('//') || new URL(base,'http://brain.invalid').pathname !== base) throw Error('Native sockets require a canonical application base path')
28
+ if (typeof cookie !== 'function' || typeof log !== 'function') throw Error('Native socket bridge callbacks are required')
29
+ const path = (base === '/' ? '' : base.slice(0,-1)) + '/_brain/socket'
30
+ const owned = new Set()
31
+ let stopped = false, window = 0, reports = 0
32
+ // Fixed fields only: never log target URLs, credentials, frames, request headers or exception messages.
33
+ const report = (code,rid) => {
34
+ const current = Math.floor(Date.now()/60000)
35
+ if (window !== current) { window=current; reports=0 }
36
+ if (reports++ >= 16) return
37
+ try { log('warn','native socket unavailable',{code,rid,fix:'Check native socket activation, the trusted Origin, authentication and backend availability; reconnect after correcting the cause'}) } catch {}
38
+ }
39
+ const reject = (socket,status,rid,code) => {
40
+ report(code,rid)
41
+ if (socket.destroyed) return
42
+ socket.setTimeout(LIMITS.close,()=>socket.destroy())
43
+ socket.end(`HTTP/1.1 ${status} Socket unavailable\r\nConnection: close\r\nCache-Control: no-store\r\nX-Request-Id: ${rid}\r\nContent-Length: 0\r\n\r\n`,()=>socket.destroy())
44
+ }
45
+ const upgrade = (incoming,browser,head) => {
46
+ const ownedPath=typeof incoming.url === 'string' && incoming.url.split(/[?#]/,1)[0] === path
47
+ // Other trusted listeners own their endpoints. If this is the only listener, reject unknown upgrades promptly
48
+ // instead of retaining a raw socket with no owner. Queries on our exact endpoint always remain ours to reject.
49
+ if (!ownedPath && server.listenerCount('upgrade')>1) return
50
+ if (browser.destroyed) return
51
+ const rid = randomUUID()
52
+ // A socket error can precede admission, including while a rejected response is flushing.
53
+ browser.on('error',()=>{})
54
+ if (stopped || server.draining) return reject(browser,503,rid,'NATIVE_SOCKET_DRAINING')
55
+ if (incoming.method !== 'GET') return reject(browser,405,rid,'NATIVE_SOCKET_METHOD')
56
+ if (incoming.url !== path) return reject(browser,400,rid,'NATIVE_SOCKET_PATH')
57
+ const headerBytes = incoming.rawHeaders.reduce((bytes,value)=>bytes+Buffer.byteLength(value)+2,0)
58
+ if (headerBytes>LIMITS.headers || !unique(incoming.rawHeaders,SINGLE)) return reject(browser,431,rid,'NATIVE_SOCKET_HEADERS')
59
+ if (head.length>LIMITS.head || incoming.headers['content-length'] && incoming.headers['content-length'] !== '0' || incoming.headers['transfer-encoding']) return reject(browser,400,rid,'NATIVE_SOCKET_BODY')
60
+ let site
61
+ try { site=new URL(incoming.headers.origin) } catch { return reject(browser,403,rid,'NATIVE_SOCKET_ORIGIN') }
62
+ if (!['http:','https:'].includes(site.protocol) || site.origin !== incoming.headers.origin || site.origin.length>2048) return reject(browser,403,rid,'NATIVE_SOCKET_ORIGIN')
63
+ const key = incoming.headers['sec-websocket-key']
64
+ if (incoming.headers.upgrade?.toLowerCase() !== 'websocket' || !connectionUpgrade(incoming.headers.connection) || incoming.headers['sec-websocket-version'] !== '13' || incoming.headers['sec-websocket-protocol'] !== PROTOCOL || typeof key !== 'string' || !/^[A-Za-z0-9+/]{22}==$/.test(key) || Buffer.from(key,'base64').toString('base64') !== key) return reject(browser,400,rid,'NATIVE_SOCKET_HANDSHAKE')
65
+ let credentials
66
+ try { credentials=cookie(incoming) } catch { return reject(browser,400,rid,'NATIVE_SOCKET_CREDENTIALS') }
67
+ const authorization = incoming.headers.authorization
68
+ if (typeof credentials !== 'string' || authorization !== undefined && typeof authorization !== 'string' || Buffer.byteLength(credentials)+Buffer.byteLength(authorization || '')>LIMITS.credentials) return reject(browser,431,rid,'NATIVE_SOCKET_CREDENTIALS')
69
+ if (owned.size>=LIMITS.connections) return reject(browser,503,rid,'NATIVE_SOCKET_CAPACITY')
70
+ browser.pause()
71
+ let outgoing, upstream, handshake, lifetime, closing, bytes=0, done=false, upgraded=false
72
+ const writes=new Set()
73
+ const finish = (code,status) => {
74
+ if (done) return
75
+ done=true
76
+ clearTimeout(handshake); clearTimeout(lifetime); clearTimeout(closing); owned.delete(finish)
77
+ for (const timer of writes) clearTimeout(timer)
78
+ writes.clear()
79
+ browser.setTimeout(0)
80
+ if (upstream) { upstream.setTimeout(0); browser.unpipe(upstream); upstream.unpipe(browser); upstream.destroy() }
81
+ outgoing?.destroy()
82
+ if (status && !upgraded) reject(browser,status,rid,code)
83
+ else { if (code) report(code,rid); browser.destroy() }
84
+ }
85
+ const relay = (source,destination) => {
86
+ let blocked
87
+ destination.on('drain',()=>{if(blocked){clearTimeout(blocked);writes.delete(blocked);blocked=undefined}})
88
+ source.on('data',chunk=>{
89
+ bytes+=chunk.length
90
+ if (bytes>LIMITS.bytes || destination.writableLength+chunk.length>LIMITS.queued) finish('NATIVE_SOCKET_WRITE_BUDGET')
91
+ })
92
+ source.once('end',()=>{
93
+ if (done) return
94
+ closing ??= setTimeout(()=>finish('NATIVE_SOCKET_HALF_CLOSE'),LIMITS.close)
95
+ closing.unref?.()
96
+ destination.once('finish',()=>finish())
97
+ })
98
+ // Native pipe honors write(false)/drain. highWaterMark alone is a threshold, so the explicit queued cap above
99
+ // also stops a future/custom stream chunk that exceeds the admitted write budget.
100
+ source.pipe(destination)
101
+ // Observe after pipe's synchronous write so an unresponsive destination cannot retain its bounded queue
102
+ // until the much longer idle/lifetime limits. There is at most one stalled-write timer per direction.
103
+ source.on('data',()=>{
104
+ if (!done && destination.writableNeedDrain && !blocked) {
105
+ blocked=setTimeout(()=>finish('NATIVE_SOCKET_WRITE_TIMEOUT'),LIMITS.write)
106
+ blocked.unref?.();writes.add(blocked)
107
+ }
108
+ })
109
+ }
110
+ owned.add(finish)
111
+ browser.once('error',()=>finish('NATIVE_SOCKET_CLIENT_ERROR'))
112
+ browser.once('close',()=>finish())
113
+ browser.setTimeout(LIMITS.idle,()=>finish('NATIVE_SOCKET_IDLE'))
114
+ handshake=setTimeout(()=>finish('NATIVE_SOCKET_HANDSHAKE_TIMEOUT',504),LIMITS.handshake)
115
+ handshake.unref?.()
116
+ try {
117
+ outgoing=request(new URL('/_brain/socket',origin),{
118
+ method:'GET',agent:false,maxHeaderSize:8192,
119
+ headers:{host:origin.host,origin:site.origin,connection:'Upgrade',upgrade:'websocket','sec-websocket-version':'13','sec-websocket-key':key,'sec-websocket-protocol':PROTOCOL,'x-request-id':rid,...(credentials ? {cookie:credentials} : {}),...(authorization !== undefined ? {authorization} : {})},
120
+ })
121
+ outgoing.once('error',()=>finish('NATIVE_SOCKET_UPSTREAM_UNAVAILABLE',502))
122
+ outgoing.once('response',response=>{
123
+ response.destroy()
124
+ finish('NATIVE_SOCKET_UPSTREAM_REJECTED',[400,401,403,404,429,503].includes(response.statusCode) ? response.statusCode : 502)
125
+ })
126
+ outgoing.once('upgrade',(response,socket,buffered)=>{
127
+ if (done) { socket.destroy(); return }
128
+ upstream=socket
129
+ socket.on('error',()=>finish('NATIVE_SOCKET_UPSTREAM_ERROR'))
130
+ socket.once('close',()=>finish())
131
+ const expected=createHash('sha1').update(key+'258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64')
132
+ if (response.statusCode !== 101 || !unique(response.rawHeaders,new Set(['upgrade','sec-websocket-accept','sec-websocket-protocol'])) || response.headers.upgrade?.toLowerCase() !== 'websocket' || !connectionUpgrade(response.headers.connection) || response.headers['sec-websocket-accept'] !== expected || response.headers['sec-websocket-protocol'] !== PROTOCOL || response.headers['sec-websocket-extensions'] || buffered.length>LIMITS.upstreamHead) return finish('NATIVE_SOCKET_BAD_UPSTREAM_HANDSHAKE',502)
133
+ clearTimeout(handshake)
134
+ lifetime=setTimeout(()=>finish('NATIVE_SOCKET_LIFETIME'),LIMITS.lifetime)
135
+ lifetime.unref?.()
136
+ socket.setTimeout(LIMITS.idle,()=>finish('NATIVE_SOCKET_IDLE'))
137
+ upgraded=true
138
+ browser.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${expected}\r\nSec-WebSocket-Protocol: ${PROTOCOL}\r\nCache-Control: no-store\r\nX-Request-Id: ${rid}\r\n\r\n`)
139
+ bytes=buffered.length+head.length
140
+ if (buffered.length) browser.write(buffered)
141
+ if (head.length) socket.write(head)
142
+ relay(browser,socket); relay(socket,browser); browser.resume()
143
+ })
144
+ outgoing.end()
145
+ } catch { finish('NATIVE_SOCKET_UPSTREAM_UNAVAILABLE',502) }
146
+ }
147
+ const closeOwned = () => { for (const finish of [...owned]) finish() }
148
+ const stop = () => { if (stopped) return; stopped=true; closeOwned() }
149
+ installed.add(server)
150
+ server.on('upgrade',upgrade)
151
+ server.once('close',()=>{stop();server.removeListener('upgrade',upgrade)})
152
+ const originalClose=server.close, originalAll=server.closeAllConnections
153
+ server.close=function(...args){stop();return originalClose.apply(this,args)}
154
+ if (originalAll) server.closeAllConnections=function(...args){closeOwned();return originalAll.apply(this,args)}
155
+ return stop
156
+ }
package/server/server.mjs CHANGED
@@ -19,6 +19,7 @@
19
19
  // `<!--brain:props-->` inside <script type="application/json" id="brain-props">.
20
20
  import { createServer as http, STATUS_CODES, request as httpRequest, Agent as HttpAgent } from 'node:http'
21
21
  import net from 'node:net'
22
+ import { installNativeSockets } from './native-sockets.mjs'
22
23
  import { Readable } from 'node:stream'
23
24
  import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto'
24
25
  import { existsSync, readFileSync, realpathSync, statSync, lstatSync } from 'node:fs'
@@ -389,6 +390,7 @@ export function nativeAuthBridge(raw) {
389
390
  headers: () => req.$nativeLoginHeaders || {},
390
391
  }
391
392
  },
393
+ _socketCookie: req => cookies(req, false),
392
394
  close() { stopped = true; for (const request of owned) request.destroy(); agent.destroy() },
393
395
  }
394
396
  }
@@ -397,6 +399,7 @@ export function createServer(o = {}) {
397
399
  const dist = resolve(o.dist || process.cwd())
398
400
  const env = Object.freeze({ ...(o.env || ENV) }) // ctx.env: what the supervisor passed, read-only
399
401
  const native = nativeAuthBridge(o.backendURL ?? env.BRAIN_BACKEND_URL)
402
+ if (o.nativeSockets !== undefined && typeof o.nativeSockets !== 'boolean') throw Error('nativeSockets must be an explicit boolean')
400
403
  const TIMEOUT = +(o.timeout ?? env.BRAIN_TIMEOUT_MS ?? 10000)
401
404
  const LIMIT = +(o.rateLimit ?? env.BRAIN_RPC_LIMIT ?? 60) // per IP + function, per minute
402
405
  const MAX_BODY = 64 * 1024
@@ -419,6 +422,10 @@ export function createServer(o = {}) {
419
422
  sink(level, msg, safe)
420
423
  }
421
424
  const mf = readManifest(dist)
425
+ const hasManifestSockets = Object.hasOwn(mf,'nativeSockets')
426
+ if (hasManifestSockets && typeof mf.nativeSockets !== 'boolean') throw Error('Manifest nativeSockets must be an explicit boolean')
427
+ const nativeSockets = o.nativeSockets === undefined ? hasManifestSockets && mf.nativeSockets : o.nativeSockets
428
+ if (nativeSockets && !native) throw Error('Native sockets require a configured native backend')
422
429
  // the build's single route table (#187): already in match order, each with its RegExp source (named groups = params)
423
430
  const routes = mf.routes.map(r => ({ ...r, rx: r.rx ? new RegExp(r.rx) : exact(r.path) }))
424
431
  // the real path: Node imports file URLs through realpath, and the /_b/ hook must name the same modules (a symlinked
@@ -1049,6 +1056,7 @@ export function createServer(o = {}) {
1049
1056
  }
1050
1057
  })
1051
1058
  if (native) server.once('close', () => native.close())
1059
+ if (native && nativeSockets) server.$brainSocketStop = installNativeSockets(server,{target:o.backendURL ?? env.BRAIN_BACKEND_URL,base:BASE,cookie:native._socketCookie,log})
1052
1060
  server.headersTimeout = 15000
1053
1061
  server.requestTimeout = 30000
1054
1062
  return server
@@ -1426,6 +1434,7 @@ if (process.argv[1] && real(process.argv[1]) === real(fileURLToPath(import.meta.
1426
1434
  const drain = () => {
1427
1435
  if (s.draining) return
1428
1436
  s.draining = true
1437
+ s.$brainSocketStop?.()
1429
1438
  const ms = +(ENV.BRAIN_DRAIN_MS || 10000)
1430
1439
  const force = setTimeout(() => {
1431
1440
  s.closeAllConnections()