import type { ArchetypeId, ComponentDef, ComponentId, EntityHandle, QueryTerm, Schema } from '@ecsia/schema'; import type { ColumnSet } from '../component/index.js'; import type { Archetype } from '../storage/index.js'; import type { TypedArray } from '../memory/index.js'; import type { CompiledQuery, RowFilterTerm } from './compile.js'; import type { SparseSetU32 } from './sparse-set.js'; import type { PinnedFactory } from './codegen.js'; declare const NO_HANDLE: EntityHandle; /** The pooled element handed to each callback: value props (per value term) + the entity handle. */ type PooledElement = Record & { handle: EntityHandle; }; /** The reactivity write-log hooks the Changed flavor drives. */ export interface ReactivityQueryHooks { /**: allocate this query's changed-flavor LogPointer + dedup bitset (idempotent). */ attachChangedFlavor(q: LiveQuery, componentIds: Iterable): void; /**: drain this frame's changed entity indices (deduped, intersected with `current`). */ drainChanged(q: LiveQuery): Uint32Array; } export interface LiveQueryDeps { /** index → its (archetypeId, row) — the entity record. */ resolveLocation(index: number): { archetypeId: number; row: number; }; /** index → its full EntityHandle (generation from the entity layer). */ handleOf(index: number): EntityHandle; /** * Dev-only re-entrancy guard: the world arms a flag for the duration of an each/eachChunk/iterator * run so its structural mutators can throw on a mutation that would corrupt the live iteration. */ beginIteration(): void; endIteration(): void; /** * The entity indices currently resident in cold archetype `archetypeId`. Backed by the * ColdStore's per-archetype membership so cold iteration is O(rows in that archetype), NOT * O(|current|) per cold archetype. */ coldResidentsOf(archetypeId: number): Iterable; /** The cold per-TYPE ColumnSet for `componentId` (cold blocks are keyed by component, not archetype). */ coldColumnSet(componentId: ComponentId): ColumnSet | undefined; /** The cold-block row holding `componentId`'s fields for entity `index`, or -1 if absent. */ coldRowOf(index: number, componentId: ComponentId): number; } /** * Row-filter test for a COLD resident. Cold blocks store the presence component's eid target column * keyed (componentId, index), so resolve it via coldColumnSet + coldRowOf rather than the hot * archetype's columnSets. Cold status is a hot-budget decision, independent of relations, so a * row-filtered archetype can be cold — the cold iteration AND the engine's cold seed/maintain must * all run this so each()/count/has agree on which residents pass. */ export declare function passesColdRowFilters(deps: Pick, index: number, filters: readonly RowFilterTerm[]): boolean; export declare class LiveQuery { #private; readonly compiled: CompiledQuery; readonly terms: readonly unknown[]; readonly matchingArchetypes: Archetype[]; readonly current: SparseSetU32; lastMatchTick: number; constructor(compiled: CompiledQuery, terms: readonly unknown[], current: SparseSetU32, byId: Archetype[], deps: LiveQueryDeps, queryFn?: ((terms: readonly QueryTerm[]) => LiveQuery) | null); get count(): number; /** * Derive a narrower query: sugar over `world.query([...this.terms, ...terms])`, riding the same * canonical-hash dedup — deriving is REFERENCE-identical to writing the combined query directly * (the hash is order-independent AND duplicate-tolerant, so chaining order is irrelevant too). * No new matching machinery; flavors (.added/.removed/.changed) are per cached query state, NOT * inherited from this one. Re-deriving a term already present collapses in the hash — deriving * write(P) from [read(P)] IS the cached query for [write(P)] (one shared LiveQuery; the * read/write role split stays per value-signature binding). */ derive(...terms: readonly QueryTerm[]): LiveQuery; /** Chainable flavor declarations. Allocate nothing for undeclared flavors. */ added(): this; removed(): this; /** Install the reactivity write-log hooks (world wiring); enables the Changed flavor. */ __setReactivity(hooks: ReactivityQueryHooks): void; changed(...components: ReadonlyArray): this; /** Reset per-frame transient flavor lists (FRAME_RESET). */ frameReset(): void; /** The value-role signature: the P/W-tagged value subset, read-vs-write distinguished. */ static valueSignature(compiled: CompiledQuery): string; ensureValueSignature(compiled: CompiledQuery): void; /** Append a newly-created (or seeded) archetype to the pointer cache. */ addMatchingArchetype(arch: Archetype): void; /**: add an entity index to `current`, recording a NET `added` delta where declared. */ addEntity(index: number): void; /**: remove an entity index from `current`, recording a NET `removed` delta where declared. */ removeEntity(index: number): void; /** The hot loop: walk matchingArchetypes contiguous rows, poke the accessor singletons, call fn. */ each(fn: (e: PooledElement) => void): void; /** * Opt-in column-cursor iteration (the SoA fast path). Instead of the per-row pooled element + * accessor getters/setters, `eachChunk` hands the callback ONE reused {@link QueryChunk} per matched * (non-cold) archetype, exposing the raw typed column views + a contiguous row span. The caller indexes * `view[row]` directly (stride-1 scalars) — bypassing the accessor decode/encode AND the per-write * change-tracking push, so it lands close to a raw SoA loop. Cold archetypes (no contiguous columns) * are NOT visited by `eachChunk`; mix `each`/`eachCold` if a query can fragment. Writes through the * cursor are NOT recorded in the reactivity write log: a `.changed`/observer consumer will not see them * — use `each` when reactivity must observe the write. * * The chunk and its column lookups are reused across calls/archetypes — do NOT store the chunk or a * returned view across iterations. */ eachChunk(fn: (chunk: QueryChunk) => void): void; /** * Pinned columns — the fastest iteration path, ~0.7× bitECS on the canonical bench (measured). * Where {@link eachChunk} re-resolves column views every call, `bindColumns` resolves each * `[ComponentDef, fieldName]` spec ONCE per matched hot archetype and mints that archetype's runner * — a persistent closure capturing the views as constants, which V8 embeds into optimized code. The * returned `run(ctx)` walks the bindings (matching `eachChunk` iteration order: cold archetypes * never visited, empty ones skipped) with one cheap safety check per binding per call. * * Per-archetype runners are CODEGEN'D (each recompiled into a distinct function so it stays a * specialized V8 singleton) where the runtime allows `new Function`; under strict CSP / a locked * sandbox it transparently falls back to the interpreted factory call. The codegen path is gated on * a pre-flight equality check against the interpreted runner, so it is a pure speed win that can * never change results. Because codegen recompiles fresh on growth, there is **NO post-growth * penalty** — the loop holds ~1.0 ns/entity even after columns re-back, with no pre-sizing required. * * Contract: * 1. The factory must be **self-contained** — it may close over NOTHING from its outer scope (the * recompiled copy only sees globals). Pass per-frame inputs through the runner's `ctx` argument * (hoist them to a local const before the loop: `const dt = ctx.dt`); define fixed constants * inside the factory body. A factory that closes over outer scope fails the pre-flight and falls * back to interpreted (correct, just unspecialized). * 2. `meta` is identity-stable; read `meta.count` (the live row count) inside the runner — population * churn (spawn/despawn) never re-invokes the factory. * * ```ts * const run = q.bindColumns<{ dt: number }>( * [Position, 'x'], [Velocity, 'dx'], * ([px, dx], meta) => (ctx) => { * const dt = ctx.dt // hoist per-frame inputs out of the loop * const count = meta.count * for (let i = 0; i < count; i++) px[i] += dx[i] * dt * }, * ) * run({ dt: 1 / 60 }) // per frame * ``` * * The factory is re-invoked ONLY when a bound column re-backs (growth) or the matched-archetype set * changes (a new archetype matched, or a matched cold archetype was warm-promoted) — never on * population change. * * Vec fields hand their raw view through: row `r` occupies `[r*stride, (r+1)*stride)` where the * stride is the declared vec arity (`vec3()` → 3). Hardcode it, or read `meta.strides[specIndex]` * ONCE outside the hot loop (`const s = meta.strides[0]`) so the loop never repeats the lookup — * the same value the `eachChunk` cursor exposes via `stride()`, def-invariant across archetypes. * Rich fields (`'string'`/`object`) carry no column and throw at bind time, as do row-filtered * queries (a pinned runner cannot skip rows; `eachChunk` silently skips, but a silently-skipping * pinned runner is a footgun) and specs naming a component the query does not REQUIRE (an * optional or unreferenced component may be absent from a future matching archetype, which would * only surface mid-run). Writes through pinned views are NOT recorded in the reactivity * write log: a `.changed`/observer consumer will not see them — use `each` when reactivity must * observe the write. Structural changes during `run()` follow the `eachChunk` discipline: collect, * then mutate after the loop (despawn swap-removes rows under the runner's feet). */ bindColumns(...args: [ ...specs: ReadonlyArray, string]>, factory: PinnedFactory ]): (ctx: Ctx) => void; /** * Compile an ergonomic `.each` body into the fast column loop `bindColumns` runs — without you naming * columns or restating the math. `compile` reads the callback's own source, rewrites `e..` * to direct typed-array indexing, and codegens a specialized per-archetype loop. The result keeps the * readable accessor syntax but lands near `eachChunk` (~1.5 ns/entity) instead of paying the per-row * proxy tax (~10 ns/entity). * * Unlike `bindColumns`, this path PRESERVES reactivity: a component the body writes is recorded in the * write log exactly as the accessor setter would, so `.changed()` filters and observers see it — for * free when no consumer is registered (the same gate the accessor uses), at write-log cost when one is. * * It is a pure SPEEDUP: the analyzer is conservative and falls back to the unchanged proxy `.each` (so * results are always identical) whenever it cannot prove the rewrite safe — a non-straight-line body * (`if`/`?`/`&&`/`return`/loops/nested fns), a string/comment/template, a non-numeric-scalar field * (vec/bool/eid/bigint/rich), a component the query does not REQUIRE, any `e` use other than * `e..`, a per-row `ctx` write, a row-filtered query, or a runtime that blocks `new * Function` (strict CSP). Call it ONCE and reuse the returned runner per frame. * * ```ts * const run = q.compile<{ dt: number }>((e, ctx) => { * e.position.x += e.velocity.dx * ctx.dt * e.position.y += e.velocity.dy * ctx.dt * }) * run({ dt: 1 / 60 }) // per frame — same result as q.each(e => ...), faster * ``` */ compile(body: (e: PooledElement, ctx: Ctx) => void): (ctx: Ctx) => void; [Symbol.iterator](): Iterator; eachAdded(fn: (e: PooledElement) => void): void; eachRemoved(fn: (index: number, handle: EntityHandle) => void): void; /** *: the Changed filter reads the reactivity WRITE LOG, not changeVersion. The write log lands * at; until then there are no recorded writes, so the changed set is empty. The matching surface * (binding the cursor per changed index) is in place for to fill the index source. */ eachChanged(fn: (e: PooledElement) => void): void; } /** * The reused per-archetype cursor handed to {@link LiveQuery.eachChunk}. Exposes one matched archetype's * raw SoA columns + its contiguous row span. Resolve each column ONCE before the inner row loop: * * ```ts * q.eachChunk((c) => { * const px = c.column(Position, 'x'), py = c.column(Position, 'y') * const dx = c.column(Velocity, 'dx'), dy = c.column(Velocity, 'dy') * for (let i = 0; i < c.count; i++) { px[i] += dx[i] * dt; py[i] += dy[i] * dt } * }) * ``` * * `column` returns the live typed view; `stride` is the slots-per-row (>1 for vec fields, where row `r` * starts at `r * stride`). The cursor (and every returned view) is reused across archetypes/calls — do NOT * store either across iterations. Writes here bypass the reactivity write log (see `eachChunk`). */ export declare class QueryChunk { #private; /** @internal — point the chunk at one matched archetype (the cursor loop owns this). */ __bind(arch: Archetype): void; /** Rows in this chunk (the archetype's dense row count). Iterate `0..count`. */ get count(): number; /** The dense row→EntityHandle list for this chunk (row `r`'s entity is `entities[r]`). */ get entities(): Uint32Array; /** The live typed column view for `def.field` in this chunk. Stride-1 scalars index by row directly. */ column(def: ComponentDef, field: string): TypedArray; /** Slots per row for `def.field` (1 scalar, N vec): row `r` starts at `r * stride`. */ stride(def: ComponentDef, field: string): number; } export type { PooledElement }; export { NO_HANDLE }; export type { ArchetypeId, ComponentId, Schema }; //# sourceMappingURL=live-query.d.ts.map