import type { ResolvedWorldOptions, WorldOptions } from './config.js'; import type { EntityGeneration, EntityHandle, EntityIndex, EntityRef, EntityReservation, HandleLayout, HandleStats } from './entity/index.js'; import type { ComponentDef, ComponentId, RelationDef, RelationId, Schema, SpawnArg, SpawnArgFor } from '@ecsia/schema'; import type { SharedHandleManifest } from './memory/index.js'; import type { AccessorWorld } from './component/index.js'; import type { ResolvedPair } from './query/index.js'; import type { ColumnSet } from './component/index.js'; import type { Buffers as BuffersType, ColumnGrowthLog } from './memory/index.js'; import type { ObserverHandle, ObserverHandler, ObserverTerm } from './reactivity/index.js'; import type { WorldQuery } from '@ecsia/schema'; import type { SerializationSurface, SerializeRelationProvider } from './serialize-surface.js'; import type { InspectSurface } from './inspect-surface.js'; import type { RelationsRollbackProvider, RollbackHost } from './rollback-surface.js'; import { Topics } from './topics/index.js'; import type { TopicDef, TopicEventInit } from './topics/index.js'; /**: 'serial' during the serial slot (and always, single-threaded); 'wave' only while the scheduler dispatches worker waves. */ export type WorldPhase = 'serial' | 'wave'; /** * The ComponentId-keyed structural-apply verbs the command-buffer flush path drives. * Built from the world's storage + registry; every call runs serial/main-thread. */ export interface WorldApplySurface { defOf(id: ComponentId): ComponentDef | undefined; addMany(handle: EntityHandle, defs: readonly ComponentDef[]): void; removeMany(handle: EntityHandle, defs: readonly ComponentDef[]): void; /** Overwrite `def`'s fields on `handle`'s current row + emit the `.changed` write-log entry. */ writePayload(handle: EntityHandle, def: ComponentDef, values: Record): void; /** * Relation apply. Filled by `__installRelations`; undefined in a * relation-free world. The scheduler's command-apply path calls these for OP_ADD_PAIR / * OP_REMOVE_PAIR — it does NOT import relations (the acyclic boundary holds). */ addPair?(subject: EntityHandle, relationId: RelationId, target: EntityHandle, payload: Record | undefined): void; removePair?(subject: EntityHandle, relationId: RelationId, target: EntityHandle): void; } /** * The minimal core surface the relations runtime drives. The world exposes it through * `__installRelations` so `@ecsia/relations` attaches WITHOUT @ecsia/core ever importing it (the * acyclic dependency direction, seam). Everything here is serial / main-thread. */ export interface RelationsHost { /** Mint the next dense ComponentId. */ allocSyntheticId(): ComponentId; /** Intern a synthetic ComponentDef (presence/overflow) at a minted id so storage can build its columns. */ registerSynthetic(def: ComponentDef, id: ComponentId): void; /** * Re-point an already-interned synthetic def at a different id. Lets relations keep ONE canonical * def per (relation, target) pair across a rollback that rewound the minting counter, instead of * interning a fresh def — and a fresh def per rollback would grow `defOf`/`idOf` without bound. */ rebindSynthetic(def: ComponentDef, id: ComponentId): void; /** id → registered def (real or synthetic). */ defOf(id: ComponentId): ComponentDef | undefined; /** ONE migration adding several ids (relations atomicity; archetype-storage ). */ migrateAddingMany(handle: EntityHandle, defs: readonly ComponentDef[]): void; migrateRemovingMany(handle: EntityHandle, defs: readonly ComponentDef[]): void; /** O(1) membership of a (possibly synthetic) component id via the per-entity bitmask. */ bitmaskHas(index: number, id: ComponentId): boolean; /** Resolve the live ColumnSet + current row for `def` on `handle`, or null if absent / not hot. */ columnSetFor(handle: EntityHandle, def: ComponentDef): { set: ColumnSet; row: number; } | null; /** * Like columnSetFor, but COLD-capable: a cold resident resolves to its per-type overflow block + * cold row (the same resolution storage's migrate path uses). Null only for dead handles, tags, * or components the entity does not hold. The prefab copy path reads/writes through this so * templates and instances parked in cold archetypes copy correctly. */ fieldLocationFor(handle: EntityHandle, def: ComponentDef): { set: ColumnSet; row: number; } | null; /** Entity-layer helpers. */ isAlive(handle: EntityHandle): boolean; handleIndex(handle: EntityHandle): number; handleOfIndex(index: number): EntityHandle; /** Re-enter the despawn protocol for a cascaded victim. */ despawn(handle: EntityHandle): void; /** * The auto-registered `Prefab` tag def when the world was created with `prefabs: true`; null * otherwise. The relations runtime keys the prefab API (definePrefab / spawnFrom / IsA) off it. */ readonly prefabDef: ComponentDef | null; /** The component ids in `handle`'s current archetype signature (a copy); [] for a dead handle. */ componentIdsOf(handle: EntityHandle): readonly number[]; /** Mint a live handle in the EMPTY archetype — the spawn step of definePrefab / spawnFrom. */ spawnEmpty(): EntityHandle; /** * Copy `componentId`'s rich (sidecar) fields from src's slot to dst's. Only PRESENT (written) * slots copy, so a never-touched field stays defaulted on dst. object fields copy the * REFERENCE — sidecar assignment semantics, same as a plain write. */ copyRichFields(src: EntityHandle, dst: EntityHandle, componentId: ComponentId): void; /** * Template-spawn deferral seam (definePrefab / spawnFrom): while an observer drain is running, * reserve a live handle NOW (so the handler gets a usable handle back — the spawnWith staging * model) and stage `build` to run against it at the next serial flush, after placement into the * EMPTY archetype. Returns null when no drain is in flight — the caller spawns and builds * directly. */ deferTemplateSpawn(build: (handle: EntityHandle) => void): EntityHandle | null; /** * True while an observer drain is deferring OR staged ops await the next serial flush. Lets the * relations dev errors distinguish a reserved (staged, not-yet-materialized) template handle — * alive but with an empty signature — from a genuinely untagged entity. */ hasPendingDeferred(): boolean; /** * deferral seam: while an observer drain is running, structural relation * ops issued by a handler must STAGE to the world's deferred command buffer rather than mutate the * world mid-drain. The relations runtime calls this at the top of addPair/removePair; if it returns * true the op was staged (the runtime returns immediately) — else the runtime applies it directly. */ deferRelationOp(op: 'add' | 'remove', subject: EntityHandle, relation: RelationDef, target: EntityHandle, payload?: Record): boolean; /** Push a write-log entry for a `.changed`-tracked pair-payload write. */ trackWrite(index: number, componentId: ComponentId): void; /** * Re-test entity `index` against every query referencing `componentId`, adding/removing it from * each query's membership. The exclusive-relation valve writes its eid target column WITHOUT a * migration (and on first attach, AFTER the migration that already maintained at the -1 default), * so a row-filtered query (`rel.Pair(R, target)`) only reflects the attach/retarget when the runtime * re-tests here after writing the eid. */ maintainEntity(index: number, componentId: ComponentId): void; /** Push an ADD_PAIR / REMOVE_PAIR shape-log entry. `journal: false` = observer-only carrier * (the exclusive valve's pair events on the presence id) — shape log yes, structural journal no. */ trackShapePair(index: number, pairId: ComponentId, targetIndex: number, add: boolean, journal?: boolean): void; /** * Journal a SET_PAYLOAD structural op for a non-exclusive overflow pair whose payload changed on an * already-live pair (overflow payload changes are explicit OP_PAIR_PAYLOAD * records, since they do not live in an archetype column the changed-row scan covers). */ trackShapeSetPayload(index: number, pairId: ComponentId, targetIndex: number): void; /** The SAB-capable buffers registry, for the non-exclusive overflow payload ColumnSet. */ readonly buffers: BuffersType; readonly accessorWorld: AccessorWorld; /** Register a preDespawn hook fired BEFORE the row is torn down. */ setPreDespawn(hook: (dying: EntityHandle) => void): void; /** Inject the Pair(...) term resolver into the query compiler. */ setPairResolver(resolve: (relationId: number, target: number | symbol) => ResolvedPair): void; /** * Inject the pair-observer resolver (synthetic pair ComponentId → RelationId, -1 = not a pair) * so the deferred drain can dispatch relation-level onPairAdded/onPairRemoved terms. */ setPairObserverResolver(resolve: (pairComponentId: number) => number): void; /** Fill the OP_ADD_PAIR / OP_REMOVE_PAIR apply seam so the scheduler can drive relations. */ setApplyPair(addPair: (subject: EntityHandle, relationId: RelationId, target: EntityHandle, payload: Record | undefined) => void, removePair: (subject: EntityHandle, relationId: RelationId, target: EntityHandle) => void): void; /** * Install the relation snapshot/apply provider the serialization module reads through `world.__serialize` * Lets @ecsia/serialization enumerate live pairs and re-establish them * WITHOUT importing @ecsia/relations directly off the world — the relations runtime hands the provider * in, core relays it, the acyclic boundary holds. */ setSerializationProvider(provider: SerializeRelationProvider): void; /** * Install the relation capture/restore leg @ecsia/rollback drives through `world.__installRollback`. * Same relay shape as the serialization provider: relations hands it in, core only stores it, and a * relation-free world leaves it unset (rollback then has nothing extra to capture). */ setRollbackProvider(provider: RelationsRollbackProvider): void; readonly maxEntities: number; readonly indexBits: number; } export interface World { /** Fully-resolved, validated configuration (frozen). */ readonly options: ResolvedWorldOptions; /** Structural-change phase. Owned by the world; the scheduler is the only component that flips it to 'wave'. */ readonly phase: WorldPhase; /** * Phase-flip seam. The world OWNS the field and seeds it to 'serial'; * the scheduler is the SOLE caller of this, flipping to 'wave' across worker dispatch and back to * 'serial' for the merge/apply flush slot. @ecsia/core never calls it (the acyclic boundary holds — * the scheduler drives the world externally). Not for user code. */ __setPhase(phase: WorldPhase): void; /** * Command-buffer apply seam (`spawnReserved`). Place a handle that was * already made alive by `reserveEntityBlock` into the EMPTY archetype and * emit its Create shape entry — the deferred analogue of `world.spawn()` whose identity was minted * before the wave. Serial-phase only. Called ONLY by the scheduler's flush path. Not for user code. */ __spawnReserved(handle: EntityHandle): void; /** * Command-buffer apply surface, exposed as ONE seam so the scheduler's flush * path drives the same storage primitives a main-thread direct-apply would, keyed by ComponentId. * Serial-phase only; called ONLY by the scheduler. Not for user code. */ readonly __apply: WorldApplySurface; /** * Relations attach seam. `@ecsia/relations` calls this ONCE to obtain the core * surface it drives (synthetic id minting, migrate-many, preDespawn hook, pair-resolver injection, * OP_ADD_PAIR apply). @ecsia/core never imports @ecsia/relations — the host is handed OUT, keeping * the dependency direction acyclic. Not for user code; call createRelations(world) instead. */ __installRelations(): RelationsHost; /** Export the SAB buffer-set manifest for one-time worker transfer. */ __exportShared(): SharedHandleManifest; /** * Column re-backing journal. The scheduler reads * `.generation` once per wave (a cheap monotonic int) and only `.drain()`s when it advanced — the * notices it gets are the new SAB backings the worker pool must re-wrap at the wave fence before the * next dispatch. In-place `.grow()` never bumps the generation (length-tracking views auto-widen). * Serial / main-thread; called ONLY by the scheduler. Not for user code. */ __columnGrowth(): ColumnGrowthLog; /** * Serialization seam. @ecsia/serialization reads archetype columns + the * registry + the relation provider through this, and drives deserialize-side spawn/migrate. Serial / * main-thread only; @ecsia/core never imports @ecsia/serialization (acyclic boundary). Not for user code. */ readonly __serialize: SerializationSurface; /** * Read-only introspection seam (@ecsia/devtools). Exposes the FULL archetype census (cold + * empty included) and the live-query enumeration — the two data an inspector needs that `__serialize` * (snapshot-shaped: hot, non-empty only) and the public query surface do not reach. Pure reads, serial * / main-thread; @ecsia/core never imports @ecsia/devtools (acyclic boundary). Not for user code. */ readonly __inspect: InspectSurface; /** * Rollback attach seam. `@ecsia/rollback` calls this ONCE to obtain the core state a handle-stable * whole-world image must reach (entity identity, the bitmask, the archetype census, changeVersion, * the tick setter). Handed OUT like `__installRelations`, so core never imports the mechanism and a * world that never rolls back ships none of it. Serial / main-thread. Not for user code; call * createRollbackSurface(world) from @ecsia/rollback instead. */ __installRollback(): RollbackHost; /** * Merge ONE worker's staged value writes into the reactivity write log ( * ). `pairs` is the worker's raw `[index, componentId, …]` corral payload and `count` the number * of `(index, componentId)` entries. The scheduler drives this in ASCENDING worker-index order in * the serial flush slot so onChange observers + `.changed` filters fire for worker writes * deterministically. Serial-phase only; called ONLY by the scheduler. Not for user code. */ __mergeWorkerWrites(pairs: Int32Array | Uint32Array, count: number): void; /** * Topic store seam. The scheduler registers `publish:`/`consume:`-declared topics, stages * `ctx.publish` events, drives the per-wave canonical merge, and routes OP_PUBLISH records here. * Serial / main-thread; not for user code — the public surfaces are `world.publish` and the * system context's `publish`/`consume`. */ readonly __topics: Topics; /** Current frame tick. Advanced by reactivity at frame reset. */ readonly tick: number; /** Alias for `tick`. */ currentTick(): number; /** Create a new entity with the empty signature. Main-thread/serial. O(1). */ spawn(): EntityHandle; /** * Create a new entity and add the given components in ONE migration (EMPTY → target signature), * never N. Main-thread/serial. * * Each argument is either a bare `ComponentDef` (membership only) or a `[def, values]` tuple that * also INITIALIZES the component: `spawnWith([Position, { x: 1, y: 2 }], Velocity)` spawns the entity, * migrates it to the target signature once, then writes the supplied values through the normal tracked * accessor path (so onChange/write-log fire). The value object is type-inferred from the def's schema. */ spawnWith(...specs: { [I in keyof T]: SpawnArgFor; }): EntityHandle; /** Add a component to a live entity (single migration via the cached edge). Main-thread/serial. */ add(handle: EntityHandle, def: ComponentDef): void; /** Remove a component from a live entity (single migration via the cached edge). Main-thread/serial. */ remove(handle: EntityHandle, def: ComponentDef): void; /** Explicit cold→hot archetype promotion at a serial flush point. */ warm(...defs: readonly ComponentDef[]): void; /** Destroy an entity. Main-thread/serial. Idempotent on dead handles. */ despawn(handle: EntityHandle): void; /** O(1) liveness/staleness check. Never consults the bitmask. */ isAlive(handle: EntityHandle): boolean; /** * O(1) component membership point-test via the per-entity bitmask. * Main-thread/serial only. Returns false for a dead handle (liveness checked first, * without reading the bitmask). */ has(handle: EntityHandle, def: ComponentDef): boolean; /** * Resolve the pooled EntityRef for `handle`; throws on a dead handle unless `{ lenient: true }` * `spawnWith(...defs)` is the other; it is * intentionally deferred to storage, which owns target-signature computation and the * single migration — the handle mint here is meaningless without an archetype to land in. */ entity(handle: EntityHandle, opts?: { lenient?: boolean; }): EntityRef; /** Pre-reserve a block of live handles for a worker to consume mid-wave. Serial-phase only. */ reserveEntityBlock(workerIndex: number, count: number): EntityReservation; /** Reclaim the unconsumed tail of a reservation, LIFO, at bumped generation. */ returnReservedIds(reservation: EntityReservation, consumedCount: number): void; /** Frozen handle codec layout, also valid to hand to workers. */ readonly handleLayout: HandleLayout; encodeHandle(index: number, generation: number): EntityHandle; decodeHandle(handle: EntityHandle): { index: EntityIndex; generation: EntityGeneration; }; handleStats(): HandleStats; /** * Push (entityIndex, componentId[, fieldIndex]) to the reactivity write log for the `.changed` * filter. STUBBED as a no-op until — the canonical signature and * accessor-setter call sites are in place now so only fills the body. */ trackWrite(index: number, componentId: ComponentId, fieldIndex?: number): void; /** * Register a deferred observer. Fires ONLY at the serial observer slot * (observerDrain), NEVER synchronously mid-system. Mutations inside the handler stage to the * command buffer and apply at the next serial flush ( re-entrancy safety). */ observe(term: ObserverTerm, handler: ObserverHandler): ObserverHandle; /** * Publish one event from OUTSIDE systems (input handlers, network code) — main thread, serial * phase, between frames. The event orders ahead of wave 0 of the next frame, in call order, and * every system sees it next update. Inside a system body use the context's `publish` instead, so * the event enters the canonical (frame, wave, SystemId, FIFO) order; calling this mid-update * throws. Values are copied at call time; missing fields take their schema defaults. */ publish(topic: TopicDef, init?: TopicEventInit): void; /** * Did any component on `handle` change strictly after tick `since`?. Driven by * the per-row changeVersion stamps, NOT the write log. Lazily enabled the first time a * `.changed` query flavor or this predicate is used. */ changedSince(handle: EntityHandle, since: number): boolean; /** The rows of `archetypeId` whose changeVersion stamp is > since (the delta serializer scan). */ changedRows(archetypeId: number, since: number): Iterable; /** Advance the frame tick (reactivity.frameReset calls this; world owns the counter). */ advanceTick(): void; /** (no-op single-threaded). */ mergeCorrals(): void; /**, re-testing affected entities against referencing queries. */ maintainStructural(): void; /** */ observerDrain(): void; /** /merge spill, schedule next-frame ring resize. */ flushLogs(): void; /** * Compile (or fetch the cached) LiveQuery for `terms` and return it. Identical * term sets share one LiveQuery by canonical hash (order-independent, pair-target-encoded). The * arity-cap overload family (1..8 inferred, 9+ → LooseQueryElement) is the WorldQuery type. */ query: WorldQuery; /** * Reset every live query's per-frame transient flavor (added/removed) lists. The kernel-only frame * loop calls this at frame start; the scheduler drives it at. */ frameReset(): void; } /** * The only world constructor. Resolves and validates options fail-fast, then * (at later milestones) probes capabilities, allocates bounded buffers, and wires the owning * modules. Returns a frozen World facade. */ export declare function createWorld(options?: WorldOptions): World; //# sourceMappingURL=world.d.ts.map