/** * Generic per-instance **observe** bus. Observe-class * services (devtools inspector, audit, sync-dirty notification) register * handlers against named lifecycle points instead of the kernel naming each * service. Mirrors the registry pattern of {@link WriteHookRegistry} but is * internal and keyed by lifecycle point. * * OBSERVE SEMANTICS: handlers react to a write that already happened. A * handler throw is warned, not propagated — it can never abort a write. Write- * *gating* services (guards, periods) need a throw-propagating gate bus. * Add observe points by extending {@link LifecycleEventMap}. Write-*gating* * services use the sibling gate API on this same class * (`registerGate`/`dispatchGate`, throw-propagating); see {@link GateEventMap}. * * @module */ import type { WriteEvent } from './write-hooks.js'; import type { Role } from '../../kernel/types.js'; /** Typed map of OBSERVE lifecycle point → event payload. Extend by adding keys. */ export interface LifecycleEventMap { afterPut: WriteEvent; afterDelete: WriteEvent; } export type LifecyclePoint = keyof LifecycleEventMap; export type BusHandler

= (event: LifecycleEventMap[P]) => void | Promise; export type Unsubscribe = () => void; /** Payload for a `beforePut` gate — carries the data guards and periods need to validate or reject a write. */ export interface GatePutEvent { readonly op: 'create' | 'update'; readonly vault: string; readonly collection: string; readonly docId: string; /** * The record about to be written (pre schema-validation). Money fields * are presented in their canonical decoded form — equal on both * sides for an unchanged value, regardless of how the caller wrote them. */ readonly incoming: unknown; /** * Decrypted prior record, or null on create / when prior is unreadable. * Money fields are decoded to the canonical decimal `get()` shape, NOT * the stored scaled-int — `incoming[f] === existing[f]` holds * for an unchanged money field. */ readonly existing: unknown; /** Prior envelope version, or 0 when none. */ readonly existingVersion: number; /** Prior envelope timestamp (`_ts` ISO string), or undefined when none — periods compares against this. */ readonly existingTs: string | undefined; readonly userId: string; readonly role: Role; /** * Names of fields whose values are schema-owned computed fields for this * collection. Gate handlers (e.g. `frozenFields`) must skip these: the * incoming record is the raw user input (computed fields not yet evaluated), * so comparing `existing[computedField]` vs `incoming[computedField]` * would always see a change even when the computed result is unchanged. */ readonly computedFieldNames?: ReadonlySet; } /** Payload for a `beforeDelete` gate. Like {@link GatePutEvent} without `incoming`. */ export interface GateDeleteEvent { readonly vault: string; readonly collection: string; readonly docId: string; /** True for system-internal (housekeeping) deletes — handlers branch on this. */ readonly internal: boolean; /** Decrypted prior record; money fields decoded to the canonical `get()` shape. */ readonly existing: unknown; readonly existingVersion: number; readonly existingTs: string | undefined; readonly userId: string; readonly role: Role; } /** Typed map of GATE lifecycle point → event payload. Extend by adding keys. */ export interface GateEventMap { beforePut: GatePutEvent; beforeDelete: GateDeleteEvent; } export type GatePoint = keyof GateEventMap; export type GateHandler

= (event: GateEventMap[P]) => void | Promise; /** * Registration options for a gate handler. * * `needsPrior` (default `true`) declares whether the handler reads the * prior-derived event fields — `existing`, `existingVersion`, `existingTs`, * and (on `beforePut`) `op`. When EVERY handler registered at a point set * `needsPrior: false`, the write path skips the prior-read (store get + * decrypt) entirely and dispatches the event with `existing: null`, * `existingVersion: 0`, `existingTs: undefined` (and `op: 'create'` on * `beforePut`; a `beforeDelete` gate then also fires for delete-of-absent). * A handler that opts out MUST NOT rely on those fields. (#267 prior-read * elision — pure perf; one prior-needing handler restores the old behavior * for the whole point.) */ export interface GateRegisterOptions { readonly needsPrior?: boolean; } export declare class ServiceBus { #private; /** Register a handler for an observe point. Returns an unsubscribe fn. */ register

(point: P, handler: BusHandler

): Unsubscribe; /** Cheap gate for the write path — true when any handler is registered for the point. */ hasHandlers(point: LifecyclePoint): boolean; /** * True while one or more dispatches are in flight. Backed by a depth counter * so that two concurrent async dispatches (`Promise.all([put('a'), put('b')])` * each captured `busAfterPut=true` at their respective put() tops while depth * was 0) both proceed independently — the counter stays > 0 until BOTH finish, * so any nested write attempted by a handler still sees `dispatching === true` * and is suppressed by the write-path gate in `collection.ts` * (`busAfterPut = hasHandlers('afterPut') && !dispatching`). Re-entrancy * suppression lives exclusively on that write-path gate; concurrent independent * dispatches must not drop each other's events. */ get dispatching(): boolean; /** * Dispatch in registration order, awaited. Per-handler errors are warned, not * thrown — an observe handler must never abort a completed write. A * re-entrancy guard suppresses nested firing so a handler that itself writes * cannot loop (same rationale as WriteHookRegistry.#suppressed). */ dispatch

(point: P, event: LifecycleEventMap[P]): Promise; /** * Register a write-gating handler. A throw from the handler ABORTS the * write. Returns an unsubscribe fn. See {@link GateRegisterOptions} for * the `needsPrior` prior-read declaration (#267). */ registerGate

(point: P, handler: GateHandler

, opts?: GateRegisterOptions): Unsubscribe; /** Cheap gate for the write path — true when any gate handler is registered for the point. */ hasGateHandlers(point: GatePoint): boolean; /** * True when at least one gate handler at `point` needs the prior record * (the default). False when the point has no handlers or every handler * registered with `needsPrior: false` — the write path then skips the * prior-read before dispatching (#267 prior-read elision). */ gateNeedsPrior(point: GatePoint): boolean; /** * Run gate handlers in registration order, awaited. Unlike `dispatch` * (observe), a handler throw is NOT swallowed — it PROPAGATES, aborting the * write before it reaches the store. The first throw stops the remaining * handlers (fail-fast). This is the seam guards/periods migrate onto. * * Note: gate handlers are validators that read, not write. A gate handler * that writes back into the same collection would re-enter the write path * and re-dispatch this point; loop-suppression for that case is deferred to * the migration slice (contract: gate handlers must not perform writes that * re-trigger their own point). */ dispatchGate

(point: P, event: GateEventMap[P]): Promise; } /** @deprecated Use ServiceBus. */ export declare const SubsystemBus: typeof ServiceBus; /** @deprecated Use ServiceBus. */ export type SubsystemBus = ServiceBus;