export type SidecarKey = number & { readonly __sidecarKey: unique symbol; }; export declare const sidecarKey: (componentId: number, fieldIndex: number) => SidecarKey; export type RichKind = 'string' | 'object'; interface SidecarColumn { /** Dense JS array indexed by entity index (handleIndex), NOT archetype row. */ data: unknown[]; /** Generation stamp per entity index (RF-HYGIENE). Parallel to `data`, grows on demand. */ gen: Uint32Array; /** Whether `gen[index]` was ever stamped — distinguishes "written gen 0" from "never written". */ written: Uint8Array; readonly def: unknown; readonly kind: RichKind; } export declare class SidecarStore { #private; /** Allocate a column for a (componentId, fieldIndex) rich field (idempotent). */ ensureColumn(key: SidecarKey, kind: RichKind, def: unknown): void; hasColumn(key: SidecarKey): boolean; /** Read entity `index`'s value for `key`, applying the lazy default + generation guard (RF-HYGIENE). */ read(key: SidecarKey, index: number, gen: number): unknown; /** * Whether entity `index` has a WRITTEN value at `key` for its current generation (vs the lazy default). * The serializer uses this to emit only present rich values (empty slots * are skipped and re-defaulted on the receiver). A recycled index reads as not-present (RF-HYGIENE). */ isPresent(key: SidecarKey, index: number, gen: number): boolean; /** Write entity `index`'s value, stamping the current generation. */ write(key: SidecarKey, index: number, gen: number, value: unknown): void; /** * Observer-window read: during an onRemove/preDespawn drain the dying entity's * index generation has already been bumped, so a normal generation-guarded read would return the * default. A pending-clear entry whose stamped generation MATCHES the reading handle's generation * serves the stashed value (the remove-event ref is bound to the dying handle — world wiring); any * other generation falls through to the generation-guarded read, so a same-window re-mint at the * index reads its own slot, never the dead tenant's stash. */ readForObserver(key: SidecarKey, index: number, gen: number): unknown; /** * The generation of the OLDEST un-superseded dying tenant stashed at `index`, or undefined. While * the drain cursor is inside a stashed tenant's lifetime (its events, up to the Create that * re-minted the index), this is that tenant's generation — the observer dispatch binds the events' * refs to it; supersedePending advances it when the drain passes the re-mint. */ pendingGenerationOf(index: number): number | undefined; /** The observer drain passed a Destroy entry for `index`: when it is the head stash's tenant's own * (matched by despawn ordinal — stashless despawns at the index consume ordinals too), mark the * stash so the NEXT Create at the index supersedes it. */ noteDestroyDrained(index: number): void; /** * Drop the oldest pending-clear entry for `index` when the observer drain encounters a Create * entry AFTER that tenant's Destroy: the log is ordered, so every remove/destroy event of the * tenant that died before this re-mint has already dispatched; later remove events at the index * belong to a newer tenant. A Create drained before the Destroy (the tenant's own same-window * mint) leaves the entry in place. */ supersedePending(index: number): void; /** * Despawn handler. When no held component has a remove-observer, clear * data[index] eagerly so the JS reference is released for GC. When one does, DEFER: stash the * dying values (gen-only when the tenant held no rich field) so an onRemove handler can read them * during the drain (RF-REMOVE-READ), then flush at the post-observer point (flushPending). * * Called for EVERY despawn (the per-index despawn ordinal must count every deferred despawn — * noteDestroyDrained pairs drain-side Destroy entries by it). `gen` is the dying handle's * generation (stamped into the stash so observer-window reads can be keyed to the dead tenant). * `richKeysOnEntity` are the sidecar keys the entity actually held (its signature's rich fields); * only those are stashed/cleared. `defer` is true iff any held component has a remove-observer. */ onDespawn(index: number, gen: number, richKeysOnEntity: readonly SidecarKey[], defer: boolean, windowArmed: boolean): void; /** * Stamp the dying tenant's final archetype location onto the entry its `onDespawn` just created — * called AFTER storage.onDespawn (the deferred swap-pop has settled the dying row), only for a * deferred despawn. The latest entry for `index` is that tenant's. */ setPendingLocation(index: number, archId: number, row: number): void; /** * Update a stashed location's row after storage relocated the dying tenant's held dead row * (allocRow eviction keeping [0,count) dense). Keyed by (index, gen); same archetype, new row. */ updatePendingLocation(index: number, gen: number, row: number): void; /** * The dying tenant's stashed (archId, row) for `index` at generation `gen`, or undefined. The * resolver shim consults this for a dead-tenant ref before resolving through the live records that * a same-window re-mint has overwritten. Mirrors readForObserver's gen match. */ locationForObserver(index: number, gen: number): { archId: number; row: number; } | undefined; /** * Flush the deferred-clear window. Called at the post-observerDrain serial slot, * mirroring where storage's deferred row reclaim is finalized — after onRemove handlers have read the * dying values, before the index is re-minted for a new tenant. */ flushPending(): void; hasPending(): boolean; columns(): IterableIterator<[SidecarKey, SidecarColumn]>; } export {}; //# sourceMappingURL=sidecar.d.ts.map