@christianirack/brainllm-runtime 0.4.0 → 0.6.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
@@ -105,12 +119,14 @@ export function store(init, o = {}, id) {
105
119
  const base = JSON.stringify(snap()) // the state as created: changes made before the module loads are still saved
106
120
  READY.push(import('./brain-persist.js').then(m => m.persist({ key: P, v, big, apply, snap, S, base })))
107
121
  }
122
+ let onStorage = null
108
123
  if (o.sync === 'storage' && G.localStorage) {
109
124
  try {
110
125
  const v = localStorage.getItem(K)
111
126
  if (v) apply(JSON.parse(v, safe))
112
127
  } catch {}
113
- G.addEventListener && G.addEventListener('storage', e => e.key === K && e.newValue && apply(JSON.parse(e.newValue, safe)))
128
+ onStorage = e => e.key === K && e.newValue && apply(JSON.parse(e.newValue, safe))
129
+ G.addEventListener && G.addEventListener('storage', onStorage)
114
130
  }
115
131
  const bc = o.sync === 'tab' && G.BroadcastChannel ? new BroadcastChannel(K) : null
116
132
  const w = o.sync === 'ws' && G.WebSocket ? ws(o.url || '/_ws', K, apply) : null
@@ -141,6 +157,14 @@ export function store(init, o = {}, id) {
141
157
  }
142
158
  }),
143
159
  I.owner())
160
+ // a store owned by a subtree (provide inside a component) releases its channel, listener and socket with it;
161
+ // a global store (channels/, no owner) lives as long as the page
162
+ if (o.sync && I.owner())
163
+ I.root(() => fx(() => () => {
164
+ if (bc && bc.close) bc.close()
165
+ if (onStorage && G.removeEventListener) G.removeEventListener('storage', onStorage)
166
+ if (w) w.close()
167
+ }), I.owner())
144
168
  return s
145
169
  }
146
170
 
@@ -185,8 +209,9 @@ function ws(url, s, apply) {
185
209
  }
186
210
  }
187
211
  const now = () => ((n = 0), open())
212
+ const shown = () => document.visibilityState === 'visible' && open()
188
213
  G.addEventListener && G.addEventListener('online', now)
189
- G.document && document.addEventListener && document.addEventListener('visibilitychange', () => document.visibilityState === 'visible' && open())
214
+ G.document && document.addEventListener && document.addEventListener('visibilitychange', shown)
190
215
  c = {
191
216
  subs,
192
217
  sub(k, f) {
@@ -198,14 +223,16 @@ function ws(url, s, apply) {
198
223
  off = true
199
224
  clearTimeout(t)
200
225
  sock && sock.close()
201
- WS.delete(url)
226
+ if (WS.get(url) === c) WS.delete(url)
202
227
  G.removeEventListener && G.removeEventListener('online', now)
228
+ G.document && document.removeEventListener && document.removeEventListener('visibilitychange', shown)
203
229
  },
204
230
  }
205
231
  WS.set(url, c)
206
232
  }
207
233
  c.sub(s, apply)
208
- return { send: d => c.send(s, d), close: () => (c.subs.delete(s), c.subs.size || c.close()) }
234
+ let gone = false // $close() and the owner's cleanup may both run: release once
235
+ return { send: d => c.send(s, d), close: () => gone || ((gone = true), c.subs.delete(s), c.subs.size || c.close()) }
209
236
  }
210
237
 
211
238
  // Shape of a store: field names + kind of each initial value. A persisted state with another shape is discarded.
