aiecsjs 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/internal/bitmask.ts","../src/internal/world.ts","../src/internal/component.ts","../src/internal/query.ts","../src/observers.ts"],"names":["createMask","wordCount","setBit","mask","bit","w","isMaskZero","m","i","matches","withMask","anyMask","noneMask","anyHasBits","words","anyHit","wm","am","nm","listBits","out","word","ctz32","v","worldRegistry","getWorldState","world","state","getOrRegisterComponentBit","info","existing","storage","soa","f","componentInfoById","getComponentInfo","component","ensureQueryRegistered","q","ww","compId","getComponentInfoById","bundle","involvedBits","b","s","getQueryArchetypes","list","arch","runQuery","query","buf","e","archIds","id","r","componentId","bindAbortSignal","unsubscribe","signal","onAbort","detached","onAdd","handler","opts","registerComponentObserver","onRemove","onSet","observe","event","entry","idx"],"mappings":"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","file":"observers.js","sourcesContent":["// Multi-word Uint32 bitmask helpers.\n\nexport function createMask(wordCount: number): Uint32Array {\n return new Uint32Array(wordCount)\n}\n\nexport function setBit(mask: Uint32Array, bit: number): void {\n const w = bit >>> 5\n mask[w] = (mask[w] ?? 0) | (1 << (bit & 31))\n}\n\nexport function clearBit(mask: Uint32Array, bit: number): void {\n const w = bit >>> 5\n mask[w] = (mask[w] ?? 0) & ~(1 << (bit & 31))\n}\n\nexport function testBit(mask: Uint32Array, bit: number): boolean {\n const w = bit >>> 5\n return ((mask[w] ?? 0) & (1 << (bit & 31))) !== 0\n}\n\nexport function copyMask(dst: Uint32Array, src: Uint32Array): void {\n const n = src.length\n for (let i = 0; i < n; i++) dst[i] = src[i] ?? 0\n}\n\nexport function cloneMask(src: Uint32Array): Uint32Array {\n const out = new Uint32Array(src.length)\n for (let i = 0; i < src.length; i++) out[i] = src[i] ?? 0\n return out\n}\n\nexport function maskEquals(a: Uint32Array, b: Uint32Array): boolean {\n const n = a.length\n if (b.length !== n) return false\n for (let i = 0; i < n; i++) if ((a[i] ?? 0) !== (b[i] ?? 0)) return false\n return true\n}\n\nexport function maskHash(m: Uint32Array): string {\n // Stable string key for Map lookups. Hex-joined.\n const parts: string[] = []\n for (let i = 0; i < m.length; i++) {\n parts.push((m[i] ?? 0).toString(16))\n }\n return parts.join(',')\n}\n\nexport function isMaskZero(m: Uint32Array): boolean {\n for (let i = 0; i < m.length; i++) if ((m[i] ?? 0) !== 0) return false\n return true\n}\n\nexport function unionMask(a: Uint32Array, b: Uint32Array): Uint32Array {\n const out = new Uint32Array(a.length)\n for (let i = 0; i < a.length; i++) out[i] = (a[i] ?? 0) | (b[i] ?? 0)\n return out\n}\n\n/**\n * Test whether an archetype's mask matches a query.\n * - withMask: all of these bits must be present\n * - anyMask: if anyHasBits, at least one of these bits must be present\n * - noneMask: none of these bits may be present\n */\nexport function matches(\n mask: Uint32Array,\n withMask: Uint32Array,\n anyMask: Uint32Array,\n noneMask: Uint32Array,\n anyHasBits: boolean,\n words: number,\n): boolean {\n let anyHit = !anyHasBits\n for (let w = 0; w < words; w++) {\n const m = mask[w] ?? 0\n const wm = withMask[w] ?? 0\n const am = anyMask[w] ?? 0\n const nm = noneMask[w] ?? 0\n if ((m & wm) !== wm) return false\n if ((m & nm) !== 0) return false\n if (!anyHit && (m & am) !== 0) anyHit = true\n }\n return anyHit\n}\n\nexport function listBits(mask: Uint32Array): number[] {\n const out: number[] = []\n for (let w = 0; w < mask.length; w++) {\n let word = mask[w] ?? 0\n while (word !== 0) {\n const bit = (w << 5) + ctz32(word)\n out.push(bit)\n word &= word - 1\n }\n }\n return out\n}\n\n// Iterate set bits in a slice of a larger Uint32Array starting at `base`.\n// Shared by destroy paths and storage cleanup so the bit-extraction maths\n// lives in exactly one place.\nexport function forEachSetBit(\n mask: Uint32Array,\n base: number,\n words: number,\n fn: (bit: number) => void,\n): void {\n for (let wi = 0; wi < words; wi++) {\n let word = mask[base + wi] ?? 0\n while (word !== 0) {\n const bit = (wi << 5) + ctz32(word)\n fn(bit)\n word &= word - 1\n }\n }\n}\n\n// Match a query against an entity's mask stored as a slice inside a larger\n// Uint32Array. Reading directly from the parent array avoids the temporary\n// Uint32Array allocation on every observer dispatch.\nexport function matchesEntityMask(\n entityMask: Uint32Array,\n base: number,\n words: number,\n withMask: Uint32Array,\n anyMask: Uint32Array,\n noneMask: Uint32Array,\n anyHasBits: boolean,\n): boolean {\n let anyHit = !anyHasBits\n for (let i = 0; i < words; i++) {\n const m = entityMask[base + i] ?? 0\n const wm = withMask[i] ?? 0\n const am = anyMask[i] ?? 0\n const nm = noneMask[i] ?? 0\n if ((m & wm) !== wm) return false\n if ((m & nm) !== 0) return false\n if (!anyHit && (m & am) !== 0) anyHit = true\n }\n return anyHit\n}\n\nfunction ctz32(v: number): number {\n // Count trailing zeros in a 32-bit integer (assumes v !== 0)\n return 31 - Math.clz32(v & -v)\n}\n","import { VERSION } from '../version.js'\nimport { cloneMask, copyMask, createMask, maskHash } from './bitmask.js'\nimport type {\n ArchetypeState,\n ComponentInfo,\n FieldInfo,\n ResolvedWorldOptions,\n World,\n WorldComponentStorage,\n WorldOptions,\n WorldState,\n} from './types.js'\n\nexport const WORLD_BRAND = Symbol.for('aiecsjs.world')\n\nconst DEFAULT_OPTIONS: Required<Omit<WorldOptions, 'buffer' | 'bufferByteOffset'>> = {\n initialCapacity: 1024,\n maxEntities: 1_000_000,\n indexBits: 24,\n generationBits: 8,\n}\n\nconst DEFAULT_MAX_COMPONENTS = 256\n\nlet nextWorldId = 1\nconst worldRegistry = new Map<number, WorldState>()\n\nexport function getWorldState(world: World): WorldState {\n const state = worldRegistry.get(world.id)\n if (!state) throw new Error(`aiecsjs: world ${world.id} is destroyed or unknown`)\n if (state.destroyed) throw new Error(`aiecsjs: world ${world.id} is destroyed`)\n return state\n}\n\nexport function registerWorld(state: WorldState): void {\n worldRegistry.set(state.id, state)\n}\n\nexport function unregisterWorld(id: number): void {\n worldRegistry.delete(id)\n}\n\nexport function isWorldRegistered(id: number): boolean {\n return worldRegistry.has(id)\n}\n\nfunction resolveOptions(opts: WorldOptions | undefined): ResolvedWorldOptions {\n const o = opts ?? {}\n const indexBits = o.indexBits ?? DEFAULT_OPTIONS.indexBits\n const generationBits = o.generationBits ?? DEFAULT_OPTIONS.generationBits\n const maxComponents = DEFAULT_MAX_COMPONENTS\n if (indexBits < 1 || indexBits > 24) {\n throw new Error('aiecsjs: indexBits must be in [1, 24]')\n }\n if (generationBits < 0 || generationBits > 16) {\n throw new Error('aiecsjs: generationBits must be in [0, 16]')\n }\n const maxByIndex = 1 << indexBits\n const initialCapacity = Math.max(\n 1,\n Math.min(o.initialCapacity ?? DEFAULT_OPTIONS.initialCapacity, maxByIndex),\n )\n const maxEntities = Math.max(\n initialCapacity,\n Math.min(o.maxEntities ?? DEFAULT_OPTIONS.maxEntities, maxByIndex),\n )\n return {\n initialCapacity,\n maxEntities,\n indexBits,\n generationBits,\n maxComponents,\n maskWordCount: Math.ceil(maxComponents / 32),\n buffer: o.buffer ?? null,\n bufferByteOffset: o.bufferByteOffset ?? 0,\n }\n}\n\nexport function createEmptyArchetype(maskWordCount: number, maxComponents: number): ArchetypeState {\n return {\n id: 0,\n mask: createMask(maskWordCount),\n size: 0,\n capacity: 16,\n entities: new Uint32Array(16),\n entityRow: new Map<number, number>(),\n componentBits: [],\n edgeAdd: new Int32Array(maxComponents).fill(-1),\n edgeRemove: new Int32Array(maxComponents).fill(-1),\n }\n}\n\nexport function createWorld(options?: WorldOptions): World {\n const resolved = resolveOptions(options)\n const id = nextWorldId++\n\n const generationCtor = resolved.generationBits > 8 ? Uint16Array : Uint8Array\n const generations = new generationCtor(resolved.initialCapacity)\n\n const state: WorldState = {\n id,\n capacity: resolved.initialCapacity,\n version: VERSION,\n options: resolved,\n size: 0,\n nextFreshIndex: 1, // 0 reserved\n freeList: [],\n generations,\n destroyed: false,\n componentBitFor: new Map<number, number>(),\n componentInfoByBit: new Array(resolved.maxComponents).fill(null),\n componentStorageByBit: new Array(resolved.maxComponents).fill(null),\n nextComponentBit: 0,\n entityArchetype: new Uint32Array(resolved.initialCapacity),\n entityMask: new Uint32Array(resolved.initialCapacity * resolved.maskWordCount),\n archetypes: [],\n archetypeByMaskHash: new Map<string, number>(),\n queryVersion: 0,\n queries: [],\n queryMasks: new Map(),\n queryArchetypeCache: [],\n queryArchetypeStamp: [],\n bitToQueries: new Map<number, Set<number>>(),\n reactiveBuffers: new Map(),\n observers: [],\n relationStorage: new Map(),\n sab: resolved.buffer,\n readOnly: false,\n }\n\n // Seed archetype 0 (the empty mask)\n const empty = createEmptyArchetype(resolved.maskWordCount, resolved.maxComponents)\n empty.id = 0\n state.archetypes.push(empty)\n state.archetypeByMaskHash.set(maskHash(empty.mask), 0)\n\n registerWorld(state)\n return makePublicWorld(state)\n}\n\nexport function makePublicWorld(state: WorldState): World {\n return {\n id: state.id,\n get capacity() {\n return state.capacity\n },\n version: state.version,\n } as World\n}\n\nexport function destroyWorld(world: World): void {\n const state = worldRegistry.get(world.id)\n if (!state || state.destroyed) return\n state.destroyed = true\n // Clear large buffers to help GC\n state.archetypes = []\n state.archetypeByMaskHash.clear()\n state.componentInfoByBit = []\n state.componentStorageByBit = []\n state.queries = []\n state.queryMasks.clear()\n state.queryArchetypeCache = []\n state.observers = []\n state.relationStorage.clear()\n state.reactiveBuffers.clear()\n unregisterWorld(world.id)\n}\n\nexport function resetWorld(world: World): void {\n const state = getWorldState(world)\n // Keep capacity and registered components; clear entities and per-entity state.\n state.size = 0\n state.nextFreshIndex = 1\n state.freeList = []\n state.generations.fill(0)\n state.entityArchetype.fill(0)\n state.entityMask.fill(0)\n // Wipe archetype memberships but keep archetype graph for stability.\n for (const arch of state.archetypes) {\n arch.size = 0\n arch.entityRow.clear()\n }\n // Clear SoA columns and AoS instances; storage allocation is preserved.\n for (const storage of state.componentStorageByBit) {\n if (!storage) continue\n if (storage.kind === 'soa' && storage.soa) {\n for (const k of Object.keys(storage.soa)) {\n storage.soa[k]?.fill(0)\n }\n } else if (storage.kind === 'aos' && storage.aos) {\n storage.aos.fill(undefined as unknown as never)\n }\n }\n state.queryVersion++\n for (const buf of state.reactiveBuffers.values()) {\n buf.entered.length = 0\n buf.exited.length = 0\n }\n}\n\nexport function getWorldSize(world: World): number {\n return getWorldState(world).size\n}\n\nexport function getWorldCapacity(world: World): number {\n return getWorldState(world).capacity\n}\n\n// --- Capacity growth ---\n\nexport function ensureCapacity(state: WorldState, needed: number): void {\n if (needed <= state.capacity) return\n if (needed > state.options.maxEntities) {\n throw new Error(\n `aiecsjs: requested capacity ${needed} exceeds maxEntities ${state.options.maxEntities}`,\n )\n }\n let newCap = state.capacity\n while (newCap < needed) newCap = Math.min(newCap * 2, state.options.maxEntities)\n growEntityArrays(state, newCap)\n}\n\nfunction growEntityArrays(state: WorldState, newCap: number): void {\n // generations\n const genCtor: any = state.generations.constructor\n const newGen = new genCtor(newCap) as Uint8Array | Uint16Array\n ;(newGen as Uint8Array).set(state.generations as Uint8Array)\n state.generations = newGen\n\n // entityArchetype\n const newArch = new Uint32Array(newCap)\n newArch.set(state.entityArchetype)\n state.entityArchetype = newArch\n\n // entityMask\n const wordCount = state.options.maskWordCount\n const newMask = new Uint32Array(newCap * wordCount)\n newMask.set(state.entityMask)\n state.entityMask = newMask\n\n // Component storages (SoA columns + AoS arrays)\n for (const storage of state.componentStorageByBit) {\n if (!storage) continue\n if (storage.kind === 'soa' && storage.soa) {\n const info = state.componentInfoByBit[storage.bit]\n if (info) growSoAColumns(storage.soa, info.fields, newCap)\n } else if (storage.kind === 'aos' && storage.aos) {\n storage.aos.length = newCap\n }\n }\n\n state.capacity = newCap\n}\n\nfunction growSoAColumns(soa: Record<string, any>, fields: FieldInfo[], newCap: number): void {\n for (const f of fields) {\n const old = soa[f.name]\n if (!old) continue\n const newLen = newCap * f.vectorLen\n if (old.length >= newLen) continue\n const next = new f.ctor(newLen)\n next.set(old)\n soa[f.name] = next\n }\n}\n\n// --- Archetype management ---\n\nexport function findOrCreateArchetype(\n state: WorldState,\n mask: Uint32Array,\n): { archId: number; created: boolean } {\n const key = maskHash(mask)\n const existing = state.archetypeByMaskHash.get(key)\n if (existing !== undefined) return { archId: existing, created: false }\n\n const id = state.archetypes.length\n const arch: ArchetypeState = {\n id,\n mask: cloneMask(mask),\n size: 0,\n capacity: 16,\n entities: new Uint32Array(16),\n entityRow: new Map<number, number>(),\n componentBits: collectBits(mask),\n edgeAdd: new Int32Array(state.options.maxComponents).fill(-1),\n edgeRemove: new Int32Array(state.options.maxComponents).fill(-1),\n }\n state.archetypes.push(arch)\n state.archetypeByMaskHash.set(key, id)\n state.queryVersion++\n return { archId: id, created: true }\n}\n\nexport function ensureArchetypeCapacity(arch: ArchetypeState, needed: number): void {\n if (needed <= arch.capacity) return\n let newCap = arch.capacity\n while (newCap < needed) newCap *= 2\n const next = new Uint32Array(newCap)\n next.set(arch.entities)\n arch.entities = next\n arch.capacity = newCap\n}\n\nfunction collectBits(mask: Uint32Array): number[] {\n const bits: number[] = []\n for (let w = 0; w < mask.length; w++) {\n let word = mask[w] ?? 0\n while (word !== 0) {\n const lsb = word & -word\n const bit = (w << 5) + (31 - Math.clz32(lsb))\n bits.push(bit)\n word &= word - 1\n }\n }\n return bits\n}\n\n// --- Component registration in a world ---\n\nexport function getOrRegisterComponentBit(state: WorldState, info: ComponentInfo): number {\n const existing = state.componentBitFor.get(info.id)\n if (existing !== undefined) return existing\n\n if (state.nextComponentBit >= state.options.maxComponents) {\n throw new Error(`aiecsjs: world reached maxComponents=${state.options.maxComponents}`)\n }\n const bit = state.nextComponentBit++\n state.componentBitFor.set(info.id, bit)\n state.componentInfoByBit[bit] = info\n\n // Create storage\n let storage: WorldComponentStorage\n if (info.kind === 'soa') {\n const soa: Record<string, any> = {}\n for (const f of info.fields) {\n soa[f.name] = new f.ctor(state.capacity * f.vectorLen)\n }\n storage = { kind: 'soa', componentId: info.id, bit, soa }\n } else if (info.kind === 'aos') {\n storage = { kind: 'aos', componentId: info.id, bit, aos: new Array(state.capacity) }\n } else {\n storage = { kind: 'tag', componentId: info.id, bit }\n }\n state.componentStorageByBit[bit] = storage\n return bit\n}\n\n// Read a bitmask out of state.entityMask[] into a fresh Uint32Array.\nexport function readEntityMask(state: WorldState, eid: number): Uint32Array {\n const w = state.options.maskWordCount\n const out = new Uint32Array(w)\n const base = eid * w\n for (let i = 0; i < w; i++) out[i] = state.entityMask[base + i] ?? 0\n return out\n}\n\nexport function writeEntityMask(state: WorldState, eid: number, mask: Uint32Array): void {\n const w = state.options.maskWordCount\n const base = eid * w\n for (let i = 0; i < w; i++) state.entityMask[base + i] = mask[i] ?? 0\n}\n\n// Get the bit of a component within a world without registering.\nexport function tryGetComponentBit(state: WorldState, info: ComponentInfo): number | undefined {\n return state.componentBitFor.get(info.id)\n}\n\nexport function isWorld(x: unknown): x is World {\n if (!x || typeof x !== 'object') return false\n const obj = x as { id?: unknown; version?: unknown }\n if (typeof obj.id !== 'number') return false\n if (typeof obj.version !== 'string') return false\n return worldRegistry.has(obj.id)\n}\n","import { clearBit, cloneMask, forEachSetBit, setBit, testBit } from './bitmask.js'\nimport { isAliveInternal } from './entity.js'\nimport type {\n AoSComponent,\n ComponentInfo,\n ComponentInit,\n ComponentLike,\n EntityId,\n FieldInfo,\n SoAColumns,\n SoAComponent,\n SoASchema,\n TagComponent,\n TypedArrayConstructor,\n World,\n WorldComponentStorage,\n WorldState,\n} from './types.js'\nimport {\n ensureArchetypeCapacity,\n findOrCreateArchetype,\n getOrRegisterComponentBit,\n getWorldState,\n readEntityMask,\n tryGetComponentBit,\n writeEntityMask,\n} from './world.js'\n\n// --- Component factories ---\n\nlet nextComponentId = 1\n\nconst TYPE_CTOR: Record<string, TypedArrayConstructor> = {\n i8: Int8Array,\n u8: Uint8Array,\n i16: Int16Array,\n u16: Uint16Array,\n i32: Int32Array,\n u32: Uint32Array,\n f32: Float32Array,\n f64: Float64Array,\n eid: Uint32Array,\n bool: Uint8Array,\n}\n\nconst componentInfoById = new Map<number, ComponentInfo>()\n\nexport function defineComponent<S extends SoASchema>(schema: S): SoAComponent<S> {\n const fields: FieldInfo[] = []\n for (const [name, decl] of Object.entries(schema)) {\n let type: keyof typeof TYPE_CTOR\n let vectorLen = 1\n if (typeof decl === 'string') {\n type = decl\n } else if (Array.isArray(decl)) {\n const [t, len] = decl\n type = t\n vectorLen = len\n } else {\n throw new TypeError(`aiecsjs: invalid field declaration for \"${name}\"`)\n }\n const ctor = TYPE_CTOR[type]\n if (!ctor) throw new TypeError(`aiecsjs: unknown field type \"${type}\" for field \"${name}\"`)\n fields.push({\n name,\n type: type as any,\n vectorLen,\n ctor,\n bytesPerElement: (ctor as any).BYTES_PER_ELEMENT,\n })\n }\n const id = nextComponentId++\n const info: ComponentInfo = { id, kind: 'soa', schema, fields, factory: null }\n componentInfoById.set(id, info)\n const handle: SoAComponent<S> = {\n __kind: 'soa',\n __id: id,\n __schema: schema,\n }\n return handle\n}\n\nexport function defineTag(): TagComponent {\n const id = nextComponentId++\n const info: ComponentInfo = { id, kind: 'tag', schema: null, fields: [], factory: null }\n componentInfoById.set(id, info)\n return { __kind: 'tag', __id: id }\n}\n\nexport function defineObjectComponent<T>(factory?: () => T): AoSComponent<T> {\n const id = nextComponentId++\n const fac = factory ?? (() => ({}) as T)\n const info: ComponentInfo = { id, kind: 'aos', schema: null, fields: [], factory: fac as any }\n componentInfoById.set(id, info)\n return { __kind: 'aos', __id: id, __factory: fac }\n}\n\nexport function getComponentInfo(component: ComponentLike): ComponentInfo {\n const info = componentInfoById.get(component.__id)\n if (!info)\n throw new Error(\n 'aiecsjs: component is not registered (call defineComponent/defineTag/defineObjectComponent)',\n )\n return info\n}\n\n// --- Component ops on entities ---\n\nexport function addComponent<C extends ComponentLike>(\n world: World,\n eid: EntityId,\n component: C,\n initial?: ComponentInit<C>,\n): void {\n const state = getWorldState(world)\n if (state.readOnly) throw new Error('aiecsjs: cannot mutate a read-only world')\n if (!isAliveInternal(state, eid)) {\n throw new Error(`aiecsjs: addComponent on dead entity ${eid}`)\n }\n const info = getComponentInfo(component)\n const bit = getOrRegisterComponentBit(state, info)\n\n const prevMask = readEntityMask(state, eid)\n if (testBit(prevMask, bit)) {\n if (initial !== undefined) writeInitial(state, eid, component, initial)\n return\n }\n const newMask = cloneMask(prevMask)\n setBit(newMask, bit)\n migrateEntity(state, eid, newMask)\n\n writeInitial(state, eid, component, initial)\n\n fireAddObservers(state, eid, bit)\n notifyMaskChange(state, eid, bit, prevMask, newMask)\n}\n\nexport function removeComponent<C extends ComponentLike>(\n world: World,\n eid: EntityId,\n component: C,\n): void {\n const state = getWorldState(world)\n if (state.readOnly) throw new Error('aiecsjs: cannot mutate a read-only world')\n if (!isAliveInternal(state, eid)) return\n const info = getComponentInfo(component)\n const bit = tryGetComponentBit(state, info)\n if (bit === undefined) return\n const prevMask = readEntityMask(state, eid)\n if (!testBit(prevMask, bit)) return\n\n // Write the new mask BEFORE firing observers so query-targeted observers\n // reading `state.entityMask` see the post-removal state (and thus correctly\n // detect \"entity left this query\"). Without this reorder, a query that\n // requires the removed component would still match during dispatch and the\n // remove observer would never fire. Component-targeted observers receive the\n // bit directly and don't depend on mask timing. addComponent already follows\n // this \"mutate then fire\" order; keeping removeComponent consistent.\n const newMask = cloneMask(prevMask)\n clearBit(newMask, bit)\n migrateEntity(state, eid, newMask)\n\n fireRemoveObservers(state, eid, bit)\n\n const storage = state.componentStorageByBit[bit]\n if (storage?.kind === 'soa' && storage.soa) {\n clearSoAEntity(storage.soa, info.fields, eid)\n } else if (storage?.kind === 'aos' && storage.aos) {\n storage.aos[eid] = undefined\n }\n\n notifyMaskChange(state, eid, bit, prevMask, newMask)\n}\n\nexport function hasComponent<C extends ComponentLike>(\n world: World,\n eid: EntityId,\n component: C,\n): boolean {\n const state = getWorldState(world)\n if (!isAliveInternal(state, eid)) return false\n const info = getComponentInfo(component)\n const bit = tryGetComponentBit(state, info)\n if (bit === undefined) return false\n const mask = readEntityMask(state, eid)\n return testBit(mask, bit)\n}\n\nexport function getComponent<C extends ComponentLike>(\n world: World,\n eid: EntityId,\n component: C,\n): unknown {\n const state = getWorldState(world)\n if (!isAliveInternal(state, eid)) return undefined\n const info = getComponentInfo(component)\n const bit = tryGetComponentBit(state, info)\n if (bit === undefined) return undefined\n const mask = readEntityMask(state, eid)\n if (!testBit(mask, bit)) return undefined\n const storage = state.componentStorageByBit[bit]\n if (!storage) return undefined\n if (storage.kind === 'soa') return storage.soa\n if (storage.kind === 'aos') return storage.aos?.[eid]\n return true // tag\n}\n\nexport function setComponent<C extends ComponentLike, V>(\n world: World,\n eid: EntityId,\n component: C,\n value: V,\n): void {\n const state = getWorldState(world)\n if (state.readOnly) throw new Error('aiecsjs: cannot mutate a read-only world')\n if (!isAliveInternal(state, eid)) throw new Error(`aiecsjs: setComponent on dead entity ${eid}`)\n const info = getComponentInfo(component)\n const bit = tryGetComponentBit(state, info)\n if (bit === undefined) {\n // Adding via set\n addComponent(world, eid, component, value as any)\n return\n }\n const mask = readEntityMask(state, eid)\n if (!testBit(mask, bit)) {\n addComponent(world, eid, component, value as any)\n return\n }\n writeInitial(state, eid, component, value as any)\n fireSetObservers(state, eid, bit, value)\n}\n\n// --- Internals ---\n\nfunction writeInitial(\n state: WorldState,\n eid: number,\n component: ComponentLike,\n initial: unknown,\n): void {\n const info = getComponentInfo(component)\n const bit = state.componentBitFor.get(info.id)\n if (bit === undefined) return\n const storage = state.componentStorageByBit[bit]\n if (!storage) return\n if (info.kind === 'soa' && storage.soa) {\n if (initial == null) return\n const obj = initial as Record<string, unknown>\n for (const f of info.fields) {\n if (!(f.name in obj)) continue\n const col = storage.soa[f.name]\n if (!col) continue\n const val = obj[f.name]\n if (f.vectorLen === 1) {\n col[eid] = typeof val === 'boolean' ? (val ? 1 : 0) : Number(val)\n } else if (Array.isArray(val) || ArrayBuffer.isView(val)) {\n const base = eid * f.vectorLen\n const arr = val as ArrayLike<number>\n for (let i = 0; i < f.vectorLen; i++) col[base + i] = Number(arr[i] ?? 0)\n }\n }\n } else if (info.kind === 'aos' && storage.aos) {\n const factory = info.factory ?? (() => ({}))\n let inst = storage.aos[eid]\n if (inst === undefined) {\n inst = factory()\n storage.aos[eid] = inst\n }\n if (initial && typeof initial === 'object') {\n // SECURITY: do NOT use `Object.assign(inst, initial)` here. When `initial`\n // comes from `JSON.parse(untrustedBytes)` — e.g. via `fromJSON`,\n // `deserializeWorld`, or app code piping a bridge payload — the parsed\n // object can carry an OWN `__proto__` property, and `Object.assign` uses\n // [[Set]] semantics which would trigger the proto setter and clobber the\n // instance's prototype chain. Explicit own-key copy with a deny-list\n // closes the prototype-pollution path.\n const src = initial as Record<string, unknown>\n const target = inst as Record<string, unknown>\n for (const key of Object.keys(src)) {\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue\n target[key] = src[key]\n }\n }\n }\n // tag: nothing\n}\n\nfunction clearSoAEntity(soa: SoAColumns, fields: FieldInfo[], eid: number): void {\n for (const f of fields) {\n const col = soa[f.name]\n if (!col) continue\n if (f.vectorLen === 1) {\n col[eid] = 0\n } else {\n const base = eid * f.vectorLen\n for (let i = 0; i < f.vectorLen; i++) col[base + i] = 0\n }\n }\n}\n\n// Clear every component-storage slot that the entity currently owns.\n// Walks the entity's mask; for each set bit, zeroes the SoA columns or\n// undefines the AoS slot at the entity index. Tag storages are bit-only.\n// Caller (e.g. destroyEntity) is expected to clear the mask separately.\nexport function clearAllEntityStorages(state: WorldState, eid: EntityId): void {\n const wordCount = state.options.maskWordCount\n const idx = eid as number\n const base = idx * wordCount\n forEachSetBit(state.entityMask, base, wordCount, (bit) => {\n const storage = state.componentStorageByBit[bit]\n const info = state.componentInfoByBit[bit]\n if (storage?.kind === 'soa' && storage.soa && info) {\n clearSoAEntity(storage.soa, info.fields, idx)\n } else if (storage?.kind === 'aos' && storage.aos) {\n storage.aos[idx] = undefined\n }\n })\n}\n\nfunction migrateEntity(state: WorldState, eid: number, newMask: Uint32Array): void {\n const srcArchId = state.entityArchetype[eid] ?? 0\n const srcArch = state.archetypes[srcArchId]\n if (!srcArch) return\n\n const found = findOrCreateArchetype(state, newMask)\n const destArchId = found.archId\n const destArch = state.archetypes[destArchId]\n if (!destArch) return\n\n if (destArchId !== srcArchId) {\n // Swap-pop from src\n const row = srcArch.entityRow.get(eid)\n if (row !== undefined) {\n const lastRow = srcArch.size - 1\n if (row !== lastRow) {\n const moved = srcArch.entities[lastRow] ?? 0\n srcArch.entities[row] = moved\n srcArch.entityRow.set(moved, row)\n }\n srcArch.entities[lastRow] = 0\n srcArch.entityRow.delete(eid)\n srcArch.size--\n }\n\n // Append to dest\n ensureArchetypeCapacity(destArch, destArch.size + 1)\n const newRow = destArch.size\n destArch.entities[newRow] = eid\n destArch.entityRow.set(eid, newRow)\n destArch.size++\n\n state.entityArchetype[eid] = destArchId\n }\n\n writeEntityMask(state, eid, newMask)\n}\n\n// --- Observer dispatch (lazy-bound) ---\n\ninterface ObserversDispatchAPI {\n fireAdd(state: WorldState, eid: EntityId, bit: number): void\n fireRemove(state: WorldState, eid: EntityId, bit: number): void\n fireSet(state: WorldState, eid: EntityId, bit: number, value: unknown): void\n}\nlet _dispatch: ObserversDispatchAPI = {\n fireAdd: () => {},\n fireRemove: () => {},\n fireSet: () => {},\n}\n\ntype MaskChangeFn = (\n state: WorldState,\n eid: EntityId,\n bit: number,\n prev: Uint32Array,\n next: Uint32Array,\n) => void\nlet _maskChange: MaskChangeFn = () => {}\nexport function registerMaskChangeDispatch(fn: MaskChangeFn): void {\n _maskChange = fn\n}\nfunction notifyMaskChange(\n state: WorldState,\n eid: number,\n bit: number,\n prev: Uint32Array,\n next: Uint32Array,\n): void {\n _maskChange(state, eid as EntityId, bit, prev, next)\n}\nexport function registerObserverDispatch(api: ObserversDispatchAPI): void {\n _dispatch = api\n}\nfunction fireAddObservers(state: WorldState, eid: number, bit: number): void {\n _dispatch.fireAdd(state, eid as EntityId, bit)\n}\nfunction fireRemoveObservers(state: WorldState, eid: number, bit: number): void {\n _dispatch.fireRemove(state, eid as EntityId, bit)\n}\nfunction fireSetObservers(state: WorldState, eid: number, bit: number, value: unknown): void {\n _dispatch.fireSet(state, eid as EntityId, bit, value)\n}\n\n// --- Helper for serialize/worker ---\n\nexport function getComponentByInternalId(id: number): ComponentInfo | undefined {\n return componentInfoById.get(id)\n}\n\nexport function listAllComponents(): ComponentInfo[] {\n return Array.from(componentInfoById.values())\n}\n\nexport function _resetComponentRegistry_FOR_TESTS_ONLY(): void {\n componentInfoById.clear()\n nextComponentId = 1\n}\n","import { createMask, isMaskZero, listBits, matches, setBit } from './bitmask.js'\nimport { getComponentInfo } from './component.js'\nimport type {\n Archetype,\n ComponentInfo,\n ComponentLike,\n EntityId,\n Query,\n QueryDescriptor,\n QueryInternal,\n QueryMaskBundle,\n ReactiveBuffer,\n World,\n WorldState,\n} from './types.js'\nimport {\n getOrRegisterComponentBit,\n getWorldState,\n readEntityMask,\n tryGetComponentBit,\n} from './world.js'\n\nlet nextQueryId = 1\nconst moduleQueryCache = new Map<string, QueryInternal>()\n\nfunction descKey(d: QueryDescriptor): string {\n const all = (d.all ?? [])\n .map((c) => c.__id)\n .sort((a, b) => a - b)\n .join('-')\n const any = (d.any ?? [])\n .map((c) => c.__id)\n .sort((a, b) => a - b)\n .join('-')\n const none = (d.none ?? [])\n .map((c) => c.__id)\n .sort((a, b) => a - b)\n .join('-')\n return `A${all}|Y${any}|N${none}`\n}\n\nexport function defineQuery(arg: ComponentLike[] | QueryDescriptor): Query {\n const desc: QueryDescriptor = Array.isArray(arg) ? { all: arg } : arg\n\n // Validate all members are components\n for (const c of [...(desc.all ?? []), ...(desc.any ?? []), ...(desc.none ?? [])]) {\n if (!c || typeof c !== 'object' || typeof (c as any).__id !== 'number') {\n throw new TypeError('aiecsjs: defineQuery received a non-component value')\n }\n }\n\n const key = descKey(desc)\n const cached = moduleQueryCache.get(key)\n if (cached) return cached\n\n const q: QueryInternal = {\n id: nextQueryId++,\n mask: [],\n all: (desc.all ?? []).map((c) => c.__id),\n any: (desc.any ?? []).map((c) => c.__id),\n none: (desc.none ?? []).map((c) => c.__id),\n columnViewCache: [...(desc.all ?? []), ...(desc.any ?? [])],\n reactiveKind: 'normal',\n sourceQueryId: -1,\n sourceQuery: null,\n }\n moduleQueryCache.set(key, q)\n return q\n}\n\nexport function enterQuery(query: Query): Query {\n const q = query as QueryInternal\n const key = `enter:${q.id}`\n const cached = moduleQueryCache.get(key)\n if (cached) return cached\n const reactive: QueryInternal = {\n id: nextQueryId++,\n mask: [],\n all: q.all,\n any: q.any,\n none: q.none,\n columnViewCache: q.columnViewCache,\n reactiveKind: 'enter',\n sourceQueryId: q.id,\n sourceQuery: q,\n }\n moduleQueryCache.set(key, reactive)\n return reactive\n}\n\nexport function exitQuery(query: Query): Query {\n const q = query as QueryInternal\n const key = `exit:${q.id}`\n const cached = moduleQueryCache.get(key)\n if (cached) return cached\n const reactive: QueryInternal = {\n id: nextQueryId++,\n mask: [],\n all: q.all,\n any: q.any,\n none: q.none,\n columnViewCache: q.columnViewCache,\n reactiveKind: 'exit',\n sourceQueryId: q.id,\n sourceQuery: q,\n }\n moduleQueryCache.set(key, reactive)\n return reactive\n}\n\n// --- Per-world query setup ---\n\nfunction ensureQueryRegistered(state: WorldState, q: QueryInternal): void {\n if (state.queries[q.id] === q && state.queryMasks.has(q.id)) return\n\n // Build per-world bitmasks (this may register new bits)\n const ww = state.options.maskWordCount\n const withMask = createMask(ww)\n const anyMask = createMask(ww)\n const noneMask = createMask(ww)\n for (const compId of q.all) {\n const info = getComponentInfoById(compId)\n const bit = getOrRegisterComponentBit(state, info)\n setBit(withMask, bit)\n }\n for (const compId of q.any) {\n const info = getComponentInfoById(compId)\n const bit = getOrRegisterComponentBit(state, info)\n setBit(anyMask, bit)\n }\n for (const compId of q.none) {\n const info = getComponentInfoById(compId)\n const bit = getOrRegisterComponentBit(state, info)\n setBit(noneMask, bit)\n }\n\n const bundle: QueryMaskBundle = {\n withMask,\n anyMask,\n noneMask,\n anyHasBits: !isMaskZero(anyMask),\n }\n state.queryMasks.set(q.id, bundle)\n\n state.queries[q.id] = q\n state.queryArchetypeCache[q.id] = null\n state.queryArchetypeStamp[q.id] = -1\n\n // Build bit → queries index for fast reactive lookup\n const involvedBits: number[] = [\n ...listBits(withMask),\n ...listBits(anyMask),\n ...listBits(noneMask),\n ]\n for (const b of involvedBits) {\n let s = state.bitToQueries.get(b)\n if (!s) {\n s = new Set<number>()\n state.bitToQueries.set(b, s)\n }\n s.add(q.id)\n }\n\n // Initialize reactive buffer for enter/exit queries\n if (q.reactiveKind !== 'normal') {\n if (!state.reactiveBuffers.has(q.id)) {\n state.reactiveBuffers.set(q.id, { entered: [], exited: [] })\n }\n // Also register the source query so recordEntityMaskChange will see it\n if (q.sourceQuery) ensureQueryRegistered(state, q.sourceQuery)\n }\n}\n\nfunction getQueryArchetypes(state: WorldState, q: QueryInternal): number[] {\n ensureQueryRegistered(state, q)\n if (state.queryArchetypeCache[q.id] && state.queryArchetypeStamp[q.id] === state.queryVersion) {\n return state.queryArchetypeCache[q.id]!\n }\n const bundle = state.queryMasks.get(q.id)!\n const words = state.options.maskWordCount\n const list: number[] = []\n for (let i = 0; i < state.archetypes.length; i++) {\n const arch = state.archetypes[i]!\n if (\n matches(arch.mask, bundle.withMask, bundle.anyMask, bundle.noneMask, bundle.anyHasBits, words)\n ) {\n list.push(i)\n }\n }\n state.queryArchetypeCache[q.id] = list\n state.queryArchetypeStamp[q.id] = state.queryVersion\n return list\n}\n\nexport function queryArchetypes(world: World, query: Query): readonly Archetype[] {\n const state = getWorldState(world)\n const q = query as QueryInternal\n const ids = getQueryArchetypes(state, q)\n const out: Archetype[] = []\n for (const id of ids) {\n const arch = state.archetypes[id]!\n out.push({ id: arch.id, mask: Array.from(arch.mask), size: arch.size })\n }\n return out\n}\n\nexport function runQuery(world: World, query: Query): readonly EntityId[] {\n const state = getWorldState(world)\n const q = query as QueryInternal\n const out: EntityId[] = []\n if (q.reactiveKind === 'enter') {\n const buf = state.reactiveBuffers.get(q.id)\n if (buf) {\n for (const e of buf.entered) out.push(e as EntityId)\n buf.entered.length = 0\n }\n return out\n }\n if (q.reactiveKind === 'exit') {\n const buf = state.reactiveBuffers.get(q.id)\n if (buf) {\n for (const e of buf.exited) out.push(e as EntityId)\n buf.exited.length = 0\n }\n return out\n }\n const archIds = getQueryArchetypes(state, q)\n for (const id of archIds) {\n const arch = state.archetypes[id]!\n for (let r = 0; r < arch.size; r++) {\n out.push(arch.entities[r] as EntityId)\n }\n }\n return out\n}\n\nexport function* iterQuery(world: World, query: Query): IterableIterator<EntityId> {\n const state = getWorldState(world)\n const q = query as QueryInternal\n if (q.reactiveKind !== 'normal') {\n const buf = state.reactiveBuffers.get(q.id)\n if (!buf) return\n const src = q.reactiveKind === 'enter' ? buf.entered : buf.exited\n for (const e of src) yield e as EntityId\n src.length = 0\n return\n }\n const archIds = getQueryArchetypes(state, q)\n for (const id of archIds) {\n const arch = state.archetypes[id]!\n for (let r = 0; r < arch.size; r++) {\n yield arch.entities[r] as EntityId\n }\n }\n}\n\nexport function forEachEntity(\n world: World,\n query: Query,\n fn: (eid: EntityId, ...cols: any[]) => void,\n): void {\n const state = getWorldState(world)\n const q = query as QueryInternal\n\n if (q.reactiveKind !== 'normal') {\n const buf = state.reactiveBuffers.get(q.id)\n if (!buf) return\n const src = q.reactiveKind === 'enter' ? buf.entered : buf.exited\n if (src.length === 0) return\n const cols = buildColumnViews(state, q)\n for (let i = 0; i < src.length; i++) {\n const e = src[i] as EntityId\n callWithCols(fn, e, cols)\n }\n src.length = 0\n return\n }\n\n ensureQueryRegistered(state, q)\n const archIds = getQueryArchetypes(state, q)\n const cols = buildColumnViews(state, q)\n for (const id of archIds) {\n const arch = state.archetypes[id]!\n const ents = arch.entities\n const n = arch.size\n for (let r = 0; r < n; r++) {\n callWithCols(fn, ents[r] as EntityId, cols)\n }\n }\n}\n\nfunction callWithCols(\n fn: (eid: EntityId, ...cols: any[]) => void,\n eid: EntityId,\n cols: any[],\n): void {\n // Specialise for low arities to avoid spread allocation.\n switch (cols.length) {\n case 0:\n fn(eid)\n break\n case 1:\n fn(eid, cols[0])\n break\n case 2:\n fn(eid, cols[0], cols[1])\n break\n case 3:\n fn(eid, cols[0], cols[1], cols[2])\n break\n case 4:\n fn(eid, cols[0], cols[1], cols[2], cols[3])\n break\n case 5:\n fn(eid, cols[0], cols[1], cols[2], cols[3], cols[4])\n break\n default:\n fn(eid, ...cols)\n }\n}\n\nfunction buildColumnViews(state: WorldState, q: QueryInternal): any[] {\n const out: any[] = []\n for (const comp of q.columnViewCache) {\n const info = getComponentInfo(comp)\n const bit = state.componentBitFor.get(info.id)\n if (bit === undefined) {\n out.push(undefined)\n continue\n }\n const storage = state.componentStorageByBit[bit]\n if (!storage) {\n out.push(undefined)\n continue\n }\n if (storage.kind === 'soa') out.push(storage.soa)\n else if (storage.kind === 'aos') out.push(storage.aos)\n else out.push(true) // tag\n }\n return out\n}\n\n// --- Reactive query updates (called from migration code) ---\n\nexport function recordEntityMaskChange(\n state: WorldState,\n eid: EntityId,\n changedBit: number,\n prevMask: Uint32Array,\n nextMask: Uint32Array,\n): void {\n // Lazy-register any reactive queries (and their sources) so we can correctly\n // determine match transitions even if the user only ever called enterQuery/exitQuery.\n for (const q of moduleQueryCache.values()) {\n if (q.reactiveKind === 'normal') continue\n if (q.sourceQuery && state.queries[q.sourceQuery.id] !== q.sourceQuery) {\n ensureQueryRegistered(state, q.sourceQuery)\n }\n }\n\n const involved = state.bitToQueries.get(changedBit)\n if (!involved) return\n const words = state.options.maskWordCount\n for (const qid of involved) {\n const q = state.queries[qid]\n if (!q) continue\n if (q.reactiveKind !== 'normal') continue\n const bundle = state.queryMasks.get(qid)\n if (!bundle) continue\n const wasMatch = matches(\n prevMask,\n bundle.withMask,\n bundle.anyMask,\n bundle.noneMask,\n bundle.anyHasBits,\n words,\n )\n const isMatch = matches(\n nextMask,\n bundle.withMask,\n bundle.anyMask,\n bundle.noneMask,\n bundle.anyHasBits,\n words,\n )\n if (!wasMatch && isMatch) {\n pushReactive(state, qid, 'enter', eid)\n } else if (wasMatch && !isMatch) {\n pushReactive(state, qid, 'exit', eid)\n }\n }\n}\n\nfunction pushReactive(\n state: WorldState,\n queryId: number,\n kind: 'enter' | 'exit',\n eid: EntityId,\n): void {\n // Walk the module cache (not just state.queries) so reactive variants that\n // haven't been registered with this world yet still receive events.\n for (const r of moduleQueryCache.values()) {\n if (r.sourceQueryId !== queryId) continue\n if (r.reactiveKind !== kind) continue\n // Lazily register the reactive query in this world so subsequent reads can find it\n ensureQueryRegistered(state, r)\n const buf = ensureReactiveBuffer(state, r.id)\n if (kind === 'enter') buf.entered.push(eid as number)\n else buf.exited.push(eid as number)\n }\n}\n\nfunction ensureReactiveBuffer(state: WorldState, qid: number): ReactiveBuffer {\n let buf = state.reactiveBuffers.get(qid)\n if (!buf) {\n buf = { entered: [], exited: [] }\n state.reactiveBuffers.set(qid, buf)\n }\n return buf\n}\n\n// --- Helpers ---\n\nfunction getComponentInfoById(componentId: number): ComponentInfo {\n // We need access to the component registry's lookup by id. Re-import dynamically to avoid cycles.\n const info = lazyGetComponentInfo(componentId)\n if (!info) throw new Error(`aiecsjs: component id ${componentId} not registered`)\n return info\n}\n\nlet _getComponentInfoFn: ((id: number) => ComponentInfo | undefined) | null = null\nexport function registerComponentLookup(fn: (id: number) => ComponentInfo | undefined): void {\n _getComponentInfoFn = fn\n}\nfunction lazyGetComponentInfo(id: number): ComponentInfo | undefined {\n return _getComponentInfoFn ? _getComponentInfoFn(id) : undefined\n}\n\nexport function _resetQueryRegistry_FOR_TESTS_ONLY(): void {\n moduleQueryCache.clear()\n nextQueryId = 1\n}\n","// aiecsjs/observers — observe component add/remove/set events.\n\nimport { forEachSetBit, matchesEntityMask } from './internal/bitmask.js'\nimport { getComponentInfo, registerObserverDispatch } from './internal/component.js'\nimport { registerObserversAPI } from './internal/entity.js'\nimport { runQuery } from './internal/query.js'\nimport type {\n ComponentLike,\n EntityId,\n ObserverEntry,\n ObserverEvent,\n Query,\n QueryInternal,\n World,\n WorldState,\n} from './internal/types.js'\nimport { getOrRegisterComponentBit, getWorldState } from './internal/world.js'\n\n/**\n * Options accepted by every observer registration. `signal` aborts the\n * subscription when fired; the returned unsubscribe is still safe to call.\n */\nexport interface ObserverOptions {\n signal?: AbortSignal\n}\n\nfunction bindAbortSignal(unsubscribe: () => void, signal: AbortSignal | undefined): () => void {\n if (!signal) return unsubscribe\n if (signal.aborted) {\n unsubscribe()\n return () => {}\n }\n const onAbort = (): void => {\n unsubscribe()\n }\n signal.addEventListener('abort', onAbort, { once: true })\n let detached = false\n return () => {\n if (detached) return\n detached = true\n signal.removeEventListener('abort', onAbort)\n unsubscribe()\n }\n}\n\nexport function onAdd(\n world: World,\n component: ComponentLike,\n handler: (eid: EntityId) => void,\n opts?: ObserverOptions,\n): () => void {\n return bindAbortSignal(registerComponentObserver(world, component, 'add', handler), opts?.signal)\n}\n\nexport function onRemove(\n world: World,\n component: ComponentLike,\n handler: (eid: EntityId) => void,\n opts?: ObserverOptions,\n): () => void {\n return bindAbortSignal(\n registerComponentObserver(world, component, 'remove', handler),\n opts?.signal,\n )\n}\n\n/**\n * Low-level mutation hook. Fires after `setComponent(world, eid, comp, value)`\n * when the component is already present on the entity (`addComponent` does\n * NOT trigger `onSet` — use `onAdd` for that path; an `addComponent` followed\n * by `setComponent` will fire `onAdd` then `onSet`).\n *\n * This is NOT a reactive value-predicate query — see `enterQuery` /\n * `exitQuery` for structural change tracking, and validate value predicates\n * in app code if you need them.\n */\nexport function onSet<C extends ComponentLike>(\n world: World,\n component: C,\n handler: (eid: EntityId, value: unknown) => void,\n opts?: ObserverOptions,\n): () => void {\n return bindAbortSignal(registerComponentObserver(world, component, 'set', handler), opts?.signal)\n}\n\nexport function observe(\n world: World,\n query: Query,\n event: ObserverEvent,\n handler: (eid: EntityId) => void,\n opts?: ObserverOptions,\n): () => void {\n const state = getWorldState(world)\n // Force registration of the query into this world so dispatch can find it.\n runQuery(world, query)\n const entry: ObserverEntry = {\n event,\n componentBit: -1,\n queryId: (query as QueryInternal).id,\n handler: handler as (eid: EntityId, value?: unknown) => void,\n }\n state.observers.push(entry)\n const unsubscribe = (): void => {\n const idx = state.observers.indexOf(entry)\n if (idx >= 0) state.observers.splice(idx, 1)\n }\n return bindAbortSignal(unsubscribe, opts?.signal)\n}\n\nfunction registerComponentObserver(\n world: World,\n component: ComponentLike,\n event: ObserverEvent,\n handler: (eid: EntityId, value?: unknown) => void,\n): () => void {\n const state = getWorldState(world)\n const info = getComponentInfo(component)\n let bit = state.componentBitFor.get(info.id)\n if (bit === undefined) {\n bit = getOrRegisterComponentBit(state, info)\n }\n const entry: ObserverEntry = { event, componentBit: bit, queryId: -1, handler }\n state.observers.push(entry)\n return () => {\n const idx = state.observers.indexOf(entry)\n if (idx >= 0) state.observers.splice(idx, 1)\n }\n}\n\n// --- Dispatch impls (wired into component.ts) ---\n//\n// CORRECTNESS: every dispatch loop snapshots `state.observers` via `Array.from`\n// before iterating. A handler may call its returned unsubscribe (which splices\n// `state.observers`), and mutating the backing array while iterating with a\n// for-of would skip the next sibling observer. The snapshot pins the visit\n// list; already-removed entries are filtered via `state.observers.includes`\n// so an in-flight unsubscribe also skips subsequent fires of the same dispatch.\n\nfunction fireAdd(state: WorldState, eid: EntityId, bit: number): void {\n const snapshot = Array.from(state.observers)\n for (const obs of snapshot) {\n if (obs.event !== 'add') continue\n if (obs.componentBit !== bit) continue\n if (!state.observers.includes(obs)) continue\n obs.handler(eid)\n }\n // Query-targeted observers: check if the entity's mask now matches the query\n // (uses the post-mutation mask, which the caller already wrote)\n dispatchQueryObservers(state, eid, 'add', /*prev*/ null, /*current*/ true)\n}\n\nfunction fireRemove(state: WorldState, eid: EntityId, bit: number): void {\n const snapshot = Array.from(state.observers)\n for (const obs of snapshot) {\n if (obs.event !== 'remove') continue\n if (obs.componentBit !== bit) continue\n if (!state.observers.includes(obs)) continue\n obs.handler(eid)\n }\n dispatchQueryObservers(state, eid, 'remove', /*prev*/ true, /*current*/ null)\n}\n\nfunction fireSet(state: WorldState, eid: EntityId, bit: number, value: unknown): void {\n const snapshot = Array.from(state.observers)\n for (const obs of snapshot) {\n if (obs.event !== 'set') continue\n if (!state.observers.includes(obs)) continue\n if (obs.componentBit === bit) obs.handler(eid, value)\n else if (obs.queryId !== -1) {\n const q = state.queries[obs.queryId]\n if (q?.all.includes(state.componentInfoByBit[bit]?.id ?? -1)) {\n obs.handler(eid, value)\n }\n }\n }\n}\n\n// Helper: when a component changes, walk query observers to see if their query\n// match status changed. Reads state.entityMask in place; no per-call allocation.\nfunction dispatchQueryObservers(\n state: WorldState,\n eid: EntityId,\n event: 'add' | 'remove',\n _prev: unknown,\n _current: unknown,\n): void {\n const w = state.options.maskWordCount\n const base = (eid as number) * w\n const snapshot = Array.from(state.observers)\n for (const obs of snapshot) {\n if (obs.event !== event) continue\n if (obs.queryId === -1) continue\n if (!state.observers.includes(obs)) continue\n const bundle = state.queryMasks.get(obs.queryId)\n if (!bundle) continue\n const isMatch = matchesEntityMask(\n state.entityMask,\n base,\n w,\n bundle.withMask,\n bundle.anyMask,\n bundle.noneMask,\n bundle.anyHasBits,\n )\n // For 'add' event, fire if newly matched. For 'remove', fire if newly unmatched.\n // We approximate \"newly\" by always firing on event when match status agrees;\n // duplicates are acceptable for 0.1 query observers.\n if (event === 'add' && isMatch) obs.handler(eid)\n if (event === 'remove' && !isMatch) obs.handler(eid)\n }\n}\n\n// Wire the dispatch into component.ts\nregisterObserverDispatch({ fireAdd, fireRemove, fireSet })\n\n// Register destroy hook so onRemove fires for every component on destroy\n// AND so query-targeted observers see the entity exit any matched query.\nregisterObserversAPI({\n dispatchDestroyObservers(state: WorldState, eid: EntityId): void {\n const w = state.options.maskWordCount\n const base = (eid as number) * w\n\n // Snapshot the pre-destroy mask so Phase 2's `wasMatch` is computed\n // against the state at destroy entry — Phase 1 handlers might reentrant-\n // mutate `state.entityMask` (e.g. by calling removeComponent on a\n // sibling), and we still want query observers to fire for queries the\n // entity was matching *before* the destroy began.\n const preMask = new Uint32Array(w)\n for (let i = 0; i < w; i++) preMask[i] = state.entityMask[base + i] ?? 0\n\n // Phase 1: component-level remove for every bit set at destroy entry.\n forEachSetBit(preMask, 0, w, (bit) => {\n const snapshot = Array.from(state.observers)\n for (const obs of snapshot) {\n if (obs.event !== 'remove') continue\n if (obs.componentBit !== bit) continue\n if (!state.observers.includes(obs)) continue\n obs.handler(eid)\n }\n })\n\n // Phase 2: query-level remove for any query that was matching this entity.\n // Read the pre-destroy mask snapshot (not live state.entityMask) so this\n // phase is decoupled from Phase 1 reentrant mutations.\n const querySnapshot = Array.from(state.observers)\n for (const obs of querySnapshot) {\n if (obs.event !== 'remove') continue\n if (obs.queryId === -1) continue\n if (!state.observers.includes(obs)) continue\n const bundle = state.queryMasks.get(obs.queryId)\n if (!bundle) continue\n const wasMatch = matchesEntityMask(\n preMask,\n 0,\n w,\n bundle.withMask,\n bundle.anyMask,\n bundle.noneMask,\n bundle.anyHasBits,\n )\n if (wasMatch) obs.handler(eid)\n }\n },\n})\n"]}
1
+ {"version":3,"sources":["../src/internal/bitmask.ts","../src/internal/world.ts","../src/internal/component.ts","../src/internal/query.ts","../src/observers.ts"],"names":["createMask","wordCount","setBit","mask","bit","w","isMaskZero","m","i","matches","withMask","anyMask","noneMask","anyHasBits","words","anyHit","wm","am","nm","listBits","out","word","ctz32","v","worldRegistry","getWorldState","world","state","getOrRegisterComponentBit","info","existing","storage","soa","f","componentInfoById","getComponentInfo","component","ensureQueryRegistered","q","ww","compId","getComponentInfoById","bundle","involvedBits","b","s","getQueryArchetypes","list","arch","runQuery","query","buf","e","archIds","id","r","componentId","bindAbortSignal","unsubscribe","signal","onAbort","detached","onAdd","handler","opts","registerComponentObserver","onRemove","onSet","observe","event","entry","idx"],"mappings":"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","file":"observers.js","sourcesContent":["// Multi-word Uint32 bitmask helpers.\n\nexport function createMask(wordCount: number): Uint32Array {\n return new Uint32Array(wordCount)\n}\n\nexport function setBit(mask: Uint32Array, bit: number): void {\n const w = bit >>> 5\n mask[w] = (mask[w] ?? 0) | (1 << (bit & 31))\n}\n\nexport function clearBit(mask: Uint32Array, bit: number): void {\n const w = bit >>> 5\n mask[w] = (mask[w] ?? 0) & ~(1 << (bit & 31))\n}\n\nexport function testBit(mask: Uint32Array, bit: number): boolean {\n const w = bit >>> 5\n return ((mask[w] ?? 0) & (1 << (bit & 31))) !== 0\n}\n\nexport function copyMask(dst: Uint32Array, src: Uint32Array): void {\n const n = src.length\n for (let i = 0; i < n; i++) dst[i] = src[i] ?? 0\n}\n\nexport function cloneMask(src: Uint32Array): Uint32Array {\n const out = new Uint32Array(src.length)\n for (let i = 0; i < src.length; i++) out[i] = src[i] ?? 0\n return out\n}\n\nexport function maskEquals(a: Uint32Array, b: Uint32Array): boolean {\n const n = a.length\n if (b.length !== n) return false\n for (let i = 0; i < n; i++) if ((a[i] ?? 0) !== (b[i] ?? 0)) return false\n return true\n}\n\nexport function maskHash(m: Uint32Array): string {\n // Stable string key for Map lookups. Hex-joined.\n const parts: string[] = []\n for (let i = 0; i < m.length; i++) {\n parts.push((m[i] ?? 0).toString(16))\n }\n return parts.join(',')\n}\n\nexport function isMaskZero(m: Uint32Array): boolean {\n for (let i = 0; i < m.length; i++) if ((m[i] ?? 0) !== 0) return false\n return true\n}\n\nexport function unionMask(a: Uint32Array, b: Uint32Array): Uint32Array {\n const out = new Uint32Array(a.length)\n for (let i = 0; i < a.length; i++) out[i] = (a[i] ?? 0) | (b[i] ?? 0)\n return out\n}\n\n/**\n * Test whether an archetype's mask matches a query.\n * - withMask: all of these bits must be present\n * - anyMask: if anyHasBits, at least one of these bits must be present\n * - noneMask: none of these bits may be present\n */\nexport function matches(\n mask: Uint32Array,\n withMask: Uint32Array,\n anyMask: Uint32Array,\n noneMask: Uint32Array,\n anyHasBits: boolean,\n words: number,\n): boolean {\n let anyHit = !anyHasBits\n for (let w = 0; w < words; w++) {\n const m = mask[w] ?? 0\n const wm = withMask[w] ?? 0\n const am = anyMask[w] ?? 0\n const nm = noneMask[w] ?? 0\n if ((m & wm) !== wm) return false\n if ((m & nm) !== 0) return false\n if (!anyHit && (m & am) !== 0) anyHit = true\n }\n return anyHit\n}\n\nexport function listBits(mask: Uint32Array): number[] {\n const out: number[] = []\n for (let w = 0; w < mask.length; w++) {\n let word = mask[w] ?? 0\n while (word !== 0) {\n const bit = (w << 5) + ctz32(word)\n out.push(bit)\n word &= word - 1\n }\n }\n return out\n}\n\n// Iterate set bits in a slice of a larger Uint32Array starting at `base`.\n// Shared by destroy paths and storage cleanup so the bit-extraction maths\n// lives in exactly one place.\nexport function forEachSetBit(\n mask: Uint32Array,\n base: number,\n words: number,\n fn: (bit: number) => void,\n): void {\n for (let wi = 0; wi < words; wi++) {\n let word = mask[base + wi] ?? 0\n while (word !== 0) {\n const bit = (wi << 5) + ctz32(word)\n fn(bit)\n word &= word - 1\n }\n }\n}\n\n// Match a query against an entity's mask stored as a slice inside a larger\n// Uint32Array. Reading directly from the parent array avoids the temporary\n// Uint32Array allocation on every observer dispatch.\nexport function matchesEntityMask(\n entityMask: Uint32Array,\n base: number,\n words: number,\n withMask: Uint32Array,\n anyMask: Uint32Array,\n noneMask: Uint32Array,\n anyHasBits: boolean,\n): boolean {\n let anyHit = !anyHasBits\n for (let i = 0; i < words; i++) {\n const m = entityMask[base + i] ?? 0\n const wm = withMask[i] ?? 0\n const am = anyMask[i] ?? 0\n const nm = noneMask[i] ?? 0\n if ((m & wm) !== wm) return false\n if ((m & nm) !== 0) return false\n if (!anyHit && (m & am) !== 0) anyHit = true\n }\n return anyHit\n}\n\nfunction ctz32(v: number): number {\n // Count trailing zeros in a 32-bit integer (assumes v !== 0)\n return 31 - Math.clz32(v & -v)\n}\n","import { VERSION } from '../version.js'\nimport { cloneMask, copyMask, createMask, maskHash } from './bitmask.js'\nimport type {\n ArchetypeState,\n ComponentInfo,\n FieldInfo,\n ResolvedWorldOptions,\n World,\n WorldComponentStorage,\n WorldOptions,\n WorldState,\n} from './types.js'\n\nexport const WORLD_BRAND = Symbol.for('aiecsjs.world')\n\nconst DEFAULT_OPTIONS: Required<Omit<WorldOptions, 'buffer' | 'bufferByteOffset'>> = {\n initialCapacity: 1024,\n maxEntities: 1_000_000,\n indexBits: 24,\n generationBits: 8,\n}\n\nconst DEFAULT_MAX_COMPONENTS = 256\n\nlet nextWorldId = 1\nconst worldRegistry = new Map<number, WorldState>()\n\nexport function getWorldState(world: World): WorldState {\n const state = worldRegistry.get(world.id)\n if (!state) throw new Error(`aiecsjs: world ${world.id} is destroyed or unknown`)\n if (state.destroyed) throw new Error(`aiecsjs: world ${world.id} is destroyed`)\n return state\n}\n\nexport function registerWorld(state: WorldState): void {\n worldRegistry.set(state.id, state)\n}\n\nexport function unregisterWorld(id: number): void {\n worldRegistry.delete(id)\n}\n\nexport function isWorldRegistered(id: number): boolean {\n return worldRegistry.has(id)\n}\n\nfunction resolveOptions(opts: WorldOptions | undefined): ResolvedWorldOptions {\n const o = opts ?? {}\n const indexBits = o.indexBits ?? DEFAULT_OPTIONS.indexBits\n const generationBits = o.generationBits ?? DEFAULT_OPTIONS.generationBits\n const maxComponents = DEFAULT_MAX_COMPONENTS\n if (indexBits < 1 || indexBits > 24) {\n throw new Error('aiecsjs: indexBits must be in [1, 24]')\n }\n if (generationBits < 0 || generationBits > 16) {\n throw new Error('aiecsjs: generationBits must be in [0, 16]')\n }\n const maxByIndex = 1 << indexBits\n const initialCapacity = Math.max(\n 1,\n Math.min(o.initialCapacity ?? DEFAULT_OPTIONS.initialCapacity, maxByIndex),\n )\n const maxEntities = Math.max(\n initialCapacity,\n Math.min(o.maxEntities ?? DEFAULT_OPTIONS.maxEntities, maxByIndex),\n )\n return {\n initialCapacity,\n maxEntities,\n indexBits,\n generationBits,\n indexMask: (1 << indexBits) - 1,\n generationMask: generationBits === 0 ? 0 : (1 << generationBits) - 1,\n maxComponents,\n maskWordCount: Math.ceil(maxComponents / 32),\n buffer: o.buffer ?? null,\n bufferByteOffset: o.bufferByteOffset ?? 0,\n }\n}\n\nexport function createEmptyArchetype(maskWordCount: number, maxComponents: number): ArchetypeState {\n return {\n id: 0,\n mask: createMask(maskWordCount),\n size: 0,\n capacity: 16,\n entities: new Uint32Array(16),\n entityRow: new Map<number, number>(),\n componentBits: [],\n edgeAdd: new Int32Array(maxComponents).fill(-1),\n edgeRemove: new Int32Array(maxComponents).fill(-1),\n }\n}\n\nexport function createWorld(options?: WorldOptions): World {\n const resolved = resolveOptions(options)\n const id = nextWorldId++\n\n const generationCtor = resolved.generationBits > 8 ? Uint16Array : Uint8Array\n const generations = new generationCtor(resolved.initialCapacity)\n\n const state: WorldState = {\n id,\n capacity: resolved.initialCapacity,\n version: VERSION,\n options: resolved,\n size: 0,\n nextFreshIndex: 1, // 0 reserved\n freeList: [],\n generations,\n destroyed: false,\n componentBitFor: new Map<number, number>(),\n componentInfoByBit: new Array(resolved.maxComponents).fill(null),\n componentStorageByBit: new Array(resolved.maxComponents).fill(null),\n nextComponentBit: 0,\n entityArchetype: new Uint32Array(resolved.initialCapacity),\n entityMask: new Uint32Array(resolved.initialCapacity * resolved.maskWordCount),\n archetypes: [],\n archetypeByMaskHash: new Map<string, number>(),\n queryVersion: 0,\n queries: [],\n queryMasks: new Map(),\n queryArchetypeCache: [],\n queryArchetypeStamp: [],\n bitToQueries: new Map<number, Set<number>>(),\n reactiveBuffers: new Map(),\n observers: [],\n relationStorage: new Map(),\n sab: resolved.buffer,\n readOnly: false,\n }\n\n // Seed archetype 0 (the empty mask)\n const empty = createEmptyArchetype(resolved.maskWordCount, resolved.maxComponents)\n empty.id = 0\n state.archetypes.push(empty)\n state.archetypeByMaskHash.set(maskHash(empty.mask), 0)\n\n registerWorld(state)\n return makePublicWorld(state)\n}\n\nexport function makePublicWorld(state: WorldState): World {\n return {\n id: state.id,\n get capacity() {\n return state.capacity\n },\n version: state.version,\n } as World\n}\n\nexport function destroyWorld(world: World): void {\n const state = worldRegistry.get(world.id)\n if (!state || state.destroyed) return\n state.destroyed = true\n // Clear large buffers to help GC\n state.archetypes = []\n state.archetypeByMaskHash.clear()\n state.componentInfoByBit = []\n state.componentStorageByBit = []\n state.queries = []\n state.queryMasks.clear()\n state.queryArchetypeCache = []\n state.observers = []\n state.relationStorage.clear()\n state.reactiveBuffers.clear()\n unregisterWorld(world.id)\n}\n\nexport function resetWorld(world: World): void {\n const state = getWorldState(world)\n // Keep capacity and registered components; clear entities and per-entity state.\n state.size = 0\n state.nextFreshIndex = 1\n state.freeList = []\n state.generations.fill(0)\n state.entityArchetype.fill(0)\n state.entityMask.fill(0)\n // Wipe archetype memberships but keep archetype graph for stability.\n for (const arch of state.archetypes) {\n arch.size = 0\n arch.entityRow.clear()\n }\n // Clear SoA columns and AoS instances; storage allocation is preserved.\n for (const storage of state.componentStorageByBit) {\n if (!storage) continue\n if (storage.kind === 'soa' && storage.soa) {\n for (const k of Object.keys(storage.soa)) {\n storage.soa[k]?.fill(0)\n }\n } else if (storage.kind === 'aos' && storage.aos) {\n storage.aos.fill(undefined as unknown as never)\n }\n }\n state.queryVersion++\n for (const buf of state.reactiveBuffers.values()) {\n buf.entered.length = 0\n buf.exited.length = 0\n }\n}\n\nexport function getWorldSize(world: World): number {\n return getWorldState(world).size\n}\n\nexport function getWorldCapacity(world: World): number {\n return getWorldState(world).capacity\n}\n\n// --- Capacity growth ---\n\nexport function ensureCapacity(state: WorldState, needed: number): void {\n if (needed <= state.capacity) return\n if (needed > state.options.maxEntities) {\n throw new Error(\n `aiecsjs: requested capacity ${needed} exceeds maxEntities ${state.options.maxEntities}`,\n )\n }\n let newCap = state.capacity\n while (newCap < needed) newCap = Math.min(newCap * 2, state.options.maxEntities)\n growEntityArrays(state, newCap)\n}\n\nfunction growEntityArrays(state: WorldState, newCap: number): void {\n // generations\n const genCtor: any = state.generations.constructor\n const newGen = new genCtor(newCap) as Uint8Array | Uint16Array\n ;(newGen as Uint8Array).set(state.generations as Uint8Array)\n state.generations = newGen\n\n // entityArchetype\n const newArch = new Uint32Array(newCap)\n newArch.set(state.entityArchetype)\n state.entityArchetype = newArch\n\n // entityMask\n const wordCount = state.options.maskWordCount\n const newMask = new Uint32Array(newCap * wordCount)\n newMask.set(state.entityMask)\n state.entityMask = newMask\n\n // Component storages (SoA columns + AoS arrays)\n for (const storage of state.componentStorageByBit) {\n if (!storage) continue\n if (storage.kind === 'soa' && storage.soa) {\n const info = state.componentInfoByBit[storage.bit]\n if (info) growSoAColumns(storage.soa, info.fields, newCap)\n } else if (storage.kind === 'aos' && storage.aos) {\n storage.aos.length = newCap\n }\n }\n\n state.capacity = newCap\n}\n\nfunction growSoAColumns(soa: Record<string, any>, fields: FieldInfo[], newCap: number): void {\n for (const f of fields) {\n const old = soa[f.name]\n if (!old) continue\n const newLen = newCap * f.vectorLen\n if (old.length >= newLen) continue\n const next = new f.ctor(newLen)\n next.set(old)\n soa[f.name] = next\n }\n}\n\n// --- Archetype management ---\n\nexport function findOrCreateArchetype(\n state: WorldState,\n mask: Uint32Array,\n): { archId: number; created: boolean } {\n const key = maskHash(mask)\n const existing = state.archetypeByMaskHash.get(key)\n if (existing !== undefined) return { archId: existing, created: false }\n\n const id = state.archetypes.length\n const arch: ArchetypeState = {\n id,\n mask: cloneMask(mask),\n size: 0,\n capacity: 16,\n entities: new Uint32Array(16),\n entityRow: new Map<number, number>(),\n componentBits: collectBits(mask),\n edgeAdd: new Int32Array(state.options.maxComponents).fill(-1),\n edgeRemove: new Int32Array(state.options.maxComponents).fill(-1),\n }\n state.archetypes.push(arch)\n state.archetypeByMaskHash.set(key, id)\n state.queryVersion++\n return { archId: id, created: true }\n}\n\nexport function ensureArchetypeCapacity(arch: ArchetypeState, needed: number): void {\n if (needed <= arch.capacity) return\n let newCap = arch.capacity\n while (newCap < needed) newCap *= 2\n const next = new Uint32Array(newCap)\n next.set(arch.entities)\n arch.entities = next\n arch.capacity = newCap\n}\n\nfunction collectBits(mask: Uint32Array): number[] {\n const bits: number[] = []\n for (let w = 0; w < mask.length; w++) {\n let word = mask[w] ?? 0\n while (word !== 0) {\n const lsb = word & -word\n const bit = (w << 5) + (31 - Math.clz32(lsb))\n bits.push(bit)\n word &= word - 1\n }\n }\n return bits\n}\n\n// --- Component registration in a world ---\n\nexport function getOrRegisterComponentBit(state: WorldState, info: ComponentInfo): number {\n const existing = state.componentBitFor.get(info.id)\n if (existing !== undefined) return existing\n\n if (state.nextComponentBit >= state.options.maxComponents) {\n throw new Error(`aiecsjs: world reached maxComponents=${state.options.maxComponents}`)\n }\n const bit = state.nextComponentBit++\n state.componentBitFor.set(info.id, bit)\n state.componentInfoByBit[bit] = info\n\n // Create storage\n let storage: WorldComponentStorage\n if (info.kind === 'soa') {\n const soa: Record<string, any> = {}\n for (const f of info.fields) {\n soa[f.name] = new f.ctor(state.capacity * f.vectorLen)\n }\n storage = { kind: 'soa', componentId: info.id, bit, soa }\n } else if (info.kind === 'aos') {\n storage = { kind: 'aos', componentId: info.id, bit, aos: new Array(state.capacity) }\n } else {\n storage = { kind: 'tag', componentId: info.id, bit }\n }\n state.componentStorageByBit[bit] = storage\n return bit\n}\n\n// Read a bitmask out of state.entityMask[] into a fresh Uint32Array.\nexport function readEntityMask(state: WorldState, eid: number): Uint32Array {\n const w = state.options.maskWordCount\n const out = new Uint32Array(w)\n const idx = eid & state.options.indexMask\n const base = idx * w\n for (let i = 0; i < w; i++) out[i] = state.entityMask[base + i] ?? 0\n return out\n}\n\nexport function writeEntityMask(state: WorldState, eid: number, mask: Uint32Array): void {\n const w = state.options.maskWordCount\n const idx = eid & state.options.indexMask\n const base = idx * w\n for (let i = 0; i < w; i++) state.entityMask[base + i] = mask[i] ?? 0\n}\n\n// Get the bit of a component within a world without registering.\nexport function tryGetComponentBit(state: WorldState, info: ComponentInfo): number | undefined {\n return state.componentBitFor.get(info.id)\n}\n\nexport function isWorld(x: unknown): x is World {\n if (!x || typeof x !== 'object') return false\n const obj = x as { id?: unknown; version?: unknown }\n if (typeof obj.id !== 'number') return false\n if (typeof obj.version !== 'string') return false\n return worldRegistry.has(obj.id)\n}\n","import { clearBit, cloneMask, forEachSetBit, setBit, testBit } from './bitmask.js'\nimport { isAliveInternal } from './entity.js'\nimport type {\n AoSComponent,\n ComponentInfo,\n ComponentInit,\n ComponentLike,\n EntityId,\n FieldInfo,\n SoAColumns,\n SoAComponent,\n SoASchema,\n TagComponent,\n TypedArrayConstructor,\n World,\n WorldComponentStorage,\n WorldState,\n} from './types.js'\nimport {\n ensureArchetypeCapacity,\n findOrCreateArchetype,\n getOrRegisterComponentBit,\n getWorldState,\n readEntityMask,\n tryGetComponentBit,\n writeEntityMask,\n} from './world.js'\n\n// --- Component factories ---\n\nlet nextComponentId = 1\n\nconst TYPE_CTOR: Record<string, TypedArrayConstructor> = {\n i8: Int8Array,\n u8: Uint8Array,\n i16: Int16Array,\n u16: Uint16Array,\n i32: Int32Array,\n u32: Uint32Array,\n f32: Float32Array,\n f64: Float64Array,\n eid: Uint32Array,\n bool: Uint8Array,\n}\n\nconst componentInfoById = new Map<number, ComponentInfo>()\n\nexport function defineComponent<S extends SoASchema>(schema: S): SoAComponent<S> {\n const fields: FieldInfo[] = []\n for (const [name, decl] of Object.entries(schema)) {\n let type: keyof typeof TYPE_CTOR\n let vectorLen = 1\n if (typeof decl === 'string') {\n type = decl\n } else if (Array.isArray(decl)) {\n const [t, len] = decl\n type = t\n vectorLen = len\n } else {\n throw new TypeError(`aiecsjs: invalid field declaration for \"${name}\"`)\n }\n const ctor = TYPE_CTOR[type]\n if (!ctor) throw new TypeError(`aiecsjs: unknown field type \"${type}\" for field \"${name}\"`)\n fields.push({\n name,\n type: type as any,\n vectorLen,\n ctor,\n bytesPerElement: (ctor as any).BYTES_PER_ELEMENT,\n })\n }\n const id = nextComponentId++\n const info: ComponentInfo = { id, kind: 'soa', schema, fields, factory: null }\n componentInfoById.set(id, info)\n const handle: SoAComponent<S> = {\n __kind: 'soa',\n __id: id,\n __schema: schema,\n }\n return handle\n}\n\nexport function defineTag(): TagComponent {\n const id = nextComponentId++\n const info: ComponentInfo = { id, kind: 'tag', schema: null, fields: [], factory: null }\n componentInfoById.set(id, info)\n return { __kind: 'tag', __id: id }\n}\n\nexport function defineObjectComponent<T>(factory?: () => T): AoSComponent<T> {\n const id = nextComponentId++\n const fac = factory ?? (() => ({}) as T)\n const info: ComponentInfo = { id, kind: 'aos', schema: null, fields: [], factory: fac as any }\n componentInfoById.set(id, info)\n return { __kind: 'aos', __id: id, __factory: fac }\n}\n\nexport function getComponentInfo(component: ComponentLike): ComponentInfo {\n const info = componentInfoById.get(component.__id)\n if (!info)\n throw new Error(\n 'aiecsjs: component is not registered (call defineComponent/defineTag/defineObjectComponent)',\n )\n return info\n}\n\n// --- Component ops on entities ---\n\nexport function addComponent<C extends ComponentLike>(\n world: World,\n eid: EntityId,\n component: C,\n initial?: ComponentInit<C>,\n): void {\n const state = getWorldState(world)\n if (state.readOnly) throw new Error('aiecsjs: cannot mutate a read-only world')\n if (!isAliveInternal(state, eid)) {\n throw new Error(`aiecsjs: addComponent on dead entity ${eid}`)\n }\n const info = getComponentInfo(component)\n const bit = getOrRegisterComponentBit(state, info)\n\n const prevMask = readEntityMask(state, eid)\n if (testBit(prevMask, bit)) {\n if (initial !== undefined) writeInitial(state, eid as number, component, initial)\n return\n }\n const newMask = cloneMask(prevMask)\n setBit(newMask, bit)\n migrateEntity(state, eid as number, newMask)\n\n writeInitial(state, eid as number, component, initial)\n\n fireAddObservers(state, eid, bit)\n notifyMaskChange(state, eid, bit, prevMask, newMask)\n}\n\nexport function removeComponent<C extends ComponentLike>(\n world: World,\n eid: EntityId,\n component: C,\n): void {\n const state = getWorldState(world)\n if (state.readOnly) throw new Error('aiecsjs: cannot mutate a read-only world')\n if (!isAliveInternal(state, eid)) return\n const info = getComponentInfo(component)\n const bit = tryGetComponentBit(state, info)\n if (bit === undefined) return\n const prevMask = readEntityMask(state, eid)\n if (!testBit(prevMask, bit)) return\n\n // Write the new mask BEFORE firing observers so query-targeted observers\n // reading `state.entityMask` see the post-removal state (and thus correctly\n // detect \"entity left this query\"). Without this reorder, a query that\n // requires the removed component would still match during dispatch and the\n // remove observer would never fire. Component-targeted observers receive the\n // bit directly and don't depend on mask timing. addComponent already follows\n // this \"mutate then fire\" order; keeping removeComponent consistent.\n const newMask = cloneMask(prevMask)\n clearBit(newMask, bit)\n migrateEntity(state, eid as number, newMask)\n\n fireRemoveObservers(state, eid, bit)\n\n const idx = (eid as number) & state.options.indexMask\n const storage = state.componentStorageByBit[bit]\n if (storage?.kind === 'soa' && storage.soa) {\n clearSoAEntity(storage.soa, info.fields, idx)\n } else if (storage?.kind === 'aos' && storage.aos) {\n storage.aos[idx] = undefined\n }\n\n notifyMaskChange(state, eid, bit, prevMask, newMask)\n}\n\nexport function hasComponent<C extends ComponentLike>(\n world: World,\n eid: EntityId,\n component: C,\n): boolean {\n const state = getWorldState(world)\n if (!isAliveInternal(state, eid)) return false\n const info = getComponentInfo(component)\n const bit = tryGetComponentBit(state, info)\n if (bit === undefined) return false\n const mask = readEntityMask(state, eid)\n return testBit(mask, bit)\n}\n\nexport function getComponent<C extends ComponentLike>(\n world: World,\n eid: EntityId,\n component: C,\n): unknown {\n const state = getWorldState(world)\n if (!isAliveInternal(state, eid)) return undefined\n const info = getComponentInfo(component)\n const bit = tryGetComponentBit(state, info)\n if (bit === undefined) return undefined\n const mask = readEntityMask(state, eid)\n if (!testBit(mask, bit)) return undefined\n const storage = state.componentStorageByBit[bit]\n if (!storage) return undefined\n if (storage.kind === 'soa') return storage.soa\n if (storage.kind === 'aos') {\n const idx = (eid as number) & state.options.indexMask\n return storage.aos?.[idx]\n }\n return true // tag\n}\n\nexport function setComponent<C extends ComponentLike, V>(\n world: World,\n eid: EntityId,\n component: C,\n value: V,\n): void {\n const state = getWorldState(world)\n if (state.readOnly) throw new Error('aiecsjs: cannot mutate a read-only world')\n if (!isAliveInternal(state, eid)) throw new Error(`aiecsjs: setComponent on dead entity ${eid}`)\n const info = getComponentInfo(component)\n const bit = tryGetComponentBit(state, info)\n if (bit === undefined) {\n // Adding via set\n addComponent(world, eid, component, value as any)\n return\n }\n const mask = readEntityMask(state, eid)\n if (!testBit(mask, bit)) {\n addComponent(world, eid, component, value as any)\n return\n }\n writeInitial(state, eid as number, component, value as any)\n fireSetObservers(state, eid, bit, value)\n}\n\n// --- Internals ---\n\nfunction writeInitial(\n state: WorldState,\n packedEid: number,\n component: ComponentLike,\n initial: unknown,\n): void {\n const info = getComponentInfo(component)\n const bit = state.componentBitFor.get(info.id)\n if (bit === undefined) return\n const storage = state.componentStorageByBit[bit]\n if (!storage) return\n // Unpack to raw index for storage access\n const idx = packedEid & state.options.indexMask\n if (info.kind === 'soa' && storage.soa) {\n if (initial == null) return\n const obj = initial as Record<string, unknown>\n for (const f of info.fields) {\n if (!(f.name in obj)) continue\n const col = storage.soa[f.name]\n if (!col) continue\n const val = obj[f.name]\n if (f.vectorLen === 1) {\n col[idx] = typeof val === 'boolean' ? (val ? 1 : 0) : Number(val)\n } else if (Array.isArray(val) || ArrayBuffer.isView(val)) {\n const base = idx * f.vectorLen\n const arr = val as ArrayLike<number>\n for (let i = 0; i < f.vectorLen; i++) col[base + i] = Number(arr[i] ?? 0)\n }\n }\n } else if (info.kind === 'aos' && storage.aos) {\n const factory = info.factory ?? (() => ({}))\n let inst = storage.aos[idx]\n if (inst === undefined) {\n inst = factory()\n storage.aos[idx] = inst\n }\n if (initial && typeof initial === 'object') {\n // SECURITY: do NOT use `Object.assign(inst, initial)` here. When `initial`\n // comes from `JSON.parse(untrustedBytes)` — e.g. via `fromJSON`,\n // `deserializeWorld`, or app code piping a bridge payload — the parsed\n // object can carry an OWN `__proto__` property, and `Object.assign` uses\n // [[Set]] semantics which would trigger the proto setter and clobber the\n // instance's prototype chain. Explicit own-key copy with a deny-list\n // closes the prototype-pollution path.\n const src = initial as Record<string, unknown>\n const target = inst as Record<string, unknown>\n for (const key of Object.keys(src)) {\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue\n target[key] = src[key]\n }\n }\n }\n // tag: nothing\n}\n\nfunction clearSoAEntity(soa: SoAColumns, fields: FieldInfo[], eid: number): void {\n for (const f of fields) {\n const col = soa[f.name]\n if (!col) continue\n if (f.vectorLen === 1) {\n col[eid] = 0\n } else {\n const base = eid * f.vectorLen\n for (let i = 0; i < f.vectorLen; i++) col[base + i] = 0\n }\n }\n}\n\n// Clear every component-storage slot that the entity currently owns.\n// Walks the entity's mask; for each set bit, zeroes the SoA columns or\n// undefines the AoS slot at the entity index. Tag storages are bit-only.\n// Caller (e.g. destroyEntity) is expected to clear the mask separately.\nexport function clearAllEntityStorages(state: WorldState, eid: EntityId): void {\n const wordCount = state.options.maskWordCount\n const idx = (eid as number) & state.options.indexMask\n const base = idx * wordCount\n forEachSetBit(state.entityMask, base, wordCount, (bit) => {\n const storage = state.componentStorageByBit[bit]\n const info = state.componentInfoByBit[bit]\n if (storage?.kind === 'soa' && storage.soa && info) {\n clearSoAEntity(storage.soa, info.fields, idx)\n } else if (storage?.kind === 'aos' && storage.aos) {\n storage.aos[idx] = undefined\n }\n })\n}\n\nfunction migrateEntity(state: WorldState, packedEid: number, newMask: Uint32Array): void {\n // Unpack to raw index for sparse-array lookups\n const idx = packedEid & state.options.indexMask\n const srcArchId = state.entityArchetype[idx] ?? 0\n const srcArch = state.archetypes[srcArchId]\n if (!srcArch) return\n\n const found = findOrCreateArchetype(state, newMask)\n const destArchId = found.archId\n const destArch = state.archetypes[destArchId]\n if (!destArch) return\n\n if (destArchId !== srcArchId) {\n // Swap-pop from src (arch.entities / entityRow use packed eid as key)\n const row = srcArch.entityRow.get(packedEid)\n if (row !== undefined) {\n const lastRow = srcArch.size - 1\n if (row !== lastRow) {\n const moved = srcArch.entities[lastRow] ?? 0\n srcArch.entities[row] = moved\n srcArch.entityRow.set(moved, row)\n }\n srcArch.entities[lastRow] = 0\n srcArch.entityRow.delete(packedEid)\n srcArch.size--\n }\n\n // Append to dest (store packed eid)\n ensureArchetypeCapacity(destArch, destArch.size + 1)\n const newRow = destArch.size\n destArch.entities[newRow] = packedEid\n destArch.entityRow.set(packedEid, newRow)\n destArch.size++\n\n state.entityArchetype[idx] = destArchId\n }\n\n writeEntityMask(state, packedEid, newMask)\n}\n\n// --- Observer dispatch (lazy-bound) ---\n\ninterface ObserversDispatchAPI {\n fireAdd(state: WorldState, eid: EntityId, bit: number): void\n fireRemove(state: WorldState, eid: EntityId, bit: number): void\n fireSet(state: WorldState, eid: EntityId, bit: number, value: unknown): void\n}\nlet _dispatch: ObserversDispatchAPI = {\n fireAdd: () => {},\n fireRemove: () => {},\n fireSet: () => {},\n}\n\ntype MaskChangeFn = (\n state: WorldState,\n eid: EntityId,\n bit: number,\n prev: Uint32Array,\n next: Uint32Array,\n) => void\nlet _maskChange: MaskChangeFn = () => {}\nexport function registerMaskChangeDispatch(fn: MaskChangeFn): void {\n _maskChange = fn\n}\nfunction notifyMaskChange(\n state: WorldState,\n eid: number,\n bit: number,\n prev: Uint32Array,\n next: Uint32Array,\n): void {\n _maskChange(state, eid as EntityId, bit, prev, next)\n}\nexport function registerObserverDispatch(api: ObserversDispatchAPI): void {\n _dispatch = api\n}\nfunction fireAddObservers(state: WorldState, eid: number, bit: number): void {\n _dispatch.fireAdd(state, eid as EntityId, bit)\n}\nfunction fireRemoveObservers(state: WorldState, eid: number, bit: number): void {\n _dispatch.fireRemove(state, eid as EntityId, bit)\n}\nfunction fireSetObservers(state: WorldState, eid: number, bit: number, value: unknown): void {\n _dispatch.fireSet(state, eid as EntityId, bit, value)\n}\n\n// --- Helper for serialize/worker ---\n\nexport function getComponentByInternalId(id: number): ComponentInfo | undefined {\n return componentInfoById.get(id)\n}\n\nexport function listAllComponents(): ComponentInfo[] {\n return Array.from(componentInfoById.values())\n}\n\nexport function _resetComponentRegistry_FOR_TESTS_ONLY(): void {\n componentInfoById.clear()\n nextComponentId = 1\n}\n","import { createMask, isMaskZero, listBits, matches, setBit } from './bitmask.js'\nimport { getComponentInfo } from './component.js'\nimport type {\n Archetype,\n ComponentInfo,\n ComponentLike,\n EntityId,\n Query,\n QueryDescriptor,\n QueryInternal,\n QueryMaskBundle,\n ReactiveBuffer,\n World,\n WorldState,\n} from './types.js'\nimport {\n getOrRegisterComponentBit,\n getWorldState,\n readEntityMask,\n tryGetComponentBit,\n} from './world.js'\n\nlet nextQueryId = 1\nconst moduleQueryCache = new Map<string, QueryInternal>()\n\nfunction descKey(d: QueryDescriptor): string {\n const all = (d.all ?? [])\n .map((c) => c.__id)\n .sort((a, b) => a - b)\n .join('-')\n const any = (d.any ?? [])\n .map((c) => c.__id)\n .sort((a, b) => a - b)\n .join('-')\n const none = (d.none ?? [])\n .map((c) => c.__id)\n .sort((a, b) => a - b)\n .join('-')\n return `A${all}|Y${any}|N${none}`\n}\n\nexport function defineQuery(arg: ComponentLike[] | QueryDescriptor): Query {\n const desc: QueryDescriptor = Array.isArray(arg) ? { all: arg } : arg\n\n // Validate all members are components\n for (const c of [...(desc.all ?? []), ...(desc.any ?? []), ...(desc.none ?? [])]) {\n if (!c || typeof c !== 'object' || typeof (c as any).__id !== 'number') {\n throw new TypeError('aiecsjs: defineQuery received a non-component value')\n }\n }\n\n const key = descKey(desc)\n const cached = moduleQueryCache.get(key)\n if (cached) return cached\n\n const q: QueryInternal = {\n id: nextQueryId++,\n mask: [],\n all: (desc.all ?? []).map((c) => c.__id),\n any: (desc.any ?? []).map((c) => c.__id),\n none: (desc.none ?? []).map((c) => c.__id),\n columnViewCache: [...(desc.all ?? []), ...(desc.any ?? [])],\n reactiveKind: 'normal',\n sourceQueryId: -1,\n sourceQuery: null,\n }\n moduleQueryCache.set(key, q)\n return q\n}\n\nexport function enterQuery(query: Query): Query {\n const q = query as QueryInternal\n const key = `enter:${q.id}`\n const cached = moduleQueryCache.get(key)\n if (cached) return cached\n const reactive: QueryInternal = {\n id: nextQueryId++,\n mask: [],\n all: q.all,\n any: q.any,\n none: q.none,\n columnViewCache: q.columnViewCache,\n reactiveKind: 'enter',\n sourceQueryId: q.id,\n sourceQuery: q,\n }\n moduleQueryCache.set(key, reactive)\n return reactive\n}\n\nexport function exitQuery(query: Query): Query {\n const q = query as QueryInternal\n const key = `exit:${q.id}`\n const cached = moduleQueryCache.get(key)\n if (cached) return cached\n const reactive: QueryInternal = {\n id: nextQueryId++,\n mask: [],\n all: q.all,\n any: q.any,\n none: q.none,\n columnViewCache: q.columnViewCache,\n reactiveKind: 'exit',\n sourceQueryId: q.id,\n sourceQuery: q,\n }\n moduleQueryCache.set(key, reactive)\n return reactive\n}\n\n// --- Per-world query setup ---\n\nfunction ensureQueryRegistered(state: WorldState, q: QueryInternal): void {\n if (state.queries[q.id] === q && state.queryMasks.has(q.id)) return\n\n // Build per-world bitmasks (this may register new bits)\n const ww = state.options.maskWordCount\n const withMask = createMask(ww)\n const anyMask = createMask(ww)\n const noneMask = createMask(ww)\n for (const compId of q.all) {\n const info = getComponentInfoById(compId)\n const bit = getOrRegisterComponentBit(state, info)\n setBit(withMask, bit)\n }\n for (const compId of q.any) {\n const info = getComponentInfoById(compId)\n const bit = getOrRegisterComponentBit(state, info)\n setBit(anyMask, bit)\n }\n for (const compId of q.none) {\n const info = getComponentInfoById(compId)\n const bit = getOrRegisterComponentBit(state, info)\n setBit(noneMask, bit)\n }\n\n const bundle: QueryMaskBundle = {\n withMask,\n anyMask,\n noneMask,\n anyHasBits: !isMaskZero(anyMask),\n }\n state.queryMasks.set(q.id, bundle)\n\n state.queries[q.id] = q\n state.queryArchetypeCache[q.id] = null\n state.queryArchetypeStamp[q.id] = -1\n\n // Build bit → queries index for fast reactive lookup\n const involvedBits: number[] = [\n ...listBits(withMask),\n ...listBits(anyMask),\n ...listBits(noneMask),\n ]\n for (const b of involvedBits) {\n let s = state.bitToQueries.get(b)\n if (!s) {\n s = new Set<number>()\n state.bitToQueries.set(b, s)\n }\n s.add(q.id)\n }\n\n // Initialize reactive buffer for enter/exit queries\n if (q.reactiveKind !== 'normal') {\n if (!state.reactiveBuffers.has(q.id)) {\n state.reactiveBuffers.set(q.id, { entered: [], exited: [] })\n }\n // Also register the source query so recordEntityMaskChange will see it\n if (q.sourceQuery) ensureQueryRegistered(state, q.sourceQuery)\n }\n}\n\nfunction getQueryArchetypes(state: WorldState, q: QueryInternal): number[] {\n ensureQueryRegistered(state, q)\n if (state.queryArchetypeCache[q.id] && state.queryArchetypeStamp[q.id] === state.queryVersion) {\n return state.queryArchetypeCache[q.id]!\n }\n const bundle = state.queryMasks.get(q.id)!\n const words = state.options.maskWordCount\n const list: number[] = []\n for (let i = 0; i < state.archetypes.length; i++) {\n const arch = state.archetypes[i]!\n if (\n matches(arch.mask, bundle.withMask, bundle.anyMask, bundle.noneMask, bundle.anyHasBits, words)\n ) {\n list.push(i)\n }\n }\n state.queryArchetypeCache[q.id] = list\n state.queryArchetypeStamp[q.id] = state.queryVersion\n return list\n}\n\nexport function queryArchetypes(world: World, query: Query): readonly Archetype[] {\n const state = getWorldState(world)\n const q = query as QueryInternal\n const ids = getQueryArchetypes(state, q)\n const out: Archetype[] = []\n for (const id of ids) {\n const arch = state.archetypes[id]!\n out.push({ id: arch.id, mask: Array.from(arch.mask), size: arch.size })\n }\n return out\n}\n\nexport function runQuery(world: World, query: Query): readonly EntityId[] {\n const state = getWorldState(world)\n const q = query as QueryInternal\n const out: EntityId[] = []\n if (q.reactiveKind === 'enter') {\n const buf = state.reactiveBuffers.get(q.id)\n if (buf) {\n for (const e of buf.entered) out.push(e as EntityId)\n buf.entered.length = 0\n }\n return out\n }\n if (q.reactiveKind === 'exit') {\n const buf = state.reactiveBuffers.get(q.id)\n if (buf) {\n for (const e of buf.exited) out.push(e as EntityId)\n buf.exited.length = 0\n }\n return out\n }\n const archIds = getQueryArchetypes(state, q)\n for (const id of archIds) {\n const arch = state.archetypes[id]!\n for (let r = 0; r < arch.size; r++) {\n out.push(arch.entities[r] as EntityId)\n }\n }\n return out\n}\n\nexport function* iterQuery(world: World, query: Query): IterableIterator<EntityId> {\n const state = getWorldState(world)\n const q = query as QueryInternal\n if (q.reactiveKind !== 'normal') {\n const buf = state.reactiveBuffers.get(q.id)\n if (!buf) return\n const src = q.reactiveKind === 'enter' ? buf.entered : buf.exited\n for (const e of src) yield e as EntityId\n src.length = 0\n return\n }\n const archIds = getQueryArchetypes(state, q)\n for (const id of archIds) {\n const arch = state.archetypes[id]!\n for (let r = 0; r < arch.size; r++) {\n yield arch.entities[r] as EntityId\n }\n }\n}\n\nexport function forEachEntity(\n world: World,\n query: Query,\n fn: (eid: EntityId, ...cols: any[]) => void,\n): void {\n const state = getWorldState(world)\n const q = query as QueryInternal\n\n if (q.reactiveKind !== 'normal') {\n const buf = state.reactiveBuffers.get(q.id)\n if (!buf) return\n const src = q.reactiveKind === 'enter' ? buf.entered : buf.exited\n if (src.length === 0) return\n const cols = buildColumnViews(state, q)\n for (let i = 0; i < src.length; i++) {\n const e = src[i] as EntityId\n callWithCols(fn, e, cols)\n }\n src.length = 0\n return\n }\n\n ensureQueryRegistered(state, q)\n const archIds = getQueryArchetypes(state, q)\n const cols = buildColumnViews(state, q)\n for (const id of archIds) {\n const arch = state.archetypes[id]!\n const ents = arch.entities\n const n = arch.size\n for (let r = 0; r < n; r++) {\n callWithCols(fn, ents[r] as EntityId, cols)\n }\n }\n}\n\nfunction callWithCols(\n fn: (eid: EntityId, ...cols: any[]) => void,\n eid: EntityId,\n cols: any[],\n): void {\n // Specialise for low arities to avoid spread allocation.\n switch (cols.length) {\n case 0:\n fn(eid)\n break\n case 1:\n fn(eid, cols[0])\n break\n case 2:\n fn(eid, cols[0], cols[1])\n break\n case 3:\n fn(eid, cols[0], cols[1], cols[2])\n break\n case 4:\n fn(eid, cols[0], cols[1], cols[2], cols[3])\n break\n case 5:\n fn(eid, cols[0], cols[1], cols[2], cols[3], cols[4])\n break\n default:\n fn(eid, ...cols)\n }\n}\n\nfunction buildColumnViews(state: WorldState, q: QueryInternal): any[] {\n const out: any[] = []\n for (const comp of q.columnViewCache) {\n const info = getComponentInfo(comp)\n const bit = state.componentBitFor.get(info.id)\n if (bit === undefined) {\n out.push(undefined)\n continue\n }\n const storage = state.componentStorageByBit[bit]\n if (!storage) {\n out.push(undefined)\n continue\n }\n if (storage.kind === 'soa') out.push(storage.soa)\n else if (storage.kind === 'aos') out.push(storage.aos)\n else out.push(true) // tag\n }\n return out\n}\n\n// --- Reactive query updates (called from migration code) ---\n\nexport function recordEntityMaskChange(\n state: WorldState,\n eid: EntityId,\n changedBit: number,\n prevMask: Uint32Array,\n nextMask: Uint32Array,\n): void {\n // Lazy-register any reactive queries (and their sources) so we can correctly\n // determine match transitions even if the user only ever called enterQuery/exitQuery.\n for (const q of moduleQueryCache.values()) {\n if (q.reactiveKind === 'normal') continue\n if (q.sourceQuery && state.queries[q.sourceQuery.id] !== q.sourceQuery) {\n ensureQueryRegistered(state, q.sourceQuery)\n }\n }\n\n const involved = state.bitToQueries.get(changedBit)\n if (!involved) return\n const words = state.options.maskWordCount\n for (const qid of involved) {\n const q = state.queries[qid]\n if (!q) continue\n if (q.reactiveKind !== 'normal') continue\n const bundle = state.queryMasks.get(qid)\n if (!bundle) continue\n const wasMatch = matches(\n prevMask,\n bundle.withMask,\n bundle.anyMask,\n bundle.noneMask,\n bundle.anyHasBits,\n words,\n )\n const isMatch = matches(\n nextMask,\n bundle.withMask,\n bundle.anyMask,\n bundle.noneMask,\n bundle.anyHasBits,\n words,\n )\n if (!wasMatch && isMatch) {\n pushReactive(state, qid, 'enter', eid)\n } else if (wasMatch && !isMatch) {\n pushReactive(state, qid, 'exit', eid)\n }\n }\n}\n\nfunction pushReactive(\n state: WorldState,\n queryId: number,\n kind: 'enter' | 'exit',\n eid: EntityId,\n): void {\n // Walk the module cache (not just state.queries) so reactive variants that\n // haven't been registered with this world yet still receive events.\n for (const r of moduleQueryCache.values()) {\n if (r.sourceQueryId !== queryId) continue\n if (r.reactiveKind !== kind) continue\n // Lazily register the reactive query in this world so subsequent reads can find it\n ensureQueryRegistered(state, r)\n const buf = ensureReactiveBuffer(state, r.id)\n if (kind === 'enter') buf.entered.push(eid as number)\n else buf.exited.push(eid as number)\n }\n}\n\nfunction ensureReactiveBuffer(state: WorldState, qid: number): ReactiveBuffer {\n let buf = state.reactiveBuffers.get(qid)\n if (!buf) {\n buf = { entered: [], exited: [] }\n state.reactiveBuffers.set(qid, buf)\n }\n return buf\n}\n\n// --- Helpers ---\n\nfunction getComponentInfoById(componentId: number): ComponentInfo {\n // We need access to the component registry's lookup by id. Re-import dynamically to avoid cycles.\n const info = lazyGetComponentInfo(componentId)\n if (!info) throw new Error(`aiecsjs: component id ${componentId} not registered`)\n return info\n}\n\nlet _getComponentInfoFn: ((id: number) => ComponentInfo | undefined) | null = null\nexport function registerComponentLookup(fn: (id: number) => ComponentInfo | undefined): void {\n _getComponentInfoFn = fn\n}\nfunction lazyGetComponentInfo(id: number): ComponentInfo | undefined {\n return _getComponentInfoFn ? _getComponentInfoFn(id) : undefined\n}\n\nexport function _resetQueryRegistry_FOR_TESTS_ONLY(): void {\n moduleQueryCache.clear()\n nextQueryId = 1\n}\n","// aiecsjs/observers — observe component add/remove/set events.\n\nimport { forEachSetBit, matchesEntityMask } from './internal/bitmask.js'\nimport { getComponentInfo, registerObserverDispatch } from './internal/component.js'\nimport { registerObserversAPI } from './internal/entity.js'\nimport { runQuery } from './internal/query.js'\nimport type {\n ComponentLike,\n EntityId,\n ObserverEntry,\n ObserverEvent,\n Query,\n QueryInternal,\n World,\n WorldState,\n} from './internal/types.js'\nimport { getOrRegisterComponentBit, getWorldState } from './internal/world.js'\n\n/**\n * Options accepted by every observer registration. `signal` aborts the\n * subscription when fired; the returned unsubscribe is still safe to call.\n */\nexport interface ObserverOptions {\n signal?: AbortSignal\n}\n\nfunction bindAbortSignal(unsubscribe: () => void, signal: AbortSignal | undefined): () => void {\n if (!signal) return unsubscribe\n if (signal.aborted) {\n unsubscribe()\n return () => {}\n }\n const onAbort = (): void => {\n unsubscribe()\n }\n signal.addEventListener('abort', onAbort, { once: true })\n let detached = false\n return () => {\n if (detached) return\n detached = true\n signal.removeEventListener('abort', onAbort)\n unsubscribe()\n }\n}\n\nexport function onAdd(\n world: World,\n component: ComponentLike,\n handler: (eid: EntityId) => void,\n opts?: ObserverOptions,\n): () => void {\n return bindAbortSignal(registerComponentObserver(world, component, 'add', handler), opts?.signal)\n}\n\nexport function onRemove(\n world: World,\n component: ComponentLike,\n handler: (eid: EntityId) => void,\n opts?: ObserverOptions,\n): () => void {\n return bindAbortSignal(\n registerComponentObserver(world, component, 'remove', handler),\n opts?.signal,\n )\n}\n\n/**\n * Low-level mutation hook. Fires after `setComponent(world, eid, comp, value)`\n * when the component is already present on the entity.\n *\n * Does NOT fire for:\n * - `addComponent` (use `onAdd` for that path; `addComponent + setComponent`\n * fires `onAdd` then `onSet`)\n * - Direct writes to a column view returned by `getComponent`. The column\n * array is the raw `TypedArray` / object — mutations bypass observer\n * dispatch.\n *\n * @example\n * // ❌ Anti-pattern: no `onSet` callback fires.\n * const col = getComponent(world, eid, Position) // raw column object\n * col.x[getEntityIndex(eid)] = 5\n *\n * // ✅ Correct: triggers `onSet`.\n * setComponent(world, eid, Position, { x: 5 })\n *\n * This is NOT a reactive value-predicate query — see `enterQuery` /\n * `exitQuery` for structural change tracking, and validate value predicates\n * in app code if you need them.\n */\nexport function onSet<C extends ComponentLike>(\n world: World,\n component: C,\n handler: (eid: EntityId, value: unknown) => void,\n opts?: ObserverOptions,\n): () => void {\n return bindAbortSignal(registerComponentObserver(world, component, 'set', handler), opts?.signal)\n}\n\nexport function observe(\n world: World,\n query: Query,\n event: ObserverEvent,\n handler: (eid: EntityId) => void,\n opts?: ObserverOptions,\n): () => void {\n const state = getWorldState(world)\n // Force registration of the query into this world so dispatch can find it.\n runQuery(world, query)\n const entry: ObserverEntry = {\n event,\n componentBit: -1,\n queryId: (query as QueryInternal).id,\n handler: handler as (eid: EntityId, value?: unknown) => void,\n }\n state.observers.push(entry)\n const unsubscribe = (): void => {\n const idx = state.observers.indexOf(entry)\n if (idx >= 0) state.observers.splice(idx, 1)\n }\n return bindAbortSignal(unsubscribe, opts?.signal)\n}\n\nfunction registerComponentObserver(\n world: World,\n component: ComponentLike,\n event: ObserverEvent,\n handler: (eid: EntityId, value?: unknown) => void,\n): () => void {\n const state = getWorldState(world)\n const info = getComponentInfo(component)\n let bit = state.componentBitFor.get(info.id)\n if (bit === undefined) {\n bit = getOrRegisterComponentBit(state, info)\n }\n const entry: ObserverEntry = { event, componentBit: bit, queryId: -1, handler }\n state.observers.push(entry)\n return () => {\n const idx = state.observers.indexOf(entry)\n if (idx >= 0) state.observers.splice(idx, 1)\n }\n}\n\n// --- Dispatch impls (wired into component.ts) ---\n//\n// CORRECTNESS: every dispatch loop snapshots `state.observers` via `Array.from`\n// before iterating. A handler may call its returned unsubscribe (which splices\n// `state.observers`), and mutating the backing array while iterating with a\n// for-of would skip the next sibling observer. The snapshot pins the visit\n// list; already-removed entries are filtered via `state.observers.includes`\n// so an in-flight unsubscribe also skips subsequent fires of the same dispatch.\n\nfunction fireAdd(state: WorldState, eid: EntityId, bit: number): void {\n const snapshot = Array.from(state.observers)\n for (const obs of snapshot) {\n if (obs.event !== 'add') continue\n if (obs.componentBit !== bit) continue\n if (!state.observers.includes(obs)) continue\n obs.handler(eid)\n }\n // Query-targeted observers: check if the entity's mask now matches the query\n // (uses the post-mutation mask, which the caller already wrote)\n dispatchQueryObservers(state, eid, 'add', /*prev*/ null, /*current*/ true)\n}\n\nfunction fireRemove(state: WorldState, eid: EntityId, bit: number): void {\n const snapshot = Array.from(state.observers)\n for (const obs of snapshot) {\n if (obs.event !== 'remove') continue\n if (obs.componentBit !== bit) continue\n if (!state.observers.includes(obs)) continue\n obs.handler(eid)\n }\n dispatchQueryObservers(state, eid, 'remove', /*prev*/ true, /*current*/ null)\n}\n\nfunction fireSet(state: WorldState, eid: EntityId, bit: number, value: unknown): void {\n const snapshot = Array.from(state.observers)\n for (const obs of snapshot) {\n if (obs.event !== 'set') continue\n if (!state.observers.includes(obs)) continue\n if (obs.componentBit === bit) obs.handler(eid, value)\n else if (obs.queryId !== -1) {\n const q = state.queries[obs.queryId]\n if (q?.all.includes(state.componentInfoByBit[bit]?.id ?? -1)) {\n obs.handler(eid, value)\n }\n }\n }\n}\n\n// Helper: when a component changes, walk query observers to see if their query\n// match status changed. Reads state.entityMask in place; no per-call allocation.\nfunction dispatchQueryObservers(\n state: WorldState,\n eid: EntityId,\n event: 'add' | 'remove',\n _prev: unknown,\n _current: unknown,\n): void {\n const w = state.options.maskWordCount\n const base = ((eid as number) & state.options.indexMask) * w\n const snapshot = Array.from(state.observers)\n for (const obs of snapshot) {\n if (obs.event !== event) continue\n if (obs.queryId === -1) continue\n if (!state.observers.includes(obs)) continue\n const bundle = state.queryMasks.get(obs.queryId)\n if (!bundle) continue\n const isMatch = matchesEntityMask(\n state.entityMask,\n base,\n w,\n bundle.withMask,\n bundle.anyMask,\n bundle.noneMask,\n bundle.anyHasBits,\n )\n // For 'add' event, fire if newly matched. For 'remove', fire if newly unmatched.\n // We approximate \"newly\" by always firing on event when match status agrees;\n // duplicates are acceptable for 0.1 query observers.\n if (event === 'add' && isMatch) obs.handler(eid)\n if (event === 'remove' && !isMatch) obs.handler(eid)\n }\n}\n\n// Wire the dispatch into component.ts\nregisterObserverDispatch({ fireAdd, fireRemove, fireSet })\n\n// Register destroy hook so onRemove fires for every component on destroy\n// AND so query-targeted observers see the entity exit any matched query.\nregisterObserversAPI({\n dispatchDestroyObservers(state: WorldState, eid: EntityId): void {\n const w = state.options.maskWordCount\n const base = ((eid as number) & state.options.indexMask) * w\n\n // Snapshot the pre-destroy mask so Phase 2's `wasMatch` is computed\n // against the state at destroy entry — Phase 1 handlers might reentrant-\n // mutate `state.entityMask` (e.g. by calling removeComponent on a\n // sibling), and we still want query observers to fire for queries the\n // entity was matching *before* the destroy began.\n const preMask = new Uint32Array(w)\n for (let i = 0; i < w; i++) preMask[i] = state.entityMask[base + i] ?? 0\n\n // Phase 1: component-level remove for every bit set at destroy entry.\n forEachSetBit(preMask, 0, w, (bit) => {\n const snapshot = Array.from(state.observers)\n for (const obs of snapshot) {\n if (obs.event !== 'remove') continue\n if (obs.componentBit !== bit) continue\n if (!state.observers.includes(obs)) continue\n obs.handler(eid)\n }\n })\n\n // Phase 2: query-level remove for any query that was matching this entity.\n // Read the pre-destroy mask snapshot (not live state.entityMask) so this\n // phase is decoupled from Phase 1 reentrant mutations.\n const querySnapshot = Array.from(state.observers)\n for (const obs of querySnapshot) {\n if (obs.event !== 'remove') continue\n if (obs.queryId === -1) continue\n if (!state.observers.includes(obs)) continue\n const bundle = state.queryMasks.get(obs.queryId)\n if (!bundle) continue\n const wasMatch = matchesEntityMask(\n preMask,\n 0,\n w,\n bundle.withMask,\n bundle.anyMask,\n bundle.noneMask,\n bundle.anyHasBits,\n )\n if (wasMatch) obs.handler(eid)\n }\n },\n})\n"]}
@@ -1,2 +1,2 @@
1
- 'use strict';var f=new Map;function d(o){let s=f.get(o.id);if(!s)throw new Error(`aiecsjs: world ${o.id} is destroyed or unknown`);if(s.destroyed)throw new Error(`aiecsjs: world ${o.id} is destroyed`);return s}var m=1;function g(o){return {__kind:"relation",__id:m++,__exclusive:o?.exclusive??false,__hasData:false}}var W=g({exclusive:true});function x(o,s){let r=o.relationStorage.get(s.__id);return r||(r={rel:s,exclusive:s.__exclusive?new Int32Array(o.capacity).fill(-1):null,outgoing:new Map,data:new Map},o.relationStorage.set(s.__id,r),r)}function I(o,s,r,a,n){let t=d(o);if(t.readOnly)throw new Error("aiecsjs: cannot mutate a read-only world");let e=x(t,r),c=s,l=a;if(e.exclusive){if(c>=e.exclusive.length){let i=new Int32Array(Math.max(c+1,e.exclusive.length*2)).fill(-1);i.set(e.exclusive),e.exclusive=i;}e.exclusive[c]=l;}else {let i=e.outgoing.get(c);i||(i=[],e.outgoing.set(c,i)),i.includes(l)||i.push(l);}if(n!==void 0){let i=e.data.get(c);i||(i=new Map,e.data.set(c,i)),i.set(l,n);}}function E(o,s,r,a){let n=d(o);if(n.readOnly)throw new Error("aiecsjs: cannot mutate a read-only world");let t=n.relationStorage.get(r.__id);if(!t)return;let e=s,c=a;if(t.exclusive)e<t.exclusive.length&&t.exclusive[e]===c&&(t.exclusive[e]=-1);else {let i=t.outgoing.get(e);if(i){let u=i.indexOf(c);u>=0&&i.splice(u,1),i.length===0&&t.outgoing.delete(e);}}let l=t.data.get(e);l&&(l.delete(c),l.size===0&&t.data.delete(e));}function A(o,s,r){let n=d(o).relationStorage.get(r.__id);if(!n)return [];let t=s;if(n.exclusive){if(t<n.exclusive.length){let c=n.exclusive[t]??-1;return c>=0?[c]:[]}return []}let e=n.outgoing.get(t);return e||[]}exports.ChildOf=W;exports.addRelation=I;exports.defineRelation=g;exports.getRelationTargets=A;exports.removeRelation=E;//# sourceMappingURL=relations.cjs.map
1
+ 'use strict';var f=new Map;function l(o){let a=f.get(o.id);if(!a)throw new Error(`aiecsjs: world ${o.id} is destroyed or unknown`);if(a.destroyed)throw new Error(`aiecsjs: world ${o.id} is destroyed`);return a}var g=1;function h(o){return {__kind:"relation",__id:g++,__exclusive:o?.exclusive??false,__hasData:false}}var A=h({exclusive:true});function b(o,a){let r=o.relationStorage.get(a.__id);return r||(r={rel:a,exclusive:a.__exclusive?new Int32Array(o.capacity).fill(-1):null,outgoing:new Map,data:new Map},o.relationStorage.set(a.__id,r),r)}function M(o,a,r,i,n){let e=l(o);if(e.readOnly)throw new Error("aiecsjs: cannot mutate a read-only world");let t=b(e,r),c=a&e.options.indexMask,d=i&e.options.indexMask;if(t.exclusive){if(c>=t.exclusive.length){let s=new Int32Array(Math.max(c+1,t.exclusive.length*2)).fill(-1);s.set(t.exclusive),t.exclusive=s;}t.exclusive[c]=d;}else {let s=t.outgoing.get(c);s||(s=[],t.outgoing.set(c,s)),s.includes(d)||s.push(d);}if(n!==void 0){let s=t.data.get(c);s||(s=new Map,t.data.set(c,s)),s.set(d,n);}}function B(o,a,r,i){let n=l(o);if(n.readOnly)throw new Error("aiecsjs: cannot mutate a read-only world");let e=n.relationStorage.get(r.__id);if(!e)return;let t=a&n.options.indexMask,c=i&n.options.indexMask;if(e.exclusive)t<e.exclusive.length&&e.exclusive[t]===c&&(e.exclusive[t]=-1);else {let s=e.outgoing.get(t);if(s){let u=s.indexOf(c);u>=0&&s.splice(u,1),s.length===0&&e.outgoing.delete(t);}}let d=e.data.get(t);d&&(d.delete(c),d.size===0&&e.data.delete(t));}function S(o,a,r){let i=l(o),n=i.relationStorage.get(r.__id);if(!n)return [];let e=a&i.options.indexMask;if(n.exclusive){if(e<n.exclusive.length){let c=n.exclusive[e]??-1;return c>=0?[c]:[]}return []}let t=n.outgoing.get(e);return t||[]}exports.ChildOf=A;exports.addRelation=M;exports.defineRelation=h;exports.getRelationTargets=S;exports.removeRelation=B;//# sourceMappingURL=relations.cjs.map
2
2
  //# sourceMappingURL=relations.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/internal/world.ts","../src/relations.ts"],"names":["worldRegistry","getWorldState","world","state","nextRelationId","defineRelation","options","ChildOf","getOrCreateStorage","rel","storage","addRelation","source","target","data","src","tgt","next","list","inner","removeRelation","idx","getRelationTargets"],"mappings":"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","file":"relations.cjs","sourcesContent":["import { VERSION } from '../version.js'\nimport { cloneMask, copyMask, createMask, maskHash } from './bitmask.js'\nimport type {\n ArchetypeState,\n ComponentInfo,\n FieldInfo,\n ResolvedWorldOptions,\n World,\n WorldComponentStorage,\n WorldOptions,\n WorldState,\n} from './types.js'\n\nexport const WORLD_BRAND = Symbol.for('aiecsjs.world')\n\nconst DEFAULT_OPTIONS: Required<Omit<WorldOptions, 'buffer' | 'bufferByteOffset'>> = {\n initialCapacity: 1024,\n maxEntities: 1_000_000,\n indexBits: 24,\n generationBits: 8,\n}\n\nconst DEFAULT_MAX_COMPONENTS = 256\n\nlet nextWorldId = 1\nconst worldRegistry = new Map<number, WorldState>()\n\nexport function getWorldState(world: World): WorldState {\n const state = worldRegistry.get(world.id)\n if (!state) throw new Error(`aiecsjs: world ${world.id} is destroyed or unknown`)\n if (state.destroyed) throw new Error(`aiecsjs: world ${world.id} is destroyed`)\n return state\n}\n\nexport function registerWorld(state: WorldState): void {\n worldRegistry.set(state.id, state)\n}\n\nexport function unregisterWorld(id: number): void {\n worldRegistry.delete(id)\n}\n\nexport function isWorldRegistered(id: number): boolean {\n return worldRegistry.has(id)\n}\n\nfunction resolveOptions(opts: WorldOptions | undefined): ResolvedWorldOptions {\n const o = opts ?? {}\n const indexBits = o.indexBits ?? DEFAULT_OPTIONS.indexBits\n const generationBits = o.generationBits ?? DEFAULT_OPTIONS.generationBits\n const maxComponents = DEFAULT_MAX_COMPONENTS\n if (indexBits < 1 || indexBits > 24) {\n throw new Error('aiecsjs: indexBits must be in [1, 24]')\n }\n if (generationBits < 0 || generationBits > 16) {\n throw new Error('aiecsjs: generationBits must be in [0, 16]')\n }\n const maxByIndex = 1 << indexBits\n const initialCapacity = Math.max(\n 1,\n Math.min(o.initialCapacity ?? DEFAULT_OPTIONS.initialCapacity, maxByIndex),\n )\n const maxEntities = Math.max(\n initialCapacity,\n Math.min(o.maxEntities ?? DEFAULT_OPTIONS.maxEntities, maxByIndex),\n )\n return {\n initialCapacity,\n maxEntities,\n indexBits,\n generationBits,\n maxComponents,\n maskWordCount: Math.ceil(maxComponents / 32),\n buffer: o.buffer ?? null,\n bufferByteOffset: o.bufferByteOffset ?? 0,\n }\n}\n\nexport function createEmptyArchetype(maskWordCount: number, maxComponents: number): ArchetypeState {\n return {\n id: 0,\n mask: createMask(maskWordCount),\n size: 0,\n capacity: 16,\n entities: new Uint32Array(16),\n entityRow: new Map<number, number>(),\n componentBits: [],\n edgeAdd: new Int32Array(maxComponents).fill(-1),\n edgeRemove: new Int32Array(maxComponents).fill(-1),\n }\n}\n\nexport function createWorld(options?: WorldOptions): World {\n const resolved = resolveOptions(options)\n const id = nextWorldId++\n\n const generationCtor = resolved.generationBits > 8 ? Uint16Array : Uint8Array\n const generations = new generationCtor(resolved.initialCapacity)\n\n const state: WorldState = {\n id,\n capacity: resolved.initialCapacity,\n version: VERSION,\n options: resolved,\n size: 0,\n nextFreshIndex: 1, // 0 reserved\n freeList: [],\n generations,\n destroyed: false,\n componentBitFor: new Map<number, number>(),\n componentInfoByBit: new Array(resolved.maxComponents).fill(null),\n componentStorageByBit: new Array(resolved.maxComponents).fill(null),\n nextComponentBit: 0,\n entityArchetype: new Uint32Array(resolved.initialCapacity),\n entityMask: new Uint32Array(resolved.initialCapacity * resolved.maskWordCount),\n archetypes: [],\n archetypeByMaskHash: new Map<string, number>(),\n queryVersion: 0,\n queries: [],\n queryMasks: new Map(),\n queryArchetypeCache: [],\n queryArchetypeStamp: [],\n bitToQueries: new Map<number, Set<number>>(),\n reactiveBuffers: new Map(),\n observers: [],\n relationStorage: new Map(),\n sab: resolved.buffer,\n readOnly: false,\n }\n\n // Seed archetype 0 (the empty mask)\n const empty = createEmptyArchetype(resolved.maskWordCount, resolved.maxComponents)\n empty.id = 0\n state.archetypes.push(empty)\n state.archetypeByMaskHash.set(maskHash(empty.mask), 0)\n\n registerWorld(state)\n return makePublicWorld(state)\n}\n\nexport function makePublicWorld(state: WorldState): World {\n return {\n id: state.id,\n get capacity() {\n return state.capacity\n },\n version: state.version,\n } as World\n}\n\nexport function destroyWorld(world: World): void {\n const state = worldRegistry.get(world.id)\n if (!state || state.destroyed) return\n state.destroyed = true\n // Clear large buffers to help GC\n state.archetypes = []\n state.archetypeByMaskHash.clear()\n state.componentInfoByBit = []\n state.componentStorageByBit = []\n state.queries = []\n state.queryMasks.clear()\n state.queryArchetypeCache = []\n state.observers = []\n state.relationStorage.clear()\n state.reactiveBuffers.clear()\n unregisterWorld(world.id)\n}\n\nexport function resetWorld(world: World): void {\n const state = getWorldState(world)\n // Keep capacity and registered components; clear entities and per-entity state.\n state.size = 0\n state.nextFreshIndex = 1\n state.freeList = []\n state.generations.fill(0)\n state.entityArchetype.fill(0)\n state.entityMask.fill(0)\n // Wipe archetype memberships but keep archetype graph for stability.\n for (const arch of state.archetypes) {\n arch.size = 0\n arch.entityRow.clear()\n }\n // Clear SoA columns and AoS instances; storage allocation is preserved.\n for (const storage of state.componentStorageByBit) {\n if (!storage) continue\n if (storage.kind === 'soa' && storage.soa) {\n for (const k of Object.keys(storage.soa)) {\n storage.soa[k]?.fill(0)\n }\n } else if (storage.kind === 'aos' && storage.aos) {\n storage.aos.fill(undefined as unknown as never)\n }\n }\n state.queryVersion++\n for (const buf of state.reactiveBuffers.values()) {\n buf.entered.length = 0\n buf.exited.length = 0\n }\n}\n\nexport function getWorldSize(world: World): number {\n return getWorldState(world).size\n}\n\nexport function getWorldCapacity(world: World): number {\n return getWorldState(world).capacity\n}\n\n// --- Capacity growth ---\n\nexport function ensureCapacity(state: WorldState, needed: number): void {\n if (needed <= state.capacity) return\n if (needed > state.options.maxEntities) {\n throw new Error(\n `aiecsjs: requested capacity ${needed} exceeds maxEntities ${state.options.maxEntities}`,\n )\n }\n let newCap = state.capacity\n while (newCap < needed) newCap = Math.min(newCap * 2, state.options.maxEntities)\n growEntityArrays(state, newCap)\n}\n\nfunction growEntityArrays(state: WorldState, newCap: number): void {\n // generations\n const genCtor: any = state.generations.constructor\n const newGen = new genCtor(newCap) as Uint8Array | Uint16Array\n ;(newGen as Uint8Array).set(state.generations as Uint8Array)\n state.generations = newGen\n\n // entityArchetype\n const newArch = new Uint32Array(newCap)\n newArch.set(state.entityArchetype)\n state.entityArchetype = newArch\n\n // entityMask\n const wordCount = state.options.maskWordCount\n const newMask = new Uint32Array(newCap * wordCount)\n newMask.set(state.entityMask)\n state.entityMask = newMask\n\n // Component storages (SoA columns + AoS arrays)\n for (const storage of state.componentStorageByBit) {\n if (!storage) continue\n if (storage.kind === 'soa' && storage.soa) {\n const info = state.componentInfoByBit[storage.bit]\n if (info) growSoAColumns(storage.soa, info.fields, newCap)\n } else if (storage.kind === 'aos' && storage.aos) {\n storage.aos.length = newCap\n }\n }\n\n state.capacity = newCap\n}\n\nfunction growSoAColumns(soa: Record<string, any>, fields: FieldInfo[], newCap: number): void {\n for (const f of fields) {\n const old = soa[f.name]\n if (!old) continue\n const newLen = newCap * f.vectorLen\n if (old.length >= newLen) continue\n const next = new f.ctor(newLen)\n next.set(old)\n soa[f.name] = next\n }\n}\n\n// --- Archetype management ---\n\nexport function findOrCreateArchetype(\n state: WorldState,\n mask: Uint32Array,\n): { archId: number; created: boolean } {\n const key = maskHash(mask)\n const existing = state.archetypeByMaskHash.get(key)\n if (existing !== undefined) return { archId: existing, created: false }\n\n const id = state.archetypes.length\n const arch: ArchetypeState = {\n id,\n mask: cloneMask(mask),\n size: 0,\n capacity: 16,\n entities: new Uint32Array(16),\n entityRow: new Map<number, number>(),\n componentBits: collectBits(mask),\n edgeAdd: new Int32Array(state.options.maxComponents).fill(-1),\n edgeRemove: new Int32Array(state.options.maxComponents).fill(-1),\n }\n state.archetypes.push(arch)\n state.archetypeByMaskHash.set(key, id)\n state.queryVersion++\n return { archId: id, created: true }\n}\n\nexport function ensureArchetypeCapacity(arch: ArchetypeState, needed: number): void {\n if (needed <= arch.capacity) return\n let newCap = arch.capacity\n while (newCap < needed) newCap *= 2\n const next = new Uint32Array(newCap)\n next.set(arch.entities)\n arch.entities = next\n arch.capacity = newCap\n}\n\nfunction collectBits(mask: Uint32Array): number[] {\n const bits: number[] = []\n for (let w = 0; w < mask.length; w++) {\n let word = mask[w] ?? 0\n while (word !== 0) {\n const lsb = word & -word\n const bit = (w << 5) + (31 - Math.clz32(lsb))\n bits.push(bit)\n word &= word - 1\n }\n }\n return bits\n}\n\n// --- Component registration in a world ---\n\nexport function getOrRegisterComponentBit(state: WorldState, info: ComponentInfo): number {\n const existing = state.componentBitFor.get(info.id)\n if (existing !== undefined) return existing\n\n if (state.nextComponentBit >= state.options.maxComponents) {\n throw new Error(`aiecsjs: world reached maxComponents=${state.options.maxComponents}`)\n }\n const bit = state.nextComponentBit++\n state.componentBitFor.set(info.id, bit)\n state.componentInfoByBit[bit] = info\n\n // Create storage\n let storage: WorldComponentStorage\n if (info.kind === 'soa') {\n const soa: Record<string, any> = {}\n for (const f of info.fields) {\n soa[f.name] = new f.ctor(state.capacity * f.vectorLen)\n }\n storage = { kind: 'soa', componentId: info.id, bit, soa }\n } else if (info.kind === 'aos') {\n storage = { kind: 'aos', componentId: info.id, bit, aos: new Array(state.capacity) }\n } else {\n storage = { kind: 'tag', componentId: info.id, bit }\n }\n state.componentStorageByBit[bit] = storage\n return bit\n}\n\n// Read a bitmask out of state.entityMask[] into a fresh Uint32Array.\nexport function readEntityMask(state: WorldState, eid: number): Uint32Array {\n const w = state.options.maskWordCount\n const out = new Uint32Array(w)\n const base = eid * w\n for (let i = 0; i < w; i++) out[i] = state.entityMask[base + i] ?? 0\n return out\n}\n\nexport function writeEntityMask(state: WorldState, eid: number, mask: Uint32Array): void {\n const w = state.options.maskWordCount\n const base = eid * w\n for (let i = 0; i < w; i++) state.entityMask[base + i] = mask[i] ?? 0\n}\n\n// Get the bit of a component within a world without registering.\nexport function tryGetComponentBit(state: WorldState, info: ComponentInfo): number | undefined {\n return state.componentBitFor.get(info.id)\n}\n\nexport function isWorld(x: unknown): x is World {\n if (!x || typeof x !== 'object') return false\n const obj = x as { id?: unknown; version?: unknown }\n if (typeof obj.id !== 'number') return false\n if (typeof obj.version !== 'string') return false\n return worldRegistry.has(obj.id)\n}\n","// aiecsjs/relations — entity-to-entity relations (experimental in 0.1).\n\nimport { registerRelationsCleanup } from './internal/entity.js'\nimport type { EntityId, Relation, RelationStorage, World, WorldState } from './internal/types.js'\nimport { getWorldState } from './internal/world.js'\n\nlet nextRelationId = 1\n\nexport function defineRelation<T = void>(options?: { exclusive?: boolean }): Relation<T> {\n const id = nextRelationId++\n return {\n __kind: 'relation',\n __id: id,\n __exclusive: options?.exclusive ?? false,\n __hasData: false,\n } as Relation<T>\n}\n\nexport const ChildOf: Relation = defineRelation({ exclusive: true })\n\nfunction getOrCreateStorage(state: WorldState, rel: Relation<any>): RelationStorage {\n let storage = state.relationStorage.get(rel.__id)\n if (storage) return storage\n storage = {\n rel,\n exclusive: rel.__exclusive ? new Int32Array(state.capacity).fill(-1) : null,\n outgoing: new Map<number, number[]>(),\n data: new Map<number, Map<number, unknown>>(),\n }\n state.relationStorage.set(rel.__id, storage)\n return storage\n}\n\nexport function addRelation<T>(\n world: World,\n source: EntityId,\n rel: Relation<T>,\n target: EntityId,\n data?: T,\n): void {\n const state = getWorldState(world)\n if (state.readOnly) throw new Error('aiecsjs: cannot mutate a read-only world')\n const storage = getOrCreateStorage(state, rel as Relation<any>)\n const src = source as number\n const tgt = target as number\n\n if (storage.exclusive) {\n if (src >= storage.exclusive.length) {\n const next = new Int32Array(Math.max(src + 1, storage.exclusive.length * 2)).fill(-1)\n next.set(storage.exclusive)\n storage.exclusive = next\n }\n storage.exclusive[src] = tgt\n } else {\n let list = storage.outgoing.get(src)\n if (!list) {\n list = []\n storage.outgoing.set(src, list)\n }\n if (!list.includes(tgt)) list.push(tgt)\n }\n\n if (data !== undefined) {\n let inner = storage.data.get(src)\n if (!inner) {\n inner = new Map<number, unknown>()\n storage.data.set(src, inner)\n }\n inner.set(tgt, data)\n }\n}\n\nexport function removeRelation(\n world: World,\n source: EntityId,\n rel: Relation,\n target: EntityId,\n): void {\n const state = getWorldState(world)\n if (state.readOnly) throw new Error('aiecsjs: cannot mutate a read-only world')\n const storage = state.relationStorage.get(rel.__id)\n if (!storage) return\n const src = source as number\n const tgt = target as number\n if (storage.exclusive) {\n if (src < storage.exclusive.length && storage.exclusive[src] === tgt) {\n storage.exclusive[src] = -1\n }\n } else {\n const list = storage.outgoing.get(src)\n if (list) {\n const idx = list.indexOf(tgt)\n if (idx >= 0) list.splice(idx, 1)\n if (list.length === 0) storage.outgoing.delete(src)\n }\n }\n const inner = storage.data.get(src)\n if (inner) {\n inner.delete(tgt)\n if (inner.size === 0) storage.data.delete(src)\n }\n}\n\nexport function getRelationTargets(\n world: World,\n source: EntityId,\n rel: Relation,\n): readonly EntityId[] {\n const state = getWorldState(world)\n const storage = state.relationStorage.get(rel.__id)\n if (!storage) return []\n const src = source as number\n if (storage.exclusive) {\n if (src < storage.exclusive.length) {\n const tgt = storage.exclusive[src] ?? -1\n return tgt >= 0 ? [tgt as EntityId] : []\n }\n return []\n }\n const list = storage.outgoing.get(src)\n if (!list) return []\n return list as EntityId[]\n}\n\n// Cleanup hook: when an entity is destroyed, remove all relations involving it.\nregisterRelationsCleanup((state: WorldState, eid: EntityId) => {\n const e = eid as number\n for (const storage of state.relationStorage.values()) {\n if (storage.exclusive) {\n if (e < storage.exclusive.length) storage.exclusive[e] = -1\n // Also clear any references TO this entity as target\n for (let i = 0; i < storage.exclusive.length; i++) {\n if (storage.exclusive[i] === e) storage.exclusive[i] = -1\n }\n }\n storage.outgoing.delete(e)\n for (const [src, list] of storage.outgoing) {\n const idx = list.indexOf(e)\n if (idx >= 0) {\n list.splice(idx, 1)\n if (list.length === 0) storage.outgoing.delete(src)\n }\n }\n // Clear data entries involving this entity. The outer key is the source\n // eid; entries whose source IS this entity drop entirely. Entries that\n // reference this entity as a target are pruned from each inner map.\n storage.data.delete(e)\n for (const [src, inner] of storage.data) {\n inner.delete(e)\n if (inner.size === 0) storage.data.delete(src)\n }\n }\n})\n"]}
1
+ {"version":3,"sources":["../src/internal/world.ts","../src/relations.ts"],"names":["worldRegistry","getWorldState","world","state","nextRelationId","defineRelation","options","ChildOf","getOrCreateStorage","rel","storage","addRelation","source","target","data","src","tgt","next","list","inner","removeRelation","idx","getRelationTargets"],"mappings":"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","file":"relations.cjs","sourcesContent":["import { VERSION } from '../version.js'\nimport { cloneMask, copyMask, createMask, maskHash } from './bitmask.js'\nimport type {\n ArchetypeState,\n ComponentInfo,\n FieldInfo,\n ResolvedWorldOptions,\n World,\n WorldComponentStorage,\n WorldOptions,\n WorldState,\n} from './types.js'\n\nexport const WORLD_BRAND = Symbol.for('aiecsjs.world')\n\nconst DEFAULT_OPTIONS: Required<Omit<WorldOptions, 'buffer' | 'bufferByteOffset'>> = {\n initialCapacity: 1024,\n maxEntities: 1_000_000,\n indexBits: 24,\n generationBits: 8,\n}\n\nconst DEFAULT_MAX_COMPONENTS = 256\n\nlet nextWorldId = 1\nconst worldRegistry = new Map<number, WorldState>()\n\nexport function getWorldState(world: World): WorldState {\n const state = worldRegistry.get(world.id)\n if (!state) throw new Error(`aiecsjs: world ${world.id} is destroyed or unknown`)\n if (state.destroyed) throw new Error(`aiecsjs: world ${world.id} is destroyed`)\n return state\n}\n\nexport function registerWorld(state: WorldState): void {\n worldRegistry.set(state.id, state)\n}\n\nexport function unregisterWorld(id: number): void {\n worldRegistry.delete(id)\n}\n\nexport function isWorldRegistered(id: number): boolean {\n return worldRegistry.has(id)\n}\n\nfunction resolveOptions(opts: WorldOptions | undefined): ResolvedWorldOptions {\n const o = opts ?? {}\n const indexBits = o.indexBits ?? DEFAULT_OPTIONS.indexBits\n const generationBits = o.generationBits ?? DEFAULT_OPTIONS.generationBits\n const maxComponents = DEFAULT_MAX_COMPONENTS\n if (indexBits < 1 || indexBits > 24) {\n throw new Error('aiecsjs: indexBits must be in [1, 24]')\n }\n if (generationBits < 0 || generationBits > 16) {\n throw new Error('aiecsjs: generationBits must be in [0, 16]')\n }\n const maxByIndex = 1 << indexBits\n const initialCapacity = Math.max(\n 1,\n Math.min(o.initialCapacity ?? DEFAULT_OPTIONS.initialCapacity, maxByIndex),\n )\n const maxEntities = Math.max(\n initialCapacity,\n Math.min(o.maxEntities ?? DEFAULT_OPTIONS.maxEntities, maxByIndex),\n )\n return {\n initialCapacity,\n maxEntities,\n indexBits,\n generationBits,\n indexMask: (1 << indexBits) - 1,\n generationMask: generationBits === 0 ? 0 : (1 << generationBits) - 1,\n maxComponents,\n maskWordCount: Math.ceil(maxComponents / 32),\n buffer: o.buffer ?? null,\n bufferByteOffset: o.bufferByteOffset ?? 0,\n }\n}\n\nexport function createEmptyArchetype(maskWordCount: number, maxComponents: number): ArchetypeState {\n return {\n id: 0,\n mask: createMask(maskWordCount),\n size: 0,\n capacity: 16,\n entities: new Uint32Array(16),\n entityRow: new Map<number, number>(),\n componentBits: [],\n edgeAdd: new Int32Array(maxComponents).fill(-1),\n edgeRemove: new Int32Array(maxComponents).fill(-1),\n }\n}\n\nexport function createWorld(options?: WorldOptions): World {\n const resolved = resolveOptions(options)\n const id = nextWorldId++\n\n const generationCtor = resolved.generationBits > 8 ? Uint16Array : Uint8Array\n const generations = new generationCtor(resolved.initialCapacity)\n\n const state: WorldState = {\n id,\n capacity: resolved.initialCapacity,\n version: VERSION,\n options: resolved,\n size: 0,\n nextFreshIndex: 1, // 0 reserved\n freeList: [],\n generations,\n destroyed: false,\n componentBitFor: new Map<number, number>(),\n componentInfoByBit: new Array(resolved.maxComponents).fill(null),\n componentStorageByBit: new Array(resolved.maxComponents).fill(null),\n nextComponentBit: 0,\n entityArchetype: new Uint32Array(resolved.initialCapacity),\n entityMask: new Uint32Array(resolved.initialCapacity * resolved.maskWordCount),\n archetypes: [],\n archetypeByMaskHash: new Map<string, number>(),\n queryVersion: 0,\n queries: [],\n queryMasks: new Map(),\n queryArchetypeCache: [],\n queryArchetypeStamp: [],\n bitToQueries: new Map<number, Set<number>>(),\n reactiveBuffers: new Map(),\n observers: [],\n relationStorage: new Map(),\n sab: resolved.buffer,\n readOnly: false,\n }\n\n // Seed archetype 0 (the empty mask)\n const empty = createEmptyArchetype(resolved.maskWordCount, resolved.maxComponents)\n empty.id = 0\n state.archetypes.push(empty)\n state.archetypeByMaskHash.set(maskHash(empty.mask), 0)\n\n registerWorld(state)\n return makePublicWorld(state)\n}\n\nexport function makePublicWorld(state: WorldState): World {\n return {\n id: state.id,\n get capacity() {\n return state.capacity\n },\n version: state.version,\n } as World\n}\n\nexport function destroyWorld(world: World): void {\n const state = worldRegistry.get(world.id)\n if (!state || state.destroyed) return\n state.destroyed = true\n // Clear large buffers to help GC\n state.archetypes = []\n state.archetypeByMaskHash.clear()\n state.componentInfoByBit = []\n state.componentStorageByBit = []\n state.queries = []\n state.queryMasks.clear()\n state.queryArchetypeCache = []\n state.observers = []\n state.relationStorage.clear()\n state.reactiveBuffers.clear()\n unregisterWorld(world.id)\n}\n\nexport function resetWorld(world: World): void {\n const state = getWorldState(world)\n // Keep capacity and registered components; clear entities and per-entity state.\n state.size = 0\n state.nextFreshIndex = 1\n state.freeList = []\n state.generations.fill(0)\n state.entityArchetype.fill(0)\n state.entityMask.fill(0)\n // Wipe archetype memberships but keep archetype graph for stability.\n for (const arch of state.archetypes) {\n arch.size = 0\n arch.entityRow.clear()\n }\n // Clear SoA columns and AoS instances; storage allocation is preserved.\n for (const storage of state.componentStorageByBit) {\n if (!storage) continue\n if (storage.kind === 'soa' && storage.soa) {\n for (const k of Object.keys(storage.soa)) {\n storage.soa[k]?.fill(0)\n }\n } else if (storage.kind === 'aos' && storage.aos) {\n storage.aos.fill(undefined as unknown as never)\n }\n }\n state.queryVersion++\n for (const buf of state.reactiveBuffers.values()) {\n buf.entered.length = 0\n buf.exited.length = 0\n }\n}\n\nexport function getWorldSize(world: World): number {\n return getWorldState(world).size\n}\n\nexport function getWorldCapacity(world: World): number {\n return getWorldState(world).capacity\n}\n\n// --- Capacity growth ---\n\nexport function ensureCapacity(state: WorldState, needed: number): void {\n if (needed <= state.capacity) return\n if (needed > state.options.maxEntities) {\n throw new Error(\n `aiecsjs: requested capacity ${needed} exceeds maxEntities ${state.options.maxEntities}`,\n )\n }\n let newCap = state.capacity\n while (newCap < needed) newCap = Math.min(newCap * 2, state.options.maxEntities)\n growEntityArrays(state, newCap)\n}\n\nfunction growEntityArrays(state: WorldState, newCap: number): void {\n // generations\n const genCtor: any = state.generations.constructor\n const newGen = new genCtor(newCap) as Uint8Array | Uint16Array\n ;(newGen as Uint8Array).set(state.generations as Uint8Array)\n state.generations = newGen\n\n // entityArchetype\n const newArch = new Uint32Array(newCap)\n newArch.set(state.entityArchetype)\n state.entityArchetype = newArch\n\n // entityMask\n const wordCount = state.options.maskWordCount\n const newMask = new Uint32Array(newCap * wordCount)\n newMask.set(state.entityMask)\n state.entityMask = newMask\n\n // Component storages (SoA columns + AoS arrays)\n for (const storage of state.componentStorageByBit) {\n if (!storage) continue\n if (storage.kind === 'soa' && storage.soa) {\n const info = state.componentInfoByBit[storage.bit]\n if (info) growSoAColumns(storage.soa, info.fields, newCap)\n } else if (storage.kind === 'aos' && storage.aos) {\n storage.aos.length = newCap\n }\n }\n\n state.capacity = newCap\n}\n\nfunction growSoAColumns(soa: Record<string, any>, fields: FieldInfo[], newCap: number): void {\n for (const f of fields) {\n const old = soa[f.name]\n if (!old) continue\n const newLen = newCap * f.vectorLen\n if (old.length >= newLen) continue\n const next = new f.ctor(newLen)\n next.set(old)\n soa[f.name] = next\n }\n}\n\n// --- Archetype management ---\n\nexport function findOrCreateArchetype(\n state: WorldState,\n mask: Uint32Array,\n): { archId: number; created: boolean } {\n const key = maskHash(mask)\n const existing = state.archetypeByMaskHash.get(key)\n if (existing !== undefined) return { archId: existing, created: false }\n\n const id = state.archetypes.length\n const arch: ArchetypeState = {\n id,\n mask: cloneMask(mask),\n size: 0,\n capacity: 16,\n entities: new Uint32Array(16),\n entityRow: new Map<number, number>(),\n componentBits: collectBits(mask),\n edgeAdd: new Int32Array(state.options.maxComponents).fill(-1),\n edgeRemove: new Int32Array(state.options.maxComponents).fill(-1),\n }\n state.archetypes.push(arch)\n state.archetypeByMaskHash.set(key, id)\n state.queryVersion++\n return { archId: id, created: true }\n}\n\nexport function ensureArchetypeCapacity(arch: ArchetypeState, needed: number): void {\n if (needed <= arch.capacity) return\n let newCap = arch.capacity\n while (newCap < needed) newCap *= 2\n const next = new Uint32Array(newCap)\n next.set(arch.entities)\n arch.entities = next\n arch.capacity = newCap\n}\n\nfunction collectBits(mask: Uint32Array): number[] {\n const bits: number[] = []\n for (let w = 0; w < mask.length; w++) {\n let word = mask[w] ?? 0\n while (word !== 0) {\n const lsb = word & -word\n const bit = (w << 5) + (31 - Math.clz32(lsb))\n bits.push(bit)\n word &= word - 1\n }\n }\n return bits\n}\n\n// --- Component registration in a world ---\n\nexport function getOrRegisterComponentBit(state: WorldState, info: ComponentInfo): number {\n const existing = state.componentBitFor.get(info.id)\n if (existing !== undefined) return existing\n\n if (state.nextComponentBit >= state.options.maxComponents) {\n throw new Error(`aiecsjs: world reached maxComponents=${state.options.maxComponents}`)\n }\n const bit = state.nextComponentBit++\n state.componentBitFor.set(info.id, bit)\n state.componentInfoByBit[bit] = info\n\n // Create storage\n let storage: WorldComponentStorage\n if (info.kind === 'soa') {\n const soa: Record<string, any> = {}\n for (const f of info.fields) {\n soa[f.name] = new f.ctor(state.capacity * f.vectorLen)\n }\n storage = { kind: 'soa', componentId: info.id, bit, soa }\n } else if (info.kind === 'aos') {\n storage = { kind: 'aos', componentId: info.id, bit, aos: new Array(state.capacity) }\n } else {\n storage = { kind: 'tag', componentId: info.id, bit }\n }\n state.componentStorageByBit[bit] = storage\n return bit\n}\n\n// Read a bitmask out of state.entityMask[] into a fresh Uint32Array.\nexport function readEntityMask(state: WorldState, eid: number): Uint32Array {\n const w = state.options.maskWordCount\n const out = new Uint32Array(w)\n const idx = eid & state.options.indexMask\n const base = idx * w\n for (let i = 0; i < w; i++) out[i] = state.entityMask[base + i] ?? 0\n return out\n}\n\nexport function writeEntityMask(state: WorldState, eid: number, mask: Uint32Array): void {\n const w = state.options.maskWordCount\n const idx = eid & state.options.indexMask\n const base = idx * w\n for (let i = 0; i < w; i++) state.entityMask[base + i] = mask[i] ?? 0\n}\n\n// Get the bit of a component within a world without registering.\nexport function tryGetComponentBit(state: WorldState, info: ComponentInfo): number | undefined {\n return state.componentBitFor.get(info.id)\n}\n\nexport function isWorld(x: unknown): x is World {\n if (!x || typeof x !== 'object') return false\n const obj = x as { id?: unknown; version?: unknown }\n if (typeof obj.id !== 'number') return false\n if (typeof obj.version !== 'string') return false\n return worldRegistry.has(obj.id)\n}\n","// aiecsjs/relations — entity-to-entity relations (experimental in 0.1).\n\nimport { registerRelationsCleanup } from './internal/entity.js'\nimport type { EntityId, Relation, RelationStorage, World, WorldState } from './internal/types.js'\nimport { getWorldState } from './internal/world.js'\n\nlet nextRelationId = 1\n\nexport function defineRelation<T = void>(options?: { exclusive?: boolean }): Relation<T> {\n const id = nextRelationId++\n return {\n __kind: 'relation',\n __id: id,\n __exclusive: options?.exclusive ?? false,\n __hasData: false,\n } as Relation<T>\n}\n\nexport const ChildOf: Relation = defineRelation({ exclusive: true })\n\nfunction getOrCreateStorage(state: WorldState, rel: Relation<any>): RelationStorage {\n let storage = state.relationStorage.get(rel.__id)\n if (storage) return storage\n storage = {\n rel,\n exclusive: rel.__exclusive ? new Int32Array(state.capacity).fill(-1) : null,\n outgoing: new Map<number, number[]>(),\n data: new Map<number, Map<number, unknown>>(),\n }\n state.relationStorage.set(rel.__id, storage)\n return storage\n}\n\nexport function addRelation<T>(\n world: World,\n source: EntityId,\n rel: Relation<T>,\n target: EntityId,\n data?: T,\n): void {\n const state = getWorldState(world)\n if (state.readOnly) throw new Error('aiecsjs: cannot mutate a read-only world')\n const storage = getOrCreateStorage(state, rel as Relation<any>)\n // Use raw idx as keys in relation storage so slot reuse invalidation is consistent\n const src = (source as number) & state.options.indexMask\n const tgt = (target as number) & state.options.indexMask\n\n if (storage.exclusive) {\n if (src >= storage.exclusive.length) {\n const next = new Int32Array(Math.max(src + 1, storage.exclusive.length * 2)).fill(-1)\n next.set(storage.exclusive)\n storage.exclusive = next\n }\n storage.exclusive[src] = tgt\n } else {\n let list = storage.outgoing.get(src)\n if (!list) {\n list = []\n storage.outgoing.set(src, list)\n }\n if (!list.includes(tgt)) list.push(tgt)\n }\n\n if (data !== undefined) {\n let inner = storage.data.get(src)\n if (!inner) {\n inner = new Map<number, unknown>()\n storage.data.set(src, inner)\n }\n inner.set(tgt, data)\n }\n}\n\nexport function removeRelation(\n world: World,\n source: EntityId,\n rel: Relation,\n target: EntityId,\n): void {\n const state = getWorldState(world)\n if (state.readOnly) throw new Error('aiecsjs: cannot mutate a read-only world')\n const storage = state.relationStorage.get(rel.__id)\n if (!storage) return\n const src = (source as number) & state.options.indexMask\n const tgt = (target as number) & state.options.indexMask\n if (storage.exclusive) {\n if (src < storage.exclusive.length && storage.exclusive[src] === tgt) {\n storage.exclusive[src] = -1\n }\n } else {\n const list = storage.outgoing.get(src)\n if (list) {\n const idx = list.indexOf(tgt)\n if (idx >= 0) list.splice(idx, 1)\n if (list.length === 0) storage.outgoing.delete(src)\n }\n }\n const inner = storage.data.get(src)\n if (inner) {\n inner.delete(tgt)\n if (inner.size === 0) storage.data.delete(src)\n }\n}\n\nexport function getRelationTargets(\n world: World,\n source: EntityId,\n rel: Relation,\n): readonly EntityId[] {\n const state = getWorldState(world)\n const storage = state.relationStorage.get(rel.__id)\n if (!storage) return []\n const src = (source as number) & state.options.indexMask\n if (storage.exclusive) {\n if (src < storage.exclusive.length) {\n const tgt = storage.exclusive[src] ?? -1\n return tgt >= 0 ? [tgt as EntityId] : []\n }\n return []\n }\n const list = storage.outgoing.get(src)\n if (!list) return []\n return list as EntityId[]\n}\n\n// Cleanup hook: when an entity is destroyed, remove all relations involving it.\nregisterRelationsCleanup((state: WorldState, eid: EntityId) => {\n const e = (eid as number) & state.options.indexMask\n for (const storage of state.relationStorage.values()) {\n if (storage.exclusive) {\n if (e < storage.exclusive.length) storage.exclusive[e] = -1\n // Also clear any references TO this entity as target\n for (let i = 0; i < storage.exclusive.length; i++) {\n if (storage.exclusive[i] === e) storage.exclusive[i] = -1\n }\n }\n storage.outgoing.delete(e)\n for (const [src, list] of storage.outgoing) {\n const idx = list.indexOf(e)\n if (idx >= 0) {\n list.splice(idx, 1)\n if (list.length === 0) storage.outgoing.delete(src)\n }\n }\n // Clear data entries involving this entity. The outer key is the source\n // eid; entries whose source IS this entity drop entirely. Entries that\n // reference this entity as a target are pruned from each inner map.\n storage.data.delete(e)\n for (const [src, inner] of storage.data) {\n inner.delete(e)\n if (inner.size === 0) storage.data.delete(src)\n }\n }\n})\n"]}
package/dist/relations.js CHANGED
@@ -1,2 +1,2 @@
1
- var f=new Map;function d(o){let s=f.get(o.id);if(!s)throw new Error(`aiecsjs: world ${o.id} is destroyed or unknown`);if(s.destroyed)throw new Error(`aiecsjs: world ${o.id} is destroyed`);return s}var m=1;function g(o){return {__kind:"relation",__id:m++,__exclusive:o?.exclusive??false,__hasData:false}}var W=g({exclusive:true});function x(o,s){let r=o.relationStorage.get(s.__id);return r||(r={rel:s,exclusive:s.__exclusive?new Int32Array(o.capacity).fill(-1):null,outgoing:new Map,data:new Map},o.relationStorage.set(s.__id,r),r)}function I(o,s,r,a,n){let t=d(o);if(t.readOnly)throw new Error("aiecsjs: cannot mutate a read-only world");let e=x(t,r),c=s,l=a;if(e.exclusive){if(c>=e.exclusive.length){let i=new Int32Array(Math.max(c+1,e.exclusive.length*2)).fill(-1);i.set(e.exclusive),e.exclusive=i;}e.exclusive[c]=l;}else {let i=e.outgoing.get(c);i||(i=[],e.outgoing.set(c,i)),i.includes(l)||i.push(l);}if(n!==void 0){let i=e.data.get(c);i||(i=new Map,e.data.set(c,i)),i.set(l,n);}}function E(o,s,r,a){let n=d(o);if(n.readOnly)throw new Error("aiecsjs: cannot mutate a read-only world");let t=n.relationStorage.get(r.__id);if(!t)return;let e=s,c=a;if(t.exclusive)e<t.exclusive.length&&t.exclusive[e]===c&&(t.exclusive[e]=-1);else {let i=t.outgoing.get(e);if(i){let u=i.indexOf(c);u>=0&&i.splice(u,1),i.length===0&&t.outgoing.delete(e);}}let l=t.data.get(e);l&&(l.delete(c),l.size===0&&t.data.delete(e));}function A(o,s,r){let n=d(o).relationStorage.get(r.__id);if(!n)return [];let t=s;if(n.exclusive){if(t<n.exclusive.length){let c=n.exclusive[t]??-1;return c>=0?[c]:[]}return []}let e=n.outgoing.get(t);return e||[]}export{W as ChildOf,I as addRelation,g as defineRelation,A as getRelationTargets,E as removeRelation};//# sourceMappingURL=relations.js.map
1
+ var f=new Map;function l(o){let a=f.get(o.id);if(!a)throw new Error(`aiecsjs: world ${o.id} is destroyed or unknown`);if(a.destroyed)throw new Error(`aiecsjs: world ${o.id} is destroyed`);return a}var g=1;function h(o){return {__kind:"relation",__id:g++,__exclusive:o?.exclusive??false,__hasData:false}}var A=h({exclusive:true});function b(o,a){let r=o.relationStorage.get(a.__id);return r||(r={rel:a,exclusive:a.__exclusive?new Int32Array(o.capacity).fill(-1):null,outgoing:new Map,data:new Map},o.relationStorage.set(a.__id,r),r)}function M(o,a,r,i,n){let e=l(o);if(e.readOnly)throw new Error("aiecsjs: cannot mutate a read-only world");let t=b(e,r),c=a&e.options.indexMask,d=i&e.options.indexMask;if(t.exclusive){if(c>=t.exclusive.length){let s=new Int32Array(Math.max(c+1,t.exclusive.length*2)).fill(-1);s.set(t.exclusive),t.exclusive=s;}t.exclusive[c]=d;}else {let s=t.outgoing.get(c);s||(s=[],t.outgoing.set(c,s)),s.includes(d)||s.push(d);}if(n!==void 0){let s=t.data.get(c);s||(s=new Map,t.data.set(c,s)),s.set(d,n);}}function B(o,a,r,i){let n=l(o);if(n.readOnly)throw new Error("aiecsjs: cannot mutate a read-only world");let e=n.relationStorage.get(r.__id);if(!e)return;let t=a&n.options.indexMask,c=i&n.options.indexMask;if(e.exclusive)t<e.exclusive.length&&e.exclusive[t]===c&&(e.exclusive[t]=-1);else {let s=e.outgoing.get(t);if(s){let u=s.indexOf(c);u>=0&&s.splice(u,1),s.length===0&&e.outgoing.delete(t);}}let d=e.data.get(t);d&&(d.delete(c),d.size===0&&e.data.delete(t));}function S(o,a,r){let i=l(o),n=i.relationStorage.get(r.__id);if(!n)return [];let e=a&i.options.indexMask;if(n.exclusive){if(e<n.exclusive.length){let c=n.exclusive[e]??-1;return c>=0?[c]:[]}return []}let t=n.outgoing.get(e);return t||[]}export{A as ChildOf,M as addRelation,h as defineRelation,S as getRelationTargets,B as removeRelation};//# sourceMappingURL=relations.js.map
2
2
  //# sourceMappingURL=relations.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/internal/world.ts","../src/relations.ts"],"names":["worldRegistry","getWorldState","world","state","nextRelationId","defineRelation","options","ChildOf","getOrCreateStorage","rel","storage","addRelation","source","target","data","src","tgt","next","list","inner","removeRelation","idx","getRelationTargets"],"mappings":"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","file":"relations.js","sourcesContent":["import { VERSION } from '../version.js'\nimport { cloneMask, copyMask, createMask, maskHash } from './bitmask.js'\nimport type {\n ArchetypeState,\n ComponentInfo,\n FieldInfo,\n ResolvedWorldOptions,\n World,\n WorldComponentStorage,\n WorldOptions,\n WorldState,\n} from './types.js'\n\nexport const WORLD_BRAND = Symbol.for('aiecsjs.world')\n\nconst DEFAULT_OPTIONS: Required<Omit<WorldOptions, 'buffer' | 'bufferByteOffset'>> = {\n initialCapacity: 1024,\n maxEntities: 1_000_000,\n indexBits: 24,\n generationBits: 8,\n}\n\nconst DEFAULT_MAX_COMPONENTS = 256\n\nlet nextWorldId = 1\nconst worldRegistry = new Map<number, WorldState>()\n\nexport function getWorldState(world: World): WorldState {\n const state = worldRegistry.get(world.id)\n if (!state) throw new Error(`aiecsjs: world ${world.id} is destroyed or unknown`)\n if (state.destroyed) throw new Error(`aiecsjs: world ${world.id} is destroyed`)\n return state\n}\n\nexport function registerWorld(state: WorldState): void {\n worldRegistry.set(state.id, state)\n}\n\nexport function unregisterWorld(id: number): void {\n worldRegistry.delete(id)\n}\n\nexport function isWorldRegistered(id: number): boolean {\n return worldRegistry.has(id)\n}\n\nfunction resolveOptions(opts: WorldOptions | undefined): ResolvedWorldOptions {\n const o = opts ?? {}\n const indexBits = o.indexBits ?? DEFAULT_OPTIONS.indexBits\n const generationBits = o.generationBits ?? DEFAULT_OPTIONS.generationBits\n const maxComponents = DEFAULT_MAX_COMPONENTS\n if (indexBits < 1 || indexBits > 24) {\n throw new Error('aiecsjs: indexBits must be in [1, 24]')\n }\n if (generationBits < 0 || generationBits > 16) {\n throw new Error('aiecsjs: generationBits must be in [0, 16]')\n }\n const maxByIndex = 1 << indexBits\n const initialCapacity = Math.max(\n 1,\n Math.min(o.initialCapacity ?? DEFAULT_OPTIONS.initialCapacity, maxByIndex),\n )\n const maxEntities = Math.max(\n initialCapacity,\n Math.min(o.maxEntities ?? DEFAULT_OPTIONS.maxEntities, maxByIndex),\n )\n return {\n initialCapacity,\n maxEntities,\n indexBits,\n generationBits,\n maxComponents,\n maskWordCount: Math.ceil(maxComponents / 32),\n buffer: o.buffer ?? null,\n bufferByteOffset: o.bufferByteOffset ?? 0,\n }\n}\n\nexport function createEmptyArchetype(maskWordCount: number, maxComponents: number): ArchetypeState {\n return {\n id: 0,\n mask: createMask(maskWordCount),\n size: 0,\n capacity: 16,\n entities: new Uint32Array(16),\n entityRow: new Map<number, number>(),\n componentBits: [],\n edgeAdd: new Int32Array(maxComponents).fill(-1),\n edgeRemove: new Int32Array(maxComponents).fill(-1),\n }\n}\n\nexport function createWorld(options?: WorldOptions): World {\n const resolved = resolveOptions(options)\n const id = nextWorldId++\n\n const generationCtor = resolved.generationBits > 8 ? Uint16Array : Uint8Array\n const generations = new generationCtor(resolved.initialCapacity)\n\n const state: WorldState = {\n id,\n capacity: resolved.initialCapacity,\n version: VERSION,\n options: resolved,\n size: 0,\n nextFreshIndex: 1, // 0 reserved\n freeList: [],\n generations,\n destroyed: false,\n componentBitFor: new Map<number, number>(),\n componentInfoByBit: new Array(resolved.maxComponents).fill(null),\n componentStorageByBit: new Array(resolved.maxComponents).fill(null),\n nextComponentBit: 0,\n entityArchetype: new Uint32Array(resolved.initialCapacity),\n entityMask: new Uint32Array(resolved.initialCapacity * resolved.maskWordCount),\n archetypes: [],\n archetypeByMaskHash: new Map<string, number>(),\n queryVersion: 0,\n queries: [],\n queryMasks: new Map(),\n queryArchetypeCache: [],\n queryArchetypeStamp: [],\n bitToQueries: new Map<number, Set<number>>(),\n reactiveBuffers: new Map(),\n observers: [],\n relationStorage: new Map(),\n sab: resolved.buffer,\n readOnly: false,\n }\n\n // Seed archetype 0 (the empty mask)\n const empty = createEmptyArchetype(resolved.maskWordCount, resolved.maxComponents)\n empty.id = 0\n state.archetypes.push(empty)\n state.archetypeByMaskHash.set(maskHash(empty.mask), 0)\n\n registerWorld(state)\n return makePublicWorld(state)\n}\n\nexport function makePublicWorld(state: WorldState): World {\n return {\n id: state.id,\n get capacity() {\n return state.capacity\n },\n version: state.version,\n } as World\n}\n\nexport function destroyWorld(world: World): void {\n const state = worldRegistry.get(world.id)\n if (!state || state.destroyed) return\n state.destroyed = true\n // Clear large buffers to help GC\n state.archetypes = []\n state.archetypeByMaskHash.clear()\n state.componentInfoByBit = []\n state.componentStorageByBit = []\n state.queries = []\n state.queryMasks.clear()\n state.queryArchetypeCache = []\n state.observers = []\n state.relationStorage.clear()\n state.reactiveBuffers.clear()\n unregisterWorld(world.id)\n}\n\nexport function resetWorld(world: World): void {\n const state = getWorldState(world)\n // Keep capacity and registered components; clear entities and per-entity state.\n state.size = 0\n state.nextFreshIndex = 1\n state.freeList = []\n state.generations.fill(0)\n state.entityArchetype.fill(0)\n state.entityMask.fill(0)\n // Wipe archetype memberships but keep archetype graph for stability.\n for (const arch of state.archetypes) {\n arch.size = 0\n arch.entityRow.clear()\n }\n // Clear SoA columns and AoS instances; storage allocation is preserved.\n for (const storage of state.componentStorageByBit) {\n if (!storage) continue\n if (storage.kind === 'soa' && storage.soa) {\n for (const k of Object.keys(storage.soa)) {\n storage.soa[k]?.fill(0)\n }\n } else if (storage.kind === 'aos' && storage.aos) {\n storage.aos.fill(undefined as unknown as never)\n }\n }\n state.queryVersion++\n for (const buf of state.reactiveBuffers.values()) {\n buf.entered.length = 0\n buf.exited.length = 0\n }\n}\n\nexport function getWorldSize(world: World): number {\n return getWorldState(world).size\n}\n\nexport function getWorldCapacity(world: World): number {\n return getWorldState(world).capacity\n}\n\n// --- Capacity growth ---\n\nexport function ensureCapacity(state: WorldState, needed: number): void {\n if (needed <= state.capacity) return\n if (needed > state.options.maxEntities) {\n throw new Error(\n `aiecsjs: requested capacity ${needed} exceeds maxEntities ${state.options.maxEntities}`,\n )\n }\n let newCap = state.capacity\n while (newCap < needed) newCap = Math.min(newCap * 2, state.options.maxEntities)\n growEntityArrays(state, newCap)\n}\n\nfunction growEntityArrays(state: WorldState, newCap: number): void {\n // generations\n const genCtor: any = state.generations.constructor\n const newGen = new genCtor(newCap) as Uint8Array | Uint16Array\n ;(newGen as Uint8Array).set(state.generations as Uint8Array)\n state.generations = newGen\n\n // entityArchetype\n const newArch = new Uint32Array(newCap)\n newArch.set(state.entityArchetype)\n state.entityArchetype = newArch\n\n // entityMask\n const wordCount = state.options.maskWordCount\n const newMask = new Uint32Array(newCap * wordCount)\n newMask.set(state.entityMask)\n state.entityMask = newMask\n\n // Component storages (SoA columns + AoS arrays)\n for (const storage of state.componentStorageByBit) {\n if (!storage) continue\n if (storage.kind === 'soa' && storage.soa) {\n const info = state.componentInfoByBit[storage.bit]\n if (info) growSoAColumns(storage.soa, info.fields, newCap)\n } else if (storage.kind === 'aos' && storage.aos) {\n storage.aos.length = newCap\n }\n }\n\n state.capacity = newCap\n}\n\nfunction growSoAColumns(soa: Record<string, any>, fields: FieldInfo[], newCap: number): void {\n for (const f of fields) {\n const old = soa[f.name]\n if (!old) continue\n const newLen = newCap * f.vectorLen\n if (old.length >= newLen) continue\n const next = new f.ctor(newLen)\n next.set(old)\n soa[f.name] = next\n }\n}\n\n// --- Archetype management ---\n\nexport function findOrCreateArchetype(\n state: WorldState,\n mask: Uint32Array,\n): { archId: number; created: boolean } {\n const key = maskHash(mask)\n const existing = state.archetypeByMaskHash.get(key)\n if (existing !== undefined) return { archId: existing, created: false }\n\n const id = state.archetypes.length\n const arch: ArchetypeState = {\n id,\n mask: cloneMask(mask),\n size: 0,\n capacity: 16,\n entities: new Uint32Array(16),\n entityRow: new Map<number, number>(),\n componentBits: collectBits(mask),\n edgeAdd: new Int32Array(state.options.maxComponents).fill(-1),\n edgeRemove: new Int32Array(state.options.maxComponents).fill(-1),\n }\n state.archetypes.push(arch)\n state.archetypeByMaskHash.set(key, id)\n state.queryVersion++\n return { archId: id, created: true }\n}\n\nexport function ensureArchetypeCapacity(arch: ArchetypeState, needed: number): void {\n if (needed <= arch.capacity) return\n let newCap = arch.capacity\n while (newCap < needed) newCap *= 2\n const next = new Uint32Array(newCap)\n next.set(arch.entities)\n arch.entities = next\n arch.capacity = newCap\n}\n\nfunction collectBits(mask: Uint32Array): number[] {\n const bits: number[] = []\n for (let w = 0; w < mask.length; w++) {\n let word = mask[w] ?? 0\n while (word !== 0) {\n const lsb = word & -word\n const bit = (w << 5) + (31 - Math.clz32(lsb))\n bits.push(bit)\n word &= word - 1\n }\n }\n return bits\n}\n\n// --- Component registration in a world ---\n\nexport function getOrRegisterComponentBit(state: WorldState, info: ComponentInfo): number {\n const existing = state.componentBitFor.get(info.id)\n if (existing !== undefined) return existing\n\n if (state.nextComponentBit >= state.options.maxComponents) {\n throw new Error(`aiecsjs: world reached maxComponents=${state.options.maxComponents}`)\n }\n const bit = state.nextComponentBit++\n state.componentBitFor.set(info.id, bit)\n state.componentInfoByBit[bit] = info\n\n // Create storage\n let storage: WorldComponentStorage\n if (info.kind === 'soa') {\n const soa: Record<string, any> = {}\n for (const f of info.fields) {\n soa[f.name] = new f.ctor(state.capacity * f.vectorLen)\n }\n storage = { kind: 'soa', componentId: info.id, bit, soa }\n } else if (info.kind === 'aos') {\n storage = { kind: 'aos', componentId: info.id, bit, aos: new Array(state.capacity) }\n } else {\n storage = { kind: 'tag', componentId: info.id, bit }\n }\n state.componentStorageByBit[bit] = storage\n return bit\n}\n\n// Read a bitmask out of state.entityMask[] into a fresh Uint32Array.\nexport function readEntityMask(state: WorldState, eid: number): Uint32Array {\n const w = state.options.maskWordCount\n const out = new Uint32Array(w)\n const base = eid * w\n for (let i = 0; i < w; i++) out[i] = state.entityMask[base + i] ?? 0\n return out\n}\n\nexport function writeEntityMask(state: WorldState, eid: number, mask: Uint32Array): void {\n const w = state.options.maskWordCount\n const base = eid * w\n for (let i = 0; i < w; i++) state.entityMask[base + i] = mask[i] ?? 0\n}\n\n// Get the bit of a component within a world without registering.\nexport function tryGetComponentBit(state: WorldState, info: ComponentInfo): number | undefined {\n return state.componentBitFor.get(info.id)\n}\n\nexport function isWorld(x: unknown): x is World {\n if (!x || typeof x !== 'object') return false\n const obj = x as { id?: unknown; version?: unknown }\n if (typeof obj.id !== 'number') return false\n if (typeof obj.version !== 'string') return false\n return worldRegistry.has(obj.id)\n}\n","// aiecsjs/relations — entity-to-entity relations (experimental in 0.1).\n\nimport { registerRelationsCleanup } from './internal/entity.js'\nimport type { EntityId, Relation, RelationStorage, World, WorldState } from './internal/types.js'\nimport { getWorldState } from './internal/world.js'\n\nlet nextRelationId = 1\n\nexport function defineRelation<T = void>(options?: { exclusive?: boolean }): Relation<T> {\n const id = nextRelationId++\n return {\n __kind: 'relation',\n __id: id,\n __exclusive: options?.exclusive ?? false,\n __hasData: false,\n } as Relation<T>\n}\n\nexport const ChildOf: Relation = defineRelation({ exclusive: true })\n\nfunction getOrCreateStorage(state: WorldState, rel: Relation<any>): RelationStorage {\n let storage = state.relationStorage.get(rel.__id)\n if (storage) return storage\n storage = {\n rel,\n exclusive: rel.__exclusive ? new Int32Array(state.capacity).fill(-1) : null,\n outgoing: new Map<number, number[]>(),\n data: new Map<number, Map<number, unknown>>(),\n }\n state.relationStorage.set(rel.__id, storage)\n return storage\n}\n\nexport function addRelation<T>(\n world: World,\n source: EntityId,\n rel: Relation<T>,\n target: EntityId,\n data?: T,\n): void {\n const state = getWorldState(world)\n if (state.readOnly) throw new Error('aiecsjs: cannot mutate a read-only world')\n const storage = getOrCreateStorage(state, rel as Relation<any>)\n const src = source as number\n const tgt = target as number\n\n if (storage.exclusive) {\n if (src >= storage.exclusive.length) {\n const next = new Int32Array(Math.max(src + 1, storage.exclusive.length * 2)).fill(-1)\n next.set(storage.exclusive)\n storage.exclusive = next\n }\n storage.exclusive[src] = tgt\n } else {\n let list = storage.outgoing.get(src)\n if (!list) {\n list = []\n storage.outgoing.set(src, list)\n }\n if (!list.includes(tgt)) list.push(tgt)\n }\n\n if (data !== undefined) {\n let inner = storage.data.get(src)\n if (!inner) {\n inner = new Map<number, unknown>()\n storage.data.set(src, inner)\n }\n inner.set(tgt, data)\n }\n}\n\nexport function removeRelation(\n world: World,\n source: EntityId,\n rel: Relation,\n target: EntityId,\n): void {\n const state = getWorldState(world)\n if (state.readOnly) throw new Error('aiecsjs: cannot mutate a read-only world')\n const storage = state.relationStorage.get(rel.__id)\n if (!storage) return\n const src = source as number\n const tgt = target as number\n if (storage.exclusive) {\n if (src < storage.exclusive.length && storage.exclusive[src] === tgt) {\n storage.exclusive[src] = -1\n }\n } else {\n const list = storage.outgoing.get(src)\n if (list) {\n const idx = list.indexOf(tgt)\n if (idx >= 0) list.splice(idx, 1)\n if (list.length === 0) storage.outgoing.delete(src)\n }\n }\n const inner = storage.data.get(src)\n if (inner) {\n inner.delete(tgt)\n if (inner.size === 0) storage.data.delete(src)\n }\n}\n\nexport function getRelationTargets(\n world: World,\n source: EntityId,\n rel: Relation,\n): readonly EntityId[] {\n const state = getWorldState(world)\n const storage = state.relationStorage.get(rel.__id)\n if (!storage) return []\n const src = source as number\n if (storage.exclusive) {\n if (src < storage.exclusive.length) {\n const tgt = storage.exclusive[src] ?? -1\n return tgt >= 0 ? [tgt as EntityId] : []\n }\n return []\n }\n const list = storage.outgoing.get(src)\n if (!list) return []\n return list as EntityId[]\n}\n\n// Cleanup hook: when an entity is destroyed, remove all relations involving it.\nregisterRelationsCleanup((state: WorldState, eid: EntityId) => {\n const e = eid as number\n for (const storage of state.relationStorage.values()) {\n if (storage.exclusive) {\n if (e < storage.exclusive.length) storage.exclusive[e] = -1\n // Also clear any references TO this entity as target\n for (let i = 0; i < storage.exclusive.length; i++) {\n if (storage.exclusive[i] === e) storage.exclusive[i] = -1\n }\n }\n storage.outgoing.delete(e)\n for (const [src, list] of storage.outgoing) {\n const idx = list.indexOf(e)\n if (idx >= 0) {\n list.splice(idx, 1)\n if (list.length === 0) storage.outgoing.delete(src)\n }\n }\n // Clear data entries involving this entity. The outer key is the source\n // eid; entries whose source IS this entity drop entirely. Entries that\n // reference this entity as a target are pruned from each inner map.\n storage.data.delete(e)\n for (const [src, inner] of storage.data) {\n inner.delete(e)\n if (inner.size === 0) storage.data.delete(src)\n }\n }\n})\n"]}
1
+ {"version":3,"sources":["../src/internal/world.ts","../src/relations.ts"],"names":["worldRegistry","getWorldState","world","state","nextRelationId","defineRelation","options","ChildOf","getOrCreateStorage","rel","storage","addRelation","source","target","data","src","tgt","next","list","inner","removeRelation","idx","getRelationTargets"],"mappings":"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","file":"relations.js","sourcesContent":["import { VERSION } from '../version.js'\nimport { cloneMask, copyMask, createMask, maskHash } from './bitmask.js'\nimport type {\n ArchetypeState,\n ComponentInfo,\n FieldInfo,\n ResolvedWorldOptions,\n World,\n WorldComponentStorage,\n WorldOptions,\n WorldState,\n} from './types.js'\n\nexport const WORLD_BRAND = Symbol.for('aiecsjs.world')\n\nconst DEFAULT_OPTIONS: Required<Omit<WorldOptions, 'buffer' | 'bufferByteOffset'>> = {\n initialCapacity: 1024,\n maxEntities: 1_000_000,\n indexBits: 24,\n generationBits: 8,\n}\n\nconst DEFAULT_MAX_COMPONENTS = 256\n\nlet nextWorldId = 1\nconst worldRegistry = new Map<number, WorldState>()\n\nexport function getWorldState(world: World): WorldState {\n const state = worldRegistry.get(world.id)\n if (!state) throw new Error(`aiecsjs: world ${world.id} is destroyed or unknown`)\n if (state.destroyed) throw new Error(`aiecsjs: world ${world.id} is destroyed`)\n return state\n}\n\nexport function registerWorld(state: WorldState): void {\n worldRegistry.set(state.id, state)\n}\n\nexport function unregisterWorld(id: number): void {\n worldRegistry.delete(id)\n}\n\nexport function isWorldRegistered(id: number): boolean {\n return worldRegistry.has(id)\n}\n\nfunction resolveOptions(opts: WorldOptions | undefined): ResolvedWorldOptions {\n const o = opts ?? {}\n const indexBits = o.indexBits ?? DEFAULT_OPTIONS.indexBits\n const generationBits = o.generationBits ?? DEFAULT_OPTIONS.generationBits\n const maxComponents = DEFAULT_MAX_COMPONENTS\n if (indexBits < 1 || indexBits > 24) {\n throw new Error('aiecsjs: indexBits must be in [1, 24]')\n }\n if (generationBits < 0 || generationBits > 16) {\n throw new Error('aiecsjs: generationBits must be in [0, 16]')\n }\n const maxByIndex = 1 << indexBits\n const initialCapacity = Math.max(\n 1,\n Math.min(o.initialCapacity ?? DEFAULT_OPTIONS.initialCapacity, maxByIndex),\n )\n const maxEntities = Math.max(\n initialCapacity,\n Math.min(o.maxEntities ?? DEFAULT_OPTIONS.maxEntities, maxByIndex),\n )\n return {\n initialCapacity,\n maxEntities,\n indexBits,\n generationBits,\n indexMask: (1 << indexBits) - 1,\n generationMask: generationBits === 0 ? 0 : (1 << generationBits) - 1,\n maxComponents,\n maskWordCount: Math.ceil(maxComponents / 32),\n buffer: o.buffer ?? null,\n bufferByteOffset: o.bufferByteOffset ?? 0,\n }\n}\n\nexport function createEmptyArchetype(maskWordCount: number, maxComponents: number): ArchetypeState {\n return {\n id: 0,\n mask: createMask(maskWordCount),\n size: 0,\n capacity: 16,\n entities: new Uint32Array(16),\n entityRow: new Map<number, number>(),\n componentBits: [],\n edgeAdd: new Int32Array(maxComponents).fill(-1),\n edgeRemove: new Int32Array(maxComponents).fill(-1),\n }\n}\n\nexport function createWorld(options?: WorldOptions): World {\n const resolved = resolveOptions(options)\n const id = nextWorldId++\n\n const generationCtor = resolved.generationBits > 8 ? Uint16Array : Uint8Array\n const generations = new generationCtor(resolved.initialCapacity)\n\n const state: WorldState = {\n id,\n capacity: resolved.initialCapacity,\n version: VERSION,\n options: resolved,\n size: 0,\n nextFreshIndex: 1, // 0 reserved\n freeList: [],\n generations,\n destroyed: false,\n componentBitFor: new Map<number, number>(),\n componentInfoByBit: new Array(resolved.maxComponents).fill(null),\n componentStorageByBit: new Array(resolved.maxComponents).fill(null),\n nextComponentBit: 0,\n entityArchetype: new Uint32Array(resolved.initialCapacity),\n entityMask: new Uint32Array(resolved.initialCapacity * resolved.maskWordCount),\n archetypes: [],\n archetypeByMaskHash: new Map<string, number>(),\n queryVersion: 0,\n queries: [],\n queryMasks: new Map(),\n queryArchetypeCache: [],\n queryArchetypeStamp: [],\n bitToQueries: new Map<number, Set<number>>(),\n reactiveBuffers: new Map(),\n observers: [],\n relationStorage: new Map(),\n sab: resolved.buffer,\n readOnly: false,\n }\n\n // Seed archetype 0 (the empty mask)\n const empty = createEmptyArchetype(resolved.maskWordCount, resolved.maxComponents)\n empty.id = 0\n state.archetypes.push(empty)\n state.archetypeByMaskHash.set(maskHash(empty.mask), 0)\n\n registerWorld(state)\n return makePublicWorld(state)\n}\n\nexport function makePublicWorld(state: WorldState): World {\n return {\n id: state.id,\n get capacity() {\n return state.capacity\n },\n version: state.version,\n } as World\n}\n\nexport function destroyWorld(world: World): void {\n const state = worldRegistry.get(world.id)\n if (!state || state.destroyed) return\n state.destroyed = true\n // Clear large buffers to help GC\n state.archetypes = []\n state.archetypeByMaskHash.clear()\n state.componentInfoByBit = []\n state.componentStorageByBit = []\n state.queries = []\n state.queryMasks.clear()\n state.queryArchetypeCache = []\n state.observers = []\n state.relationStorage.clear()\n state.reactiveBuffers.clear()\n unregisterWorld(world.id)\n}\n\nexport function resetWorld(world: World): void {\n const state = getWorldState(world)\n // Keep capacity and registered components; clear entities and per-entity state.\n state.size = 0\n state.nextFreshIndex = 1\n state.freeList = []\n state.generations.fill(0)\n state.entityArchetype.fill(0)\n state.entityMask.fill(0)\n // Wipe archetype memberships but keep archetype graph for stability.\n for (const arch of state.archetypes) {\n arch.size = 0\n arch.entityRow.clear()\n }\n // Clear SoA columns and AoS instances; storage allocation is preserved.\n for (const storage of state.componentStorageByBit) {\n if (!storage) continue\n if (storage.kind === 'soa' && storage.soa) {\n for (const k of Object.keys(storage.soa)) {\n storage.soa[k]?.fill(0)\n }\n } else if (storage.kind === 'aos' && storage.aos) {\n storage.aos.fill(undefined as unknown as never)\n }\n }\n state.queryVersion++\n for (const buf of state.reactiveBuffers.values()) {\n buf.entered.length = 0\n buf.exited.length = 0\n }\n}\n\nexport function getWorldSize(world: World): number {\n return getWorldState(world).size\n}\n\nexport function getWorldCapacity(world: World): number {\n return getWorldState(world).capacity\n}\n\n// --- Capacity growth ---\n\nexport function ensureCapacity(state: WorldState, needed: number): void {\n if (needed <= state.capacity) return\n if (needed > state.options.maxEntities) {\n throw new Error(\n `aiecsjs: requested capacity ${needed} exceeds maxEntities ${state.options.maxEntities}`,\n )\n }\n let newCap = state.capacity\n while (newCap < needed) newCap = Math.min(newCap * 2, state.options.maxEntities)\n growEntityArrays(state, newCap)\n}\n\nfunction growEntityArrays(state: WorldState, newCap: number): void {\n // generations\n const genCtor: any = state.generations.constructor\n const newGen = new genCtor(newCap) as Uint8Array | Uint16Array\n ;(newGen as Uint8Array).set(state.generations as Uint8Array)\n state.generations = newGen\n\n // entityArchetype\n const newArch = new Uint32Array(newCap)\n newArch.set(state.entityArchetype)\n state.entityArchetype = newArch\n\n // entityMask\n const wordCount = state.options.maskWordCount\n const newMask = new Uint32Array(newCap * wordCount)\n newMask.set(state.entityMask)\n state.entityMask = newMask\n\n // Component storages (SoA columns + AoS arrays)\n for (const storage of state.componentStorageByBit) {\n if (!storage) continue\n if (storage.kind === 'soa' && storage.soa) {\n const info = state.componentInfoByBit[storage.bit]\n if (info) growSoAColumns(storage.soa, info.fields, newCap)\n } else if (storage.kind === 'aos' && storage.aos) {\n storage.aos.length = newCap\n }\n }\n\n state.capacity = newCap\n}\n\nfunction growSoAColumns(soa: Record<string, any>, fields: FieldInfo[], newCap: number): void {\n for (const f of fields) {\n const old = soa[f.name]\n if (!old) continue\n const newLen = newCap * f.vectorLen\n if (old.length >= newLen) continue\n const next = new f.ctor(newLen)\n next.set(old)\n soa[f.name] = next\n }\n}\n\n// --- Archetype management ---\n\nexport function findOrCreateArchetype(\n state: WorldState,\n mask: Uint32Array,\n): { archId: number; created: boolean } {\n const key = maskHash(mask)\n const existing = state.archetypeByMaskHash.get(key)\n if (existing !== undefined) return { archId: existing, created: false }\n\n const id = state.archetypes.length\n const arch: ArchetypeState = {\n id,\n mask: cloneMask(mask),\n size: 0,\n capacity: 16,\n entities: new Uint32Array(16),\n entityRow: new Map<number, number>(),\n componentBits: collectBits(mask),\n edgeAdd: new Int32Array(state.options.maxComponents).fill(-1),\n edgeRemove: new Int32Array(state.options.maxComponents).fill(-1),\n }\n state.archetypes.push(arch)\n state.archetypeByMaskHash.set(key, id)\n state.queryVersion++\n return { archId: id, created: true }\n}\n\nexport function ensureArchetypeCapacity(arch: ArchetypeState, needed: number): void {\n if (needed <= arch.capacity) return\n let newCap = arch.capacity\n while (newCap < needed) newCap *= 2\n const next = new Uint32Array(newCap)\n next.set(arch.entities)\n arch.entities = next\n arch.capacity = newCap\n}\n\nfunction collectBits(mask: Uint32Array): number[] {\n const bits: number[] = []\n for (let w = 0; w < mask.length; w++) {\n let word = mask[w] ?? 0\n while (word !== 0) {\n const lsb = word & -word\n const bit = (w << 5) + (31 - Math.clz32(lsb))\n bits.push(bit)\n word &= word - 1\n }\n }\n return bits\n}\n\n// --- Component registration in a world ---\n\nexport function getOrRegisterComponentBit(state: WorldState, info: ComponentInfo): number {\n const existing = state.componentBitFor.get(info.id)\n if (existing !== undefined) return existing\n\n if (state.nextComponentBit >= state.options.maxComponents) {\n throw new Error(`aiecsjs: world reached maxComponents=${state.options.maxComponents}`)\n }\n const bit = state.nextComponentBit++\n state.componentBitFor.set(info.id, bit)\n state.componentInfoByBit[bit] = info\n\n // Create storage\n let storage: WorldComponentStorage\n if (info.kind === 'soa') {\n const soa: Record<string, any> = {}\n for (const f of info.fields) {\n soa[f.name] = new f.ctor(state.capacity * f.vectorLen)\n }\n storage = { kind: 'soa', componentId: info.id, bit, soa }\n } else if (info.kind === 'aos') {\n storage = { kind: 'aos', componentId: info.id, bit, aos: new Array(state.capacity) }\n } else {\n storage = { kind: 'tag', componentId: info.id, bit }\n }\n state.componentStorageByBit[bit] = storage\n return bit\n}\n\n// Read a bitmask out of state.entityMask[] into a fresh Uint32Array.\nexport function readEntityMask(state: WorldState, eid: number): Uint32Array {\n const w = state.options.maskWordCount\n const out = new Uint32Array(w)\n const idx = eid & state.options.indexMask\n const base = idx * w\n for (let i = 0; i < w; i++) out[i] = state.entityMask[base + i] ?? 0\n return out\n}\n\nexport function writeEntityMask(state: WorldState, eid: number, mask: Uint32Array): void {\n const w = state.options.maskWordCount\n const idx = eid & state.options.indexMask\n const base = idx * w\n for (let i = 0; i < w; i++) state.entityMask[base + i] = mask[i] ?? 0\n}\n\n// Get the bit of a component within a world without registering.\nexport function tryGetComponentBit(state: WorldState, info: ComponentInfo): number | undefined {\n return state.componentBitFor.get(info.id)\n}\n\nexport function isWorld(x: unknown): x is World {\n if (!x || typeof x !== 'object') return false\n const obj = x as { id?: unknown; version?: unknown }\n if (typeof obj.id !== 'number') return false\n if (typeof obj.version !== 'string') return false\n return worldRegistry.has(obj.id)\n}\n","// aiecsjs/relations — entity-to-entity relations (experimental in 0.1).\n\nimport { registerRelationsCleanup } from './internal/entity.js'\nimport type { EntityId, Relation, RelationStorage, World, WorldState } from './internal/types.js'\nimport { getWorldState } from './internal/world.js'\n\nlet nextRelationId = 1\n\nexport function defineRelation<T = void>(options?: { exclusive?: boolean }): Relation<T> {\n const id = nextRelationId++\n return {\n __kind: 'relation',\n __id: id,\n __exclusive: options?.exclusive ?? false,\n __hasData: false,\n } as Relation<T>\n}\n\nexport const ChildOf: Relation = defineRelation({ exclusive: true })\n\nfunction getOrCreateStorage(state: WorldState, rel: Relation<any>): RelationStorage {\n let storage = state.relationStorage.get(rel.__id)\n if (storage) return storage\n storage = {\n rel,\n exclusive: rel.__exclusive ? new Int32Array(state.capacity).fill(-1) : null,\n outgoing: new Map<number, number[]>(),\n data: new Map<number, Map<number, unknown>>(),\n }\n state.relationStorage.set(rel.__id, storage)\n return storage\n}\n\nexport function addRelation<T>(\n world: World,\n source: EntityId,\n rel: Relation<T>,\n target: EntityId,\n data?: T,\n): void {\n const state = getWorldState(world)\n if (state.readOnly) throw new Error('aiecsjs: cannot mutate a read-only world')\n const storage = getOrCreateStorage(state, rel as Relation<any>)\n // Use raw idx as keys in relation storage so slot reuse invalidation is consistent\n const src = (source as number) & state.options.indexMask\n const tgt = (target as number) & state.options.indexMask\n\n if (storage.exclusive) {\n if (src >= storage.exclusive.length) {\n const next = new Int32Array(Math.max(src + 1, storage.exclusive.length * 2)).fill(-1)\n next.set(storage.exclusive)\n storage.exclusive = next\n }\n storage.exclusive[src] = tgt\n } else {\n let list = storage.outgoing.get(src)\n if (!list) {\n list = []\n storage.outgoing.set(src, list)\n }\n if (!list.includes(tgt)) list.push(tgt)\n }\n\n if (data !== undefined) {\n let inner = storage.data.get(src)\n if (!inner) {\n inner = new Map<number, unknown>()\n storage.data.set(src, inner)\n }\n inner.set(tgt, data)\n }\n}\n\nexport function removeRelation(\n world: World,\n source: EntityId,\n rel: Relation,\n target: EntityId,\n): void {\n const state = getWorldState(world)\n if (state.readOnly) throw new Error('aiecsjs: cannot mutate a read-only world')\n const storage = state.relationStorage.get(rel.__id)\n if (!storage) return\n const src = (source as number) & state.options.indexMask\n const tgt = (target as number) & state.options.indexMask\n if (storage.exclusive) {\n if (src < storage.exclusive.length && storage.exclusive[src] === tgt) {\n storage.exclusive[src] = -1\n }\n } else {\n const list = storage.outgoing.get(src)\n if (list) {\n const idx = list.indexOf(tgt)\n if (idx >= 0) list.splice(idx, 1)\n if (list.length === 0) storage.outgoing.delete(src)\n }\n }\n const inner = storage.data.get(src)\n if (inner) {\n inner.delete(tgt)\n if (inner.size === 0) storage.data.delete(src)\n }\n}\n\nexport function getRelationTargets(\n world: World,\n source: EntityId,\n rel: Relation,\n): readonly EntityId[] {\n const state = getWorldState(world)\n const storage = state.relationStorage.get(rel.__id)\n if (!storage) return []\n const src = (source as number) & state.options.indexMask\n if (storage.exclusive) {\n if (src < storage.exclusive.length) {\n const tgt = storage.exclusive[src] ?? -1\n return tgt >= 0 ? [tgt as EntityId] : []\n }\n return []\n }\n const list = storage.outgoing.get(src)\n if (!list) return []\n return list as EntityId[]\n}\n\n// Cleanup hook: when an entity is destroyed, remove all relations involving it.\nregisterRelationsCleanup((state: WorldState, eid: EntityId) => {\n const e = (eid as number) & state.options.indexMask\n for (const storage of state.relationStorage.values()) {\n if (storage.exclusive) {\n if (e < storage.exclusive.length) storage.exclusive[e] = -1\n // Also clear any references TO this entity as target\n for (let i = 0; i < storage.exclusive.length; i++) {\n if (storage.exclusive[i] === e) storage.exclusive[i] = -1\n }\n }\n storage.outgoing.delete(e)\n for (const [src, list] of storage.outgoing) {\n const idx = list.indexOf(e)\n if (idx >= 0) {\n list.splice(idx, 1)\n if (list.length === 0) storage.outgoing.delete(src)\n }\n }\n // Clear data entries involving this entity. The outer key is the source\n // eid; entries whose source IS this entity drop entirely. Entries that\n // reference this entity as a target are pruned from each inner map.\n storage.data.delete(e)\n for (const [src, inner] of storage.data) {\n inner.delete(e)\n if (inner.size === 0) storage.data.delete(src)\n }\n }\n})\n"]}