--- title: Reacting to Changes slug: guides/reacting-to-changes public: true category: guides template: guide order: 11 description: Subscribe to every committed mutation with `brain.onChange` — the in-process change feed behind live UIs, cache invalidation, and realtime sync. Covers the event shape, delivery guarantees, and catch-up patterns. next: - guides/optimistic-concurrency - guides/snapshots-and-time-travel --- # Reacting to Changes `brain.onChange(cb)` is Brainy's in-process change feed: subscribe once and receive one event per committed mutation — **every** mutation, regardless of how it happened. Direct calls, batch methods, `transact()`, imports, and Virtual Filesystem writes all funnel through the same commit point the feed is emitted from, so nothing slips past it. ```ts const off = brain.onChange((e) => { if (e.kind === 'entity') { console.log(`${e.op} ${e.entity?.type} ${e.id} @ generation ${e.generation}`) } }) await brain.add({ data: 'Ada Lovelace', type: 'person' }) // → "add person 0198... @ generation 42" off() // unsubscribe when done ``` ## The event ```ts interface BrainyChangeEvent { kind: 'entity' | 'relation' | 'store' op: 'add' | 'update' | 'remove' | 'relate' | 'unrelate' | 'updateRelation' | 'clear' | 'restore' id?: string entity?: { id: string; type: string; subtype?: string; metadata: Record; service?: string } relation?: { id: string; from: string; to: string; type: string; metadata?: Record } generation?: number timestamp: number } ``` - **Entity events** (`add` / `update` / `remove`) carry the post-commit indexed view — `type`, `subtype`, and the full custom `metadata`, so you can match your own `where`-style filters against events without a read. - **Deletes are fully described.** A `remove` or `unrelate` event carries the record's *last committed state* (sourced from the commit's own history record), not just an id. - **Batches emit per item.** `addMany` / `updateMany` / `relateMany` / `removeMany` emit one event per affected record; a `transact()` batch emits one event per item, all sharing the batch's single `generation`. - **Cascades are visible.** Removing an entity also emits `unrelate` for each relationship the delete cascaded to. - **Store-level events** (`kind: 'store'`) fire for the two wholesale operations — `clear()` and `restore()` — and mean *"everything may have changed; refetch what you care about."* ## Delivery guarantees - **Post-commit only.** An aborted write — a losing [`ifRev` compare-and-swap](optimistic-concurrency.md), a rejected transaction — never emits. If you received the event, the write is durable. - **Commit-ordered.** Events arrive in the order writes committed; `generation` is monotonic. - **Asynchronous, never blocking.** Delivery happens in a microtask after the write completes. A slow listener cannot delay a write; a throwing listener is logged and isolated from other listeners. - **Zero overhead when unused.** With no subscribers, the write path does no event work at all. - **Fire-and-forget.** There is no replay or backpressure. For catch-up after a disconnect, use the `generation` on each event together with [`asOf()` / the transaction log](snapshots-and-time-travel.md): record the last generation you processed, and on reconnect diff from there. For file content specifically, `vfs.readFile(path, { asOf })` and `vfs.history(path)` are the temporal read — see [Snapshots & Time Travel](snapshots-and-time-travel.md). ## Patterns **Cache invalidation** — drop cached reads for whatever changed: ```ts brain.onChange((e) => { if (e.kind === 'store') return cache.clear() if (e.id) cache.delete(e.id) }) ``` **Live queries (notify-and-refetch)** — re-run a query when a relevant change lands, rather than diffing incrementally: ```ts brain.onChange((e) => { if (e.kind === 'entity' && e.entity?.type === 'order') { refreshOpenOrdersView() // debounce as needed } }) ``` **Forwarding to other processes** — the feed is in-process by design. To push changes to browsers or other services, forward events through your own transport (WebSocket, SSE) from the process that owns the brain. ## Lifecycle `onChange` returns an unsubscribe function — call it when tearing down a subscriber (for example, when evicting a pooled instance). `brain.close()` drops all listeners; no events are delivered for or after `close()`.