package/brain-data.js CHANGED
@@ -186,8 +186,9 @@ function request(path, M, def, arg, o) {
186
186
  // optimistic: () => (list = [...list, item]) — applied now, undone if the request fails
187
187
  const undo = o.optimistic ? rec(o.optimistic) : null
188
188
  // a call made from an event handler is cancelled when its component unmounts (brain-guard keeps `$c` on its nodes)
189
+ let reg = null
189
190
  if (!CUR && G.event && G.event.target) for (let el = G.event.target; el; el = el.parentNode) if (el.$c) {
190
- const c = el.$c
191
+ const c = (reg = el.$c)
191
192
  c.add(ctl)
192
193
  ctl.signal.addEventListener('abort', () => c.delete(ctl))
193
194
  break
@@ -238,6 +239,8 @@ function request(path, M, def, arg, o) {
238
239
  batch(() => { for (let i = undo.length - 2; i >= 0; i -= 2) undo[i].v = undo[i + 1] })
239
240
  throw e
240
241
  })
242
+ // a settled request leaves its component's registry: a long-lived view must not keep every finished call
243
+ if (reg) p.then(() => reg.delete(ctl), () => reg.delete(ctl))
241
244
  p.cancel = () => ctl.abort()
242
245
  if (file) Object.defineProperty(p, 'progress', { get: () => up.v }) // 0–1, a signal read
243
246
  return p
package/brain-env.js CHANGED
@@ -24,10 +24,11 @@ export function mounted(f) {
24
24
  onDispose(() => ((alive = false), typeof c === 'function' && c()))
25
25
  }
26
26
 
27
- // watch(() => src, (v, old) => …): runs the callback when the source changes, not on the first read; the callback is
28
- // not tracked. A returned function is its cleanup (before the next run and on unmount).
29
- export function watch(src, cb) {
30
- let first = true, old
27
+ // watch(() => src, (v, old) => …): runs the callback when the source changes, not on the first read (with
28
+ // `{ immediate: true }` also once with the initial value, `old` undefined); the callback is not tracked. A returned
29
+ // function is its cleanup (before the next run and on unmount).
30
+ export function watch(src, cb, options) {
31
+ let first = !options?.immediate, old
31
32
  fx(() => {
32
33
  const v = src()
33
34
  if (first) return void ((first = false), (old = v))
package/brain-guard.js CHANGED
@@ -24,22 +24,22 @@ const TEXT = 'This section could not load.'
24
24
  const BUDGET = { nodes: 200000, rows: 100000, runs: 1000, ms: 200 }
25
25
  const STOPPED = 'This part stopped to protect the page.'
26
26
  const OWN = new WeakMap() // root owner → instance
27
- const now = () => (G.performance ? performance.now() : Date.now())
27
+ const now = () => performance.now()
28
28
  // work per flush: the time between two effect runs belongs to the earlier one (the hook fires as a run starts); the
29
29
  // last run of a flush is settled by a microtask queued at the flush's first run
30
30
  let LX = null, LT = 0, inFlush = false
31
31
  const touched = new Set()
32
32
  // elements under a root, and the most element children any one of them has (the rows of a list)
33
33
  const count = (e, c) => {
34
- if (e.nodeType !== 1) return
34
+ if (e.nodeType != 1) return
35
35
  c.nodes++
36
36
  let k = 0
37
- for (const n of e.childNodes) if (n.nodeType === 1) k++, count(n, c)
37
+ for (const n of e.childNodes) if (n.nodeType == 1) k++, count(n, c)
38
38
  if (k > c.rows) c.rows = k
39
39
  }
40
40
  // elements under a root: native when the DOM has it (the shim walks)
41
41
  const elems = r => {
42
- if (r.nodeType !== 1) return 0
42
+ if (r.nodeType != 1) return 0
43
43
  if (r.getElementsByTagName) return 1 + r.getElementsByTagName('*').length
44
44
  const c = { nodes: 0, rows: 0 }
45
45
  count(r, c)
@@ -219,7 +219,7 @@ export function guard(C, fb, budget) {
219
219
  report(pending[0], pending[1])
220
220
  pending = null
221
221
  }
222
- for (const n of nodes) if (n.nodeType === 1) (n.$g = handler), (n.$c = reqs)
222
+ for (const n of nodes) if (n.nodeType == 1) (n.$g = handler), (n.$c = reqs)
223
223
  }, own)
224
224
  function again() {
225
225
  const p = end.parentNode
@@ -232,7 +232,7 @@ export function guard(C, fb, budget) {
232
232
  if (own)
233
233
  I.root(() => fx(() => () => {
234
234
  if (dispose) dispose()
235
- for (const n of nodes) if (n.nodeType === 1) (n.$g = null), (n.$c = null) // see brain-dev: no node → instance links
235
+ for (const n of nodes) if (n.nodeType == 1) (n.$g = null), (n.$c = null) // see brain-dev: no node → instance links
236
236
  I.rm(nodes)
237
237
  }), own)
238
238
  return [...nodes, end]
package/brain-live.js ADDED
@@ -0,0 +1,367 @@
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
+ // A joined room proves health only once the connection stays up this long; shorter sessions keep spending the budget.
8
+ const STABLE_MS = 30000
9
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
10
+ const own = (value, key) => Object.prototype.hasOwnProperty.call(value, key)
11
+ const RESERVED = new Set(['constructor', 'prototype', '__proto__', 'id', 'owner', 'created_at', 'updated_at', 'search'])
12
+ let broker
13
+ const fault = fix => Object.assign(Error('B0918: ' + fix), { code: 'B0918', fix })
14
+ const fail = fix => { throw fault(fix) }
15
+ const exact = (value, keys) => value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).every(key => keys.includes(key))
16
+ const resourceName = name => typeof name === 'string' && /^[a-z][a-z0-9_]{0,47}$/.test(name) && !RESERVED.has(name)
17
+
18
+ function roomsOf(value) {
19
+ if (!Array.isArray(value) || value.length > MAX_ROOMS) fail('Use at most 32 native resource rooms')
20
+ const rooms = new Map()
21
+ for (const room of value) {
22
+ let command, key
23
+ const id = room && typeof room === 'object' && own(room, 'id') ? room.id : undefined
24
+ if (exact(room, ['room']) && own(room, 'room') && room.room === 'user:self') {
25
+ command = { room: 'user:self' }; key = 'user:self'
26
+ } else if (exact(room, ['resource', 'id']) && own(room, 'resource') && resourceName(room.resource) && (id === undefined || (typeof id === 'string' && UUID.test(id)))) {
27
+ command = id === undefined ? { resource: room.resource } : { resource: room.resource, id: id.toLowerCase() }
28
+ key = command.resource + ':' + (command.id || '')
29
+ } else fail('Use a declared resource, an optional UUID, or the user:self room')
30
+ rooms.set(key, Object.freeze(command))
31
+ }
32
+ return rooms
33
+ }
34
+
35
+ function endpoint() {
36
+ const location = G.location
37
+ if (!location || !/^https?:$/.test(location.protocol)) fail('Serve live resources from an HTTP(S) application origin')
38
+ const meta = G.document && document.querySelector('meta[name=brain-base]')
39
+ const base = meta && meta.getAttribute('content') || ''
40
+ 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')
41
+ const origin = new URL(location.origin)
42
+ if (origin.origin !== location.origin || origin.username || origin.password) fail('Use a canonical application origin')
43
+ const normalized = base.replace(/\/$/, '')
44
+ const url = new URL(normalized + '/_brain/socket', origin)
45
+ if (url.origin !== origin.origin || url.pathname !== normalized + '/_brain/socket') fail('Use a normalized same-origin application base path')
46
+ url.protocol = origin.protocol === 'https:' ? 'wss:' : 'ws:'
47
+ return url.href
48
+ }
49
+
50
+ function metadata(text) {
51
+ if (typeof text !== 'string' || text.length > FRAME || /[^\x09\x0a\x0d\x20-\x7e]/.test(text)) return null
52
+ let value
53
+ try {
54
+ value = JSON.parse(text, (key, item) => {
55
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') throw Error('Unsafe metadata')
56
+ return item
57
+ })
58
+ } catch { return null }
59
+ if (exact(value, ['type', 'protocol', 'durable', 'resyncRequired']) && value.type === 'ready' && value.protocol === 1 && value.durable === false && value.resyncRequired === true) return Object.freeze(value)
60
+ if (exact(value, ['type', 'reason']) && value.type === 'resync' && value.reason === 'joined') return Object.freeze(value)
61
+ if (exact(value, ['type']) && value.type === 'pong') return Object.freeze(value)
62
+ if (exact(value, ['type', 'code']) && value.type === 'error' && value.code === 'ROOM_FORBIDDEN') return Object.freeze(value)
63
+ 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)
64
+ return null
65
+ }
66
+
67
+ function createBroker() {
68
+ const members = new Set(), joined = new Map(), waiting = [], events = new Map()
69
+ let socket = null, epoch = 0, scope = null, terminal = '', ready = false
70
+ let reconcileQueued = false, retryTimer, readyTimer, heartbeat, pongTimer, flushTimer
71
+ let attempts = 0, retryStart = 0, lastConnect = 0, healthySince = 0
72
+
73
+ const update = () => {
74
+ for (const member of members) {
75
+ const active = [...member.rooms.keys()].some(key => joined.get(key) === 'ready')
76
+ const forbidden = member.rooms.size > 0 && [...member.rooms.keys()].every(key => joined.get(key) === 'forbidden')
77
+ member.up.v = !!(socket && ready && member.scope && member.scope === scope && active)
78
+ member.state.v = !member.scope || !member.rooms.size ? 'idle' : terminal || (member.refreshState || (forbidden ? 'forbidden' : member.up.v ? 'open' : retryTimer ? 'retrying' : 'connecting'))
79
+ }
80
+ }
81
+ const clearMember = member => {
82
+ member.generation++
83
+ member.dirty = false
84
+ member.pending = member.resync = null
85
+ member.refreshState = member.flight ? 'refresh-pending' : ''
86
+ if (member.flight) {
87
+ const flight = member.flight
88
+ clearTimeout(flight.deadline); clearTimeout(flight.tick)
89
+ flight.deadline = flight.tick = null
90
+ if (flight.resolveTick) { flight.resolveTick(); flight.resolveTick = null }
91
+ }
92
+ }
93
+ const stop = () => {
94
+ epoch++; ready = false; healthySince = 0
95
+ clearTimeout(retryTimer); retryTimer = null
96
+ clearTimeout(readyTimer); clearInterval(heartbeat); clearTimeout(pongTimer)
97
+ readyTimer = heartbeat = pongTimer = null
98
+ clearTimeout(flushTimer); flushTimer = null
99
+ joined.clear(); waiting.length = 0; events.clear(); currentRooms.clear()
100
+ const previous = socket; socket = null
101
+ if (previous) {
102
+ previous.onopen = previous.onmessage = previous.onclose = previous.onerror = null
103
+ try { previous.close(1000, 'Live resource lifecycle ended') } catch {}
104
+ }
105
+ for (const member of members) { clearMember(member); member.up.v = false }
106
+ }
107
+ const reject = state => { stop(); terminal = state; update() }
108
+ const send = value => {
109
+ if (!socket || socket.readyState !== 1 || socket.bufferedAmount > BUFFER) return false
110
+ const text = JSON.stringify(value)
111
+ if (text.length > FRAME) return false
112
+ try { socket.send(text); return true } catch { return false }
113
+ }
114
+ const refresh = member => {
115
+ if (!members.has(member) || !member.up.v || !member.dirty || member.flight) return
116
+ member.dirty = false; member.refreshState = ''; update()
117
+ const pending = member.pending, resync = member.resync
118
+ member.pending = member.resync = null
119
+ const flight = { generation: member.generation, epoch, deadline: null, tick: null, resolveTick: null }
120
+ member.flight = flight
121
+ flight.deadline = setTimeout(timeoutFlight.bind(null, member.cell, flight), REFRESH_MS)
122
+ try {
123
+ if (resync && own(member.options, 'onResync') && member.options.onResync) observeMetadata(member.options.onResync, resync)
124
+ if (pending && own(member.options, 'onInvalidate') && member.options.onInvalidate) observeMetadata(member.options.onInvalidate, pending)
125
+ untrack(() => member.node.refetch())
126
+ // Equivalent post-effect tick, with an owned cancellation handle. The
127
+ // standard tick() intentionally exposes no handle for owner cancellation.
128
+ const next = new Promise(resolve => {
129
+ flight.resolveTick = resolve
130
+ flight.tick = setTimeout(resolve, 0)
131
+ })
132
+ next.then(readResource.bind(null, member.cell, flight)).then(
133
+ settleFlight.bind(null, member.cell, flight, false),
134
+ settleFlight.bind(null, member.cell, flight, true),
135
+ )
136
+ } catch {
137
+ I.oops(fault('Keep live metadata observers synchronous and handle resource failures in the view'), member.owner, 'B0918')
138
+ dispose(member.cell)
139
+ }
140
+ }
141
+ const valid = (member, flight) => members.has(member) && member.generation === flight.generation && epoch === flight.epoch
142
+ function complete(member, flight, failed) {
143
+ if (member.flight !== flight) return
144
+ clearTimeout(flight.deadline); clearTimeout(flight.tick)
145
+ flight.deadline = flight.tick = flight.resolveTick = null
146
+ member.flight = null; member.refreshState = ''
147
+ if (valid(member, flight) && failed) member.refreshState = 'refresh-error'
148
+ if (members.has(member) && member.dirty) schedule()
149
+ if (members.has(member)) update()
150
+ }
151
+ function schedule() {
152
+ if (!flushTimer) flushTimer = setTimeout(() => {
153
+ flushTimer = null
154
+ for (const member of members) refresh(member)
155
+ }, COALESCE)
156
+ }
157
+ const invalidate = (value, roomKey) => {
158
+ for (const member of members) {
159
+ if (!member.scope || member.scope !== scope) continue
160
+ 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))))
161
+ if (interested) { member.dirty = true; if (value.type === 'resync') member.resync = value; else member.pending = value; schedule() }
162
+ }
163
+ }
164
+ const retry = () => {
165
+ if (healthySince && Date.now() - healthySince >= STABLE_MS) { attempts = 0; retryStart = 0 }
166
+ stop()
167
+ if (++attempts > 5 || (retryStart && Date.now() - retryStart >= RETRY_MS)) { terminal = 'exhausted'; update(); return }
168
+ if (!retryStart) retryStart = Date.now()
169
+ const delay = Math.floor(Math.min(8000, 250 * 2 ** (attempts - 1)) * (0.75 + Math.random() * 0.25))
170
+ retryTimer = setTimeout(() => { retryTimer = null; reconcile() }, delay)
171
+ update()
172
+ }
173
+ const desired = () => {
174
+ const rooms = new Map(), scopes = new Set()
175
+ for (const member of members) if (member.scope && member.rooms.size) {
176
+ scopes.add(member.scope)
177
+ for (const [key, value] of member.rooms) rooms.set(key, value)
178
+ }
179
+ return { rooms, scopes }
180
+ }
181
+ const syncRooms = rooms => {
182
+ const changed = [...rooms.keys()].some(key => !joined.has(key)) || [...joined.keys()].some(key => !rooms.has(key))
183
+ if (waiting.length && changed) { retry(); return }
184
+ for (const [key] of joined) if (!rooms.has(key)) {
185
+ const room = currentRooms.get(key)
186
+ if (!room || !send({ type: 'leave', ...room })) { retry(); return }
187
+ joined.delete(key)
188
+ }
189
+ currentRooms = rooms
190
+ for (const [key, room] of rooms) if (!joined.has(key)) {
191
+ if (waiting.length >= MAX_ROOMS || !send({ type: 'join', ...room })) { retry(); return }
192
+ joined.set(key, 'pending'); waiting.push(key)
193
+ }
194
+ update()
195
+ }
196
+ let currentRooms = new Map()
197
+ const connect = () => {
198
+ const captured = ++epoch
199
+ lastConnect = Date.now()
200
+ let address
201
+ try { address = endpoint() } catch { reject('invalid-config'); return }
202
+ try { socket = new G.WebSocket(address, 'brain.v1') } catch { retry(); return }
203
+ readyTimer = setTimeout(() => { if (epoch === captured) retry() }, READY_MS)
204
+ socket.onopen = () => {
205
+ if (epoch !== captured) return
206
+ if (socket.protocol !== 'brain.v1') reject('protocol-error')
207
+ }
208
+ socket.onerror = () => { /* close or the ready deadline owns retries; arbitrary browser errors are not retained. */ }
209
+ socket.onclose = event => {
210
+ if (epoch !== captured) return
211
+ if (event.code === 1008) reject(event.reason === 'Authentication expired' ? 'auth-required' : 'policy-closed')
212
+ else retry()
213
+ }
214
+ socket.onmessage = event => {
215
+ if (epoch !== captured) return
216
+ const value = metadata(event.data)
217
+ if (!value) { reject('protocol-error'); return }
218
+ if (value.type === 'ready') {
219
+ if (ready) { reject('protocol-error'); return }
220
+ clearTimeout(readyTimer); readyTimer = null; ready = true
221
+ syncRooms(desired().rooms)
222
+ heartbeat = setInterval(() => {
223
+ if (!send({ type: 'ping' })) { retry(); return }
224
+ clearTimeout(pongTimer)
225
+ pongTimer = setTimeout(() => { if (epoch === captured) retry() }, 12000)
226
+ }, 25000)
227
+ } else if (!ready) reject('protocol-error')
228
+ else if (value.type === 'pong') { clearTimeout(pongTimer); pongTimer = null }
229
+ else if (value.type === 'resync' || value.type === 'error') {
230
+ const key = waiting.shift()
231
+ if (!key) { reject('protocol-error'); return }
232
+ joined.set(key, value.type === 'resync' ? 'ready' : 'forbidden')
233
+ if (value.type === 'resync' && !healthySince) healthySince = Date.now()
234
+ update()
235
+ if (value.type === 'resync') invalidate(value, key)
236
+ } else {
237
+ const now = Date.now()
238
+ for (const [id, time] of events) if (now - time > 60000) events.delete(id)
239
+ if (events.has(value.eventId)) return
240
+ events.set(value.eventId, now)
241
+ while (events.size > 128) events.delete(events.keys().next().value)
242
+ invalidate(value)
243
+ }
244
+ }
245
+ update()
246
+ }
247
+ function reconcile() {
248
+ reconcileQueued = false
249
+ const { rooms, scopes } = desired()
250
+ if (!rooms.size) { stop(); scope = null; terminal = ''; attempts = 0; retryStart = 0; update(); return }
251
+ if (scopes.size !== 1) { reject('scope-conflict'); return }
252
+ const next = scopes.values().next().value
253
+ if (next !== scope || terminal === 'scope-conflict') { stop(); scope = next; terminal = ''; attempts = 0; retryStart = 0 }
254
+ if (rooms.size > MAX_ROOMS) { reject('room-limit'); return }
255
+ if (terminal || retryTimer) { update(); return }
256
+ if (socket && ready) syncRooms(rooms)
257
+ else if (!socket) {
258
+ const elapsed = Date.now() - lastConnect
259
+ const delay = elapsed < 0 ? 0 : Math.max(0, 250 - elapsed)
260
+ if (delay) retryTimer = setTimeout(() => { retryTimer = null; reconcile() }, delay)
261
+ else connect()
262
+ }
263
+ update()
264
+ }
265
+ const queue = () => { if (!reconcileQueued) { reconcileQueued = true; queueMicrotask(reconcile) } }
266
+ return {
267
+ valid, complete,
268
+ 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 } },
269
+ add(member) {
270
+ if (!members.has(member) && members.size >= MAX_OWNERS) fail('Close unused live resource owners before adding another')
271
+ members.add(member)
272
+ if (scope && member.scope && member.scope !== scope) stop()
273
+ queue()
274
+ },
275
+ remove(member) {
276
+ members.delete(member); clearMember(member); member.up.v = false
277
+ if (!members.size) { stop(); scope = null; terminal = ''; attempts = 0; retryStart = 0 }
278
+ else queue()
279
+ },
280
+ reconnect(member) {
281
+ if (!members.has(member)) return
282
+ stop(); terminal = ''; attempts = 0; retryStart = 0; queue()
283
+ },
284
+ }
285
+ }
286
+
287
+ function observeMetadata(callback, value) {
288
+ const result = untrack(() => callback(value))
289
+ if (result && typeof result.then === 'function') {
290
+ // Observe rejection without retaining an owner or the user-provided error.
291
+ Promise.resolve(result).catch(() => {})
292
+ fail('Use synchronous metadata observers; resource owns asynchronous fetching')
293
+ }
294
+ }
295
+
296
+ // Promise reactions retain only a nullable cell and scalar flight metadata. An
297
+ // externally retained, never-settling Promise cannot keep a disposed resource alive.
298
+ function readResource(cell, flight) {
299
+ flight.tick = flight.resolveTick = null
300
+ const member = cell.record
301
+ if (member && broker.valid(member, flight)) return member.node.v
302
+ }
303
+ function settleFlight(cell, flight, failed) {
304
+ const member = cell.record
305
+ if (member) broker.complete(member, flight, failed)
306
+ }
307
+ function timeoutFlight(cell, flight) {
308
+ const member = cell.record
309
+ if (member && broker.valid(member, flight)) {
310
+ // Never release a pending fetch slot merely because its deadline elapsed.
311
+ member.refreshState = 'refresh-timeout'
312
+ member.state.v = 'refresh-timeout'
313
+ }
314
+ }
315
+ function detach(cell) {
316
+ const member = cell.record
317
+ if (member && broker) broker.remove(member)
318
+ }
319
+ function dispose(cell) {
320
+ const member = cell.record
321
+ if (!member) return
322
+ cell.record = null
323
+ if (member.stopWatch) member.stopWatch()
324
+ if (broker) broker.remove(member)
325
+ member.node = member.options = member.owner = member.stopWatch = null
326
+ member.scope = null; member.rooms.clear(); member.flight = null
327
+ member.state.v = 'closed'
328
+ }
329
+ function observe(cell) {
330
+ const member = cell.record
331
+ if (!member) return
332
+ const scope = member.options.scope()
333
+ 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')
334
+ member.scope = scope
335
+ member.rooms = roomsOf(typeof member.options.rooms === 'function' ? member.options.rooms() : member.options.rooms)
336
+ broker = broker || createBroker()
337
+ broker.add(member)
338
+ return detach.bind(null, cell)
339
+ }
340
+ function ownerCleanup(cell) { return dispose.bind(null, cell) }
341
+ function reconnect(cell) { if (cell.record && broker) broker.reconnect(cell.record) }
342
+
343
+ // Internal, metadata-only capacity evidence; never scope identifiers, rooms or values.
344
+ export function _liveStats() {
345
+ return broker ? broker.stats() : { owners: 0, rooms: 0, pendingJoins: 0, dedupe: 0, connection: 0, timers: 0, refreshes: 0, dirty: 0 }
346
+ }
347
+
348
+ export function liveResource(node, options = {}) {
349
+ 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')
350
+ if (own(options, 'onInvalidate') && options.onInvalidate !== undefined && typeof options.onInvalidate !== 'function') fail('Use a metadata callback for onInvalidate')
351
+ if (own(options, 'onResync') && options.onResync !== undefined && typeof options.onResync !== 'function') fail('Use a metadata callback for onResync')
352
+ const state = sig('idle'), up = sig(false), cell = { record: null }
353
+ const out = {
354
+ get connected() { return up.v },
355
+ get state() { return state.v },
356
+ close() { dispose(cell); up.v = false; state.v = 'closed' },
357
+ reconnect: reconnect.bind(null, cell),
358
+ }
359
+ if (G.__brainSSR || G.__brainRoute) return out
360
+ if (typeof G.WebSocket !== 'function') { state.v = 'unsupported'; return out }
361
+ const owner = I.owner()
362
+ if (!owner) fail('Create liveResource inside a component so unmount closes its subscription')
363
+ 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 }
364
+ fx(ownerCleanup.bind(null, cell))
365
+ cell.record.stopWatch = fx(observe.bind(null, cell))
366
+ return out
367
+ }