import type { ComponentDef, ComponentId, RelationDef, Schema } from '@ecsia/schema'; import type { EntityRef } from '../entity/index.js'; export type ObserverKind = 'add' | 'remove' | 'change'; export type PairObserverKind = 'pair-add' | 'pair-remove'; export interface ObserverHandle { readonly id: number; dispose(): void; } /** A typed observer subscription term (the component set + the kind), mirroring the query DSL. */ export interface ComponentObserverTerm { readonly kind: ObserverKind; readonly components: readonly ComponentDef[]; } /** * A relation-level pair subscription: fires when ANY (subject, relation, target) pair of this * relation is added/removed, regardless of target. Produced by @ecsia/relations' onPairAdded / * onPairRemoved — core stores only the relation's numeric id (the acyclic boundary: core never * sees a RelationDef). Dispatch requires the relations runtime's pair resolver (installed at * createRelations); observing a pair term in a world without relations attached never fires. */ export interface PairObserverTerm { readonly kind: PairObserverKind; readonly relationId: number; } export type ObserverTerm = ComponentObserverTerm | PairObserverTerm; export interface ObserverContext { readonly kind: ObserverKind | PairObserverKind; /** For pair events this is the pair's SYNTHETIC component id (an implementation detail — * consumers wanting targets call rel.targetsOf(subject, R) for always-current truth). */ readonly component: ComponentId; readonly tick: number; } export type ObserverHandler = (e: EntityRef, ctx: ObserverContext) => void; export declare function onAdd(...components: ComponentDef[]): ComponentObserverTerm; export declare function onRemove(...components: ComponentDef[]): ComponentObserverTerm; export declare function onChange(...components: ComponentDef[]): ComponentObserverTerm; /** * Fires when ANY pair of `relation` is added on a subject — any target, including an exclusive * relation's retarget (which fires onPairRemoved for the old target, then onPairAdded for the new). * The handler gets the SUBJECT ref; for the current targets call rel.targetsOf(subject, relation) — * always-current truth, no stale-identity trap (the event itself carries only the subject). * Deferred like every observer: fires at the drain inside update(), never mid-mutation. */ export declare function onPairAdded(relation: RelationDef): PairObserverTerm; /** Fires when any pair of `relation` is removed on a subject — explicit removePair, an exclusive * retarget's implicit removal, or the despawn cascade tearing pairs down. See {@link onPairAdded}. */ export declare function onPairRemoved(relation: RelationDef): PairObserverTerm; export interface ObserverDeps { /** Resolve a registered def's dense id (throws if not registered with this world). */ idOf(def: ComponentDef): ComponentId; /** Does `index` currently hold ALL of `componentIds`? (multi-component add satisfaction). */ holdsAll(index: number, componentIds: readonly ComponentId[]): boolean; /** The pooled EntityRef an event for `index` dispatches with — bound to the tenant whose lifetime * the drain cursor is inside. While a rich pending-clear window covers the index that is the * stashed DYING handle (the handler reads the dead tenant's values, not a same-window re-mint's); * otherwise the current handle. Change events route here too: the change drain runs AFTER the * structural drain, so a pending window still open at that point covers an index that stayed dead * through the whole drain — its last-write events belong to the dead tenant. */ eventRefOf(index: number): EntityRef; /** The current frame tick. */ tick(): number; } export declare class ObserverRegistry { #private; constructor(deps: ObserverDeps); get hasObservers(): boolean; /** True iff any `change`-kind observer is registered — gates the write-log push fast-out. */ get hasChangeObservers(): boolean; /** True iff any pair-kind observer is registered — gates the drain's pairId→relationId resolve. */ get hasPairObservers(): boolean; /** True iff ANY component remove-observer (`onRemove`) is registered — arms the deferred-despawn * window (the numeric location stash + ordinal counting) even in a rich-free world. Pair-remove * observers (`onPairRemoved`) do NOT arm it: this window serves component reads of a dying entity, * and the pair-observer subject leg is a separate concern. */ get hasAnyRemoveObserver(): boolean; /** True iff any observer subscribes to `kind` events on `componentId` — gates deferred-row reclaim. */ hasKindFor(kind: ObserverKind, componentId: number): boolean; observe(term: ObserverTerm, handler: ObserverHandler): ObserverHandle; /** Fire a structural (add/remove) event for one (index, componentId). */ dispatchStructural(kind: 'add' | 'remove', index: number, componentId: number): void; /** * Fire a relation-level pair event for one (subjectIndex, relationId). `pairComponentId` is the * synthetic pair id the shape entry carried — exposed on ctx for symmetry, an implementation * detail otherwise (target resolution belongs to rel.targetsOf, always-current truth). */ dispatchPair(kind: PairObserverKind, index: number, relationId: number, pairComponentId: number): void; /** Fire a change event for one (index, componentId), deduped per frame. */ dispatchChange(index: number, componentId: number): void; /**: an overflow forces every change observer to assume worst-case. */ fireAllChangeConservatively(current: Iterable): void; /** Clear per-frame change dedup state (called at the start of each observer drain). */ resetChangeDedup(): void; } //# sourceMappingURL=observers.d.ts.map