import type { Buffers } from '../memory/index.js'; import type { ComponentDef, ComponentId, EntityHandle, Schema } from '@ecsia/schema'; import type { EntityRef } from '../entity/index.js'; import type { LiveQuery } from '../query/index.js'; import { ShapeKind } from './log.js'; import { ChangeVersionStore } from './change-version.js'; import type { StructuralRecord } from './structural-journal.js'; import type { ObserverHandle, ObserverHandler, ObserverTerm } from './observers.js'; export interface ReactivityDeps { readonly buffers: Buffers; readonly maxEntities: number; readonly indexBits: number; readonly logEntryWords: 1 | 2; readonly maxWritesPerFrame: number; readonly maxShapeChangesPerFrame: number; readonly shrinkRings: boolean; readonly dev: boolean; /** index → its (archetypeId, row) — for changeVersion stamping + the public predicate. */ resolveLocation(index: number): { archetypeId: number; row: number; }; /** The world frame tick (world owns it; reactivity reads it, never holds it). */ tick(): number; /** Advance the world frame tick (world.advanceTick). Called at frameReset. */ advanceTick(): void; /** Resolve a registered def's dense id. */ idOf(def: ComponentDef): ComponentId; /** Does `index` currently hold ALL of `componentIds`? */ holdsAll(index: number, componentIds: readonly ComponentId[]): boolean; /** The pooled EntityRef an observer event at `index` dispatches with — the stashed dying handle * while a rich pending-clear window covers the index, else the current handle (see * ObserverDeps.eventRefOf). */ eventRefOf(index: number): EntityRef; /** A Destroy entry for `index` was drained — pairs the oldest rich pending-clear stash with its * dead tenant so a LATER Create can be recognized as a re-mint (see onCreateDrained). */ onDestroyDrained(index: number): void; /** A Create entry for `index` was drained AFTER a Destroy: the index was re-minted inside this * observer window, so the oldest pending-clear tenant's remove events are all behind the drain * cursor (log order) and its stash window closes. */ onCreateDrained(index: number): void; /** index → its current FULL (generational) handle — for the structural journal's portable handles. */ resolveHandle(index: number): number; /** The accessor's shared write-path fast-out cell. Reactivity flips `.active` whenever a write consumer * (changed-flavor pointer / change observer) or changeVersion stamping (de)registers, so the setter's * `track()` can skip the handleIndex decode + closure hops when no consumer is present. */ readonly tracking: { active: boolean; }; } export declare class Reactivity { #private; constructor(deps: ReactivityDeps); /** Late-bind the single-entity maintenance hook (the query engine's maintainEntity). */ setMaintainHook(fn: (index: number, componentId: number) => void): void; /** Late-bind a "current matching members across all queries" source for the conservative path. */ setCurrentMembersSource(fn: () => Iterable): void; /** Late-bind the relations runtime's pairId→relationId resolver (createRelations installs it). */ setPairObserverResolver(fn: (pairComponentId: number) => number): void; /** +: push the write entry (when a consumer exists) and stamp changeVersion (when enabled). * Single-thread. The write log is read ONLY by changed-flavor query pointers and change observers; with * neither present every appended word is dead (rewound at frame-recycle), so the push is a pure cost on * the iteration hot path. Gate it on `#writeLogActive` (recomputed on flavor/observer (de)registration) — * a semantics-preserving fast-out, since a later-attached changed flavor's pointer starts at the live head * and sees only writes after it attaches. Pack the word inline (no per-write array). */ trackWrite(index: number, componentId: ComponentId, fieldIndex?: number): void; /**: append one shape entry. Main thread only, O(1). */ trackShape(index: number, componentId: ComponentId, kind: ShapeKind): void; /**: carries the target index in word B/C. `journal: false` marks an OBSERVER-ONLY carrier entry * (the exclusive valve's pair events ride the presence id) — it reaches the shape log (observers + * query maintenance) but NOT the structural journal: exclusive pairs serialize through the eid * column snapshot, and a presence-id pair record would corrupt the since-T delta replay. */ trackShapePair(index: number, pairId: ComponentId, targetIndex: number, kind: ShapeKind.AddPair | ShapeKind.RemovePair, journal?: boolean): void; /** * _PAYLOAD: a non-exclusive overflow pair's payload changed on an already-live pair. This is * NOT a membership change (no shape-log entry, no add/remove observer), but it IS a structural delta * the since-T stream must carry, so we record it in the persistent journal only * (overflow payload changes are explicit OP_PAIR_PAYLOAD records). */ trackShapeSetPayload(index: number, pairId: ComponentId, targetIndex: number): void; /** * The enqueueRemoveLog stub body: storage calls this for each component in fromArch \ toArch at * a migration, and for each held component at despawn (BEFORE removeRow + identity invalidation, * ). It emits a shape-log Remove entry — the single source for onRemove dispatch + Removed * delta maintenance. */ enqueueRemoveLog(index: number, componentId: ComponentId): void; /** ROLLBACK-ONLY: the changeVersion column owner, so @ecsia/rollback can image its stamps. */ get changeVersionStore(): ChangeVersionStore; /** Enable per-row changeVersion stamping (a `.changed` predicate consumer / serializer exists). */ enableChangeVersion(): void; /** * Enable the persistent structural journal (the since-T STRUCTURAL source). A delta serializer * that includes the structural section calls this once at construction — it is the structural twin of * `enableChangeVersion`. Until then, zero record cost. */ enableStructuralJournal(): void; /** * /: the structural ops committed with tick > since, in commit order, as portable full-handle * records. `gap` is true when `since` predates the bounded journal's live window (the caller must * resync from a fresh snapshot — the no-partial-apply delta-gap rule). */ drainStructuralSince(since: number): { records: StructuralRecord[]; gap: boolean; }; /**: "did any component on `handle` change since tick `since`?" (strict >). */ changedSince(handle: EntityHandle, since: number): boolean; /** * /: rows of `archetypeId` whose ENTITY's stamp is > since (the delta-serializer scan). * `indexOfRow` maps a live row of the archetype to its entity index — the stamp is keyed by entity * index (it follows the entity across relocations), so we resolve each row's current occupant. */ changedRows(_archetypeId: number, since: number, count: number, indexOfRow: (row: number) => number): Iterable; currentTick(): number; observe(term: ObserverTerm, handler: ObserverHandler): ObserverHandle; /**: is there a remove-observer on `componentId` (gates deferred row reclaim)? */ hasRemoveObserver(componentId: number): boolean; /** Any remove-observer at all — arms the deferred-despawn window (numeric location stash). */ get hasAnyRemoveObserver(): boolean; /** *: allocate the `changed` flavor's LogPointer + dedup bitset for `q`. `added`/`removed` lists * are owned by the LiveQuery itself and filled by maintenance; this hook wires only `changed`. */ attachChangedFlavor(q: LiveQuery, componentIds: Iterable): void; /** * _CHANGED: drain `q`'s write-log pointer, returning this frame's changed indices (deduped, * intersected with `q.current` and the query's filtered components). Write-log driven — never * consults changeVersion. */ drainChanged(q: LiveQuery): Uint32Array; /**: start of frame — advance the world tick, snapshot peak, recycle the rings. */ frameReset(): void; /**: merge per-worker write corrals into the shared ring (deterministic). No-op single-thread. */ mergeCorrals(): void; /** * /+: merge ONE worker's staged value writes into the shared write log. `pairs` is a * flat `[index, componentId, index, componentId, …]` buffer (the worker's raw corral payload); the * caller drives this in ASCENDING worker-index order so the merged stream is deterministic. We * (re)pack each pair through the module's own packWrite so single/wide layout stays the single * source of truth — the worker never duplicates the packing scheme. Writes flow into the SAME ring * the main thread appends to, so `.changed` filters and onChange observers fire for worker writes * exactly as for single-thread writes.: when changeVersion is enabled we also * stamp each row here (the worker hot path stays atomic-free; the stamp lands at the serial merge). */ mergeWorkerWrites(pairs: Int32Array | Uint32Array, count: number): void; /** * _STRUCTURAL: drain the shape log, re-testing each affected entity against the queries * referencing the changed component. In single-thread mode already maintains `current` * synchronously at the commit point, so this re-runs the same idempotent re-test off the log (the * drain is the spec's serial mechanism; the synchronous path is the optimization that agrees with * it). Add/remove coalesce within the frame because the drain happens once. */ maintainStructural(): void; /** _DRAIN: fire deferred observers from the saved shape/write pointers. */ observerDrain(): void; /** * Drop the rest of the in-flight observer window after a handler threw mid-drain. The consume * advances a pointer only AFTER its loop completes, so a throw leaves the cursor at the window's * start — but the sidecar's pending stashes and despawn ordinals are flushed in the caller's * finally regardless. Replaying the window's remainder next drain would then pair its Destroy * entries against the NEXT window's stashes (misattribution). The window is already lost (its * handlers partially ran); fast-forwarding both consumers to head keeps counters and cursors * consistent with the flushed sidecar state. */ abandonObserverWindow(): void; /** _LOGS: drain/merge spill (consumers already drained it), schedule next-frame resize. */ flushLogs(): void; } //# sourceMappingURL=reactivity.d.ts.map