aiecsjs 0.5.9 → 0.6.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.
- package/README.md +46 -12
- package/README_ZHTW.md +46 -12
- package/api.json +171 -48
- package/dist/chunk-27JX7WHY.js +2 -0
- package/dist/chunk-27JX7WHY.js.map +1 -0
- package/dist/chunk-GJ6U2TIU.js +2 -0
- package/dist/chunk-GJ6U2TIU.js.map +1 -0
- package/dist/chunk-LNZZ4WWC.js +2 -0
- package/dist/chunk-LNZZ4WWC.js.map +1 -0
- package/dist/chunk-MVBFYRHY.cjs +2 -0
- package/dist/chunk-MVBFYRHY.cjs.map +1 -0
- package/dist/chunk-NPNWRCB5.cjs +2 -0
- package/dist/chunk-NPNWRCB5.cjs.map +1 -0
- package/dist/chunk-O4UVAWFU.cjs +2 -0
- package/dist/chunk-O4UVAWFU.cjs.map +1 -0
- package/dist/commands.cjs +1 -1
- package/dist/commands.cjs.map +1 -1
- package/dist/commands.d.cts +1 -1
- package/dist/commands.d.ts +1 -1
- package/dist/commands.js +1 -1
- package/dist/commands.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -13
- package/dist/index.d.ts +79 -13
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loop.cjs +1 -1
- package/dist/loop.cjs.map +1 -1
- package/dist/loop.d.cts +12 -0
- package/dist/loop.d.ts +12 -0
- package/dist/loop.js +1 -1
- package/dist/loop.js.map +1 -1
- package/dist/observers.cjs +1 -1
- package/dist/observers.cjs.map +1 -1
- package/dist/observers.d.cts +7 -1
- package/dist/observers.d.ts +7 -1
- package/dist/observers.js +1 -1
- package/dist/observers.js.map +1 -1
- package/dist/relations.cjs +1 -1
- package/dist/relations.cjs.map +1 -1
- package/dist/relations.d.cts +18 -9
- package/dist/relations.d.ts +18 -9
- package/dist/relations.js +1 -1
- package/dist/relations.js.map +1 -1
- package/dist/serialize.cjs +1 -1
- package/dist/serialize.d.cts +30 -5
- package/dist/serialize.d.ts +30 -5
- package/dist/serialize.js +1 -1
- package/dist/{types-BeVLA7xG.d.cts → types-C6TkQ7HW.d.cts} +33 -0
- package/dist/{types-BeVLA7xG.d.ts → types-C6TkQ7HW.d.ts} +33 -0
- package/dist/worker.cjs +1 -1
- package/dist/worker.cjs.map +1 -1
- package/dist/worker.d.cts +10 -3
- package/dist/worker.d.ts +10 -3
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/llms-full.txt +114 -18
- package/llms.txt +1 -0
- package/package.json +57 -22
- package/dist/chunk-JDV5JZP2.cjs +0 -2
- package/dist/chunk-JDV5JZP2.cjs.map +0 -1
- package/dist/chunk-OBMQME2P.js +0 -2
- package/dist/chunk-OBMQME2P.js.map +0 -1
- package/dist/chunk-Q2XLL6YG.cjs +0 -2
- package/dist/chunk-Q2XLL6YG.cjs.map +0 -1
- package/dist/chunk-USHKPJQG.cjs +0 -2
- package/dist/chunk-USHKPJQG.cjs.map +0 -1
- package/dist/chunk-XKN3ULCC.js +0 -2
- package/dist/chunk-XKN3ULCC.js.map +0 -1
- package/dist/chunk-ZZBNE7C5.js +0 -2
- package/dist/chunk-ZZBNE7C5.js.map +0 -1
package/dist/loop.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
1
|
+
var d=typeof globalThis.requestAnimationFrame=="function",h=()=>typeof performance<"u"?performance.now():Date.now();function b(e){return d?globalThis.requestAnimationFrame(e):setTimeout(()=>e(h()),16)}function x(e){d&&typeof globalThis.cancelAnimationFrame=="function"?globalThis.cancelAnimationFrame(e):clearTimeout(e);}function R(e){if(!e||typeof e!="object")throw new TypeError("aiecsjs: options must be an object");let n=e.fixed??1/60,i=e.maxSubSteps??5,f=e.onUpdate,a=e.onRender;if(typeof f!="function")throw new TypeError("aiecsjs: onUpdate must be a function");if(a!=null&&typeof a!="function")throw new TypeError("aiecsjs: onRender must be a function");if(!(Number.isFinite(n)&&n>0))throw new RangeError("aiecsjs: fixed must be a finite number > 0");if(!(Number.isFinite(i)&&i>=1))throw new RangeError("aiecsjs: maxSubSteps must be a finite number >= 1");let t=false,o=0,s=0,r=0,c=0;return {start(){if(t)return;t=true;let w=++c;s=h(),r=0;let u=()=>t&&w===c,m=p=>{if(!u())return;let T=p-s;s=p,r+=Math.min(Math.max(0,T)/1e3,n*i);let l=0;for(;r>=n&&l<i;){if(f(n),!u())return;r-=n,l++;}if(a){let y=r/n;if(a(y),!u())return}o=b(m);};o=b(m);},stop(){t&&(t=false,o&&x(o),o=0);}}}export{R as createLoop};//# sourceMappingURL=loop.js.map
|
|
2
2
|
//# sourceMappingURL=loop.js.map
|
package/dist/loop.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/loop.ts"],"names":["hasRAF","now","raf","cb","cancelRaf","handle","createLoop","options","fixed","maxSubSteps","onUpdate","onRender","running","lastT","accumulator","tick","t","dtMs","steps","alpha"],"mappings":"AAcA,IAAMA,
|
|
1
|
+
{"version":3,"sources":["../src/loop.ts"],"names":["hasRAF","now","raf","cb","cancelRaf","handle","createLoop","options","fixed","maxSubSteps","onUpdate","onRender","running","lastT","accumulator","epoch","token","live","tick","t","dtMs","steps","alpha"],"mappings":"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","file":"loop.js","sourcesContent":["// aiecsjs/loop — fixed-timestep accumulator loop.\n\ninterface LoopOptions {\n fixed?: number\n maxSubSteps?: number\n onUpdate: (dt: number) => void\n onRender?: (alpha: number) => void\n}\n\ninterface Loop {\n start(): void\n stop(): void\n}\n\nconst hasRAF = typeof globalThis.requestAnimationFrame === 'function'\nconst now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now())\n\nfunction raf(cb: (t: number) => void): number {\n if (hasRAF) return globalThis.requestAnimationFrame(cb)\n return setTimeout(() => cb(now()), 16) as unknown as number\n}\nfunction cancelRaf(handle: number): void {\n if (hasRAF && typeof globalThis.cancelAnimationFrame === 'function') {\n globalThis.cancelAnimationFrame(handle)\n } else {\n clearTimeout(handle as unknown as ReturnType<typeof setTimeout>)\n }\n}\n\n/**\n * Create a fixed-timestep loop driven by `requestAnimationFrame` (a 16 ms\n * `setTimeout` outside browsers). `onUpdate(fixed)` runs up to `maxSubSteps`\n * times per frame; `onRender(alpha)` runs once per frame with the leftover\n * fraction of a step.\n *\n * Arguments are validated before anything is scheduled. This subpath exports\n * no error class, so misuse throws built-in errors with an `aiecsjs: ` prefix:\n * `TypeError` for a missing options object or a non-function `onUpdate` /\n * `onRender`, `RangeError` for a `fixed` that is not a finite number > 0 or a\n * `maxSubSteps` that is not a finite number >= 1.\n */\nexport function createLoop(options: LoopOptions): Loop {\n if (!options || typeof options !== 'object') {\n throw new TypeError('aiecsjs: options must be an object')\n }\n const fixed = options.fixed ?? 1 / 60\n const maxSubSteps = options.maxSubSteps ?? 5\n const onUpdate = options.onUpdate\n const onRender = options.onRender\n if (typeof onUpdate !== 'function') throw new TypeError('aiecsjs: onUpdate must be a function')\n if (onRender != null && typeof onRender !== 'function') {\n throw new TypeError('aiecsjs: onRender must be a function')\n }\n // NaN / Infinity / <= 0 would stall the loop, run zero-length steps every\n // frame, or disable the spiral-of-death clamp.\n if (!(Number.isFinite(fixed) && fixed > 0)) {\n throw new RangeError('aiecsjs: fixed must be a finite number > 0')\n }\n if (!(Number.isFinite(maxSubSteps) && maxSubSteps >= 1)) {\n throw new RangeError('aiecsjs: maxSubSteps must be a finite number >= 1')\n }\n\n let running = false\n let handle = 0\n let lastT = 0\n let accumulator = 0\n // Bumped by every start(). A tick chain belongs to the start() that created\n // it; it stops as soon as the loop is stopped or restarted — even from\n // inside onUpdate / onRender — so it never keeps stepping, renders after\n // stop(), or runs alongside a newer chain.\n let epoch = 0\n\n return {\n start() {\n if (running) return\n running = true\n const token = ++epoch\n lastT = now()\n accumulator = 0\n const live = (): boolean => running && token === epoch\n const tick = (t: number): void => {\n if (!live()) return\n const dtMs = t - lastT\n lastT = t\n // Clamp to non-negative: the first rAF callback can receive a\n // frame-begin timestamp earlier than the `performance.now()` sampled\n // by `start()`, which would otherwise drive the accumulator (and thus\n // `onRender`'s alpha) negative.\n accumulator += Math.min(Math.max(0, dtMs) / 1000, fixed * maxSubSteps)\n let steps = 0\n while (accumulator >= fixed && steps < maxSubSteps) {\n onUpdate(fixed)\n if (!live()) return\n accumulator -= fixed\n steps++\n }\n if (onRender) {\n const alpha = accumulator / fixed\n onRender(alpha)\n if (!live()) return\n }\n handle = raf(tick)\n }\n handle = raf(tick)\n },\n stop() {\n if (!running) return\n running = false\n if (handle) cancelRaf(handle)\n handle = 0\n },\n }\n}\n"]}
|
package/dist/observers.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
'use strict';var
|
|
1
|
+
'use strict';var chunkO4UVAWFU_cjs=require('./chunk-O4UVAWFU.cjs'),chunkNPNWRCB5_cjs=require('./chunk-NPNWRCB5.cjs');function C(n,e){if(!e)return n;if(e.aborted)return n(),()=>{};let o=()=>{n();};e.addEventListener("abort",o,{once:true});let r=false;return ()=>{r||(r=true,e.removeEventListener("abort",o),n());}}function j(n,e,o,r){return y(n,e,"add",o,r)}function D(n,e,o,r){return y(n,e,"remove",o,r)}function H(n,e,o,r){return y(n,e,"set",o,r)}function P(n,e,o,r,i){let d=chunkNPNWRCB5_cjs.i(n);W(r);let t=chunkO4UVAWFU_cjs.d(e);return chunkO4UVAWFU_cjs.e(d,t),A(d,o,-1,t.id,r,i)}function W(n){if(typeof n!="function")throw new chunkNPNWRCB5_cjs.f("aiecsjs: handler must be a function")}function y(n,e,o,r,i){let d=chunkNPNWRCB5_cjs.i(n);W(r);let t=chunkNPNWRCB5_cjs.q(d,chunkNPNWRCB5_cjs.J(e));return A(d,o,t,-1,r,i)}function A(n,e,o,r,i,d){let t={event:e,componentBit:o,queryId:r,handler:i};return n.observers.push(t),C(()=>{let s=n.observers.indexOf(t);s>=0&&n.observers.splice(s,1);},d?.signal)}function f(n,e,o,r,i){let d=Array.from(n.observers);for(let t of d)t.event===r&&t.componentBit===o&&n.observers.includes(t)&&t.handler(e,i);}function v(n,e,o,r){let{withMask:i,anyMask:d,noneMask:t,anyHasBits:s}=n;return chunkNPNWRCB5_cjs.e(e,o,r,i,d,t,s)}function g(n,e,o,r,i){f(n,e,o,"add"),S(n,e,r,i);}function q(n,e,o){f(n,e,o,"remove");}function w(n,e,o,r){let i=n.options.maskWordCount,d=(e&n.options.indexMask)*i,t=n.componentInfoByBit[o]?.id??-1;f(n,e,o,"set",r);let s=Array.from(n.observers);for(let u of s){if(u.event!=="set"||u.queryId===-1||!n.observers.includes(u))continue;let c=n.queries[u.queryId],m=n.queryMasks.get(u.queryId);!c||!m||!c.all.includes(t)&&!c.any.includes(t)||v(m,n.entityMask,d,i)&&u.handler(e,r);}}function S(n,e,o,r){let i=n.options.maskWordCount,d=Array.from(n.observers);for(let t of d){if(t.event==="set"||t.queryId===-1||!n.observers.includes(t))continue;let s=n.queryMasks.get(t.queryId);if(!s)continue;let u=v(s,o,0,i),c=v(s,r,0,i);(t.event==="add"?!u&&c:u&&!c)&&t.handler(e);}}chunkNPNWRCB5_cjs.Q({fireAdd:g,fireRemove:q,fireRemoveQuery:S,fireSet:w});chunkNPNWRCB5_cjs.E({dispatchDestroyObservers(n,e){let o=n.options.maskWordCount,r=e&n.options.indexMask,i=r*o,d=n.entityMask.slice(i,i+o);chunkNPNWRCB5_cjs.d(d,0,o,s=>{(n.entityMask[i+(s>>>5)]??0)&1<<(s&31)&&(n.removing.has(r*n.options.maxComponents+s)||f(n,e,s,"remove"));});let t=Array.from(n.observers);for(let s of t){if(s.event!=="remove"||s.queryId===-1||!n.observers.includes(s))continue;let u=n.queryMasks.get(s.queryId);u&&v(u,n.entityMask,i,o)&&s.handler(e);}}});exports.observe=P;exports.onAdd=j;exports.onRemove=D;exports.onSet=H;//# sourceMappingURL=observers.cjs.map
|
|
2
2
|
//# sourceMappingURL=observers.cjs.map
|
package/dist/observers.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/observers.ts"],"names":["bindAbortSignal","unsubscribe","signal","onAbort","detached","onAdd","world","component","handler","opts","registerComponentObserver","onRemove","onSet","observe","query","event","state","getWorldState","runQuery","entry","idx","info","getComponentInfo","bit","getOrRegisterComponentBit","fireAdd","eid","snapshot","obs","dispatchQueryObservers","fireRemove","fireSet","value","_prev","_current","w","base","bundle","isMatch","matchesEntityMask","registerObserverDispatch","registerObserversAPI","preMask","forEachSetBit","querySnapshot"],"mappings":"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","file":"observers.cjs","sourcesContent":["// 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
|
+
{"version":3,"sources":["../src/observers.ts"],"names":["bindAbortSignal","unsubscribe","signal","onAbort","detached","onAdd","world","component","handler","opts","registerComponentObserver","onRemove","onSet","observe","query","event","state","getWorldState","assertHandler","q","asQueryInternal","ensureQueryRegistered","addObserver","EcsError","bit","getOrRegisterComponentBit","getComponentInfo","componentBit","queryId","entry","idx","fireComponent","eid","value","snapshot","obs","bundleMatches","bundle","mask","base","w","withMask","anyMask","noneMask","anyHasBits","matchesEntityMask","fireAdd","prev","next","dispatchQueryObservers","fireRemove","fireSet","componentId","wasMatch","isMatch","registerObserverDispatch","registerObserversAPI","preMask","forEachSetBit","querySnapshot"],"mappings":"qHA2BA,SAASA,CAAAA,CAAgBC,CAAAA,CAAyBC,CAAAA,CAA6C,CAC7F,GAAI,CAACA,CAAAA,CAAQ,OAAOD,CAAAA,CACpB,GAAIC,CAAAA,CAAO,OAAA,CACT,OAAAD,CAAAA,GACO,IAAM,CAAC,CAAA,CAEhB,IAAME,EAAU,IAAY,CAC1BF,CAAAA,GACF,EACAC,CAAAA,CAAO,gBAAA,CAAiB,OAAA,CAASC,CAAAA,CAAS,CAAE,IAAA,CAAM,IAAK,CAAC,CAAA,CACxD,IAAIC,CAAAA,CAAW,KAAA,CACf,OAAO,IAAM,CACPA,CAAAA,GACJA,CAAAA,CAAW,IAAA,CACXF,CAAAA,CAAO,oBAAoB,OAAA,CAASC,CAAO,CAAA,CAC3CF,CAAAA,IACF,CACF,CAEO,SAASI,CAAAA,CACdC,EACAC,CAAAA,CACAC,CAAAA,CACAC,CAAAA,CACY,CACZ,OAAOC,CAAAA,CAA0BJ,CAAAA,CAAOC,CAAAA,CAAW,KAAA,CAAOC,EAASC,CAAI,CACzE,CAEO,SAASE,EACdL,CAAAA,CACAC,CAAAA,CACAC,CAAAA,CACAC,CAAAA,CACY,CACZ,OAAOC,CAAAA,CAA0BJ,CAAAA,CAAOC,CAAAA,CAAW,QAAA,CAAUC,CAAAA,CAASC,CAAI,CAC5E,CAyBO,SAASG,CAAAA,CACdN,CAAAA,CACAC,CAAAA,CACAC,EACAC,CAAAA,CACY,CACZ,OAAOC,CAAAA,CAA0BJ,EAAOC,CAAAA,CAAW,KAAA,CAAOC,CAAAA,CAASC,CAAI,CACzE,CAQO,SAASI,CAAAA,CACdP,CAAAA,CACAQ,EACAC,CAAAA,CACAP,CAAAA,CACAC,CAAAA,CACY,CACZ,IAAMO,CAAAA,CAAQC,mBAAAA,CAAcX,CAAK,CAAA,CACjCY,EAAcV,CAAO,CAAA,CAIrB,IAAMW,CAAAA,CAAIC,oBAAgBN,CAAK,CAAA,CAC/B,OAAAO,mBAAAA,CAAsBL,EAAOG,CAAC,CAAA,CACvBG,CAAAA,CAAYN,CAAAA,CAAOD,EAAO,EAAA,CAAII,CAAAA,CAAE,EAAA,CAAIX,CAAAA,CAASC,CAAI,CAC1D,CAKA,SAASS,CAAAA,CAAcV,EAAwB,CAC7C,GAAI,OAAOA,CAAAA,EAAY,WAAY,MAAM,IAAIe,mBAAAA,CAAS,qCAAqC,CAC7F,CAEA,SAASb,CAAAA,CACPJ,CAAAA,CACAC,EACAQ,CAAAA,CACAP,CAAAA,CACAC,CAAAA,CACY,CACZ,IAAMO,CAAAA,CAAQC,mBAAAA,CAAcX,CAAK,EACjCY,CAAAA,CAAcV,CAAO,CAAA,CACrB,IAAMgB,EAAMC,mBAAAA,CAA0BT,CAAAA,CAAOU,mBAAAA,CAAiBnB,CAAS,CAAC,CAAA,CACxE,OAAOe,CAAAA,CAAYN,CAAAA,CAAOD,EAAOS,CAAAA,CAAK,EAAA,CAAIhB,CAAAA,CAASC,CAAI,CACzD,CAGA,SAASa,CAAAA,CACPN,CAAAA,CACAD,EACAY,CAAAA,CACAC,CAAAA,CACApB,CAAAA,CACAC,CAAAA,CACY,CACZ,IAAMoB,CAAAA,CAAuB,CAAE,KAAA,CAAAd,EAAO,YAAA,CAAAY,CAAAA,CAAc,OAAA,CAAAC,CAAAA,CAAS,QAAApB,CAAQ,CAAA,CACrE,OAAAQ,CAAAA,CAAM,UAAU,IAAA,CAAKa,CAAK,CAAA,CACnB7B,CAAAA,CAAgB,IAAM,CAC3B,IAAM8B,CAAAA,CAAMd,CAAAA,CAAM,UAAU,OAAA,CAAQa,CAAK,CAAA,CACrCC,CAAAA,EAAO,GAAGd,CAAAA,CAAM,SAAA,CAAU,MAAA,CAAOc,CAAAA,CAAK,CAAC,EAC7C,CAAA,CAAGrB,CAAAA,EAAM,MAAM,CACjB,CAYA,SAASsB,CAAAA,CACPf,CAAAA,CACAgB,EACAR,CAAAA,CACAT,CAAAA,CACAkB,CAAAA,CACM,CACN,IAAMC,CAAAA,CAAW,KAAA,CAAM,IAAA,CAAKlB,CAAAA,CAAM,SAAS,CAAA,CAC3C,IAAA,IAAWmB,CAAAA,IAAOD,CAAAA,CACZC,EAAI,KAAA,GAAUpB,CAAAA,EACdoB,CAAAA,CAAI,YAAA,GAAiBX,GACpBR,CAAAA,CAAM,SAAA,CAAU,QAAA,CAASmB,CAAG,GACjCA,CAAAA,CAAI,OAAA,CAAQH,CAAAA,CAAKC,CAAK,EAE1B,CAGA,SAASG,CAAAA,CAAcC,CAAAA,CAAyBC,EAAmBC,CAAAA,CAAcC,CAAAA,CAAW,CAC1F,GAAM,CAAE,QAAA,CAAAC,CAAAA,CAAU,OAAA,CAAAC,CAAAA,CAAS,SAAAC,CAAAA,CAAU,UAAA,CAAAC,CAAW,CAAA,CAAIP,EACpD,OAAOQ,mBAAAA,CAAkBP,CAAAA,CAAMC,CAAAA,CAAMC,EAAGC,CAAAA,CAAUC,CAAAA,CAASC,CAAAA,CAAUC,CAAU,CACjF,CAEA,SAASE,CAAAA,CACP9B,CAAAA,CACAgB,EACAR,CAAAA,CACAuB,CAAAA,CACAC,CAAAA,CACM,CACNjB,EAAcf,CAAAA,CAAOgB,CAAAA,CAAKR,CAAAA,CAAK,KAAK,EACpCyB,CAAAA,CAAuBjC,CAAAA,CAAOgB,CAAAA,CAAKe,CAAAA,CAAMC,CAAI,EAC/C,CAIA,SAASE,EAAWlC,CAAAA,CAAmBgB,CAAAA,CAAeR,CAAAA,CAAmB,CACvEO,EAAcf,CAAAA,CAAOgB,CAAAA,CAAKR,CAAAA,CAAK,QAAQ,EACzC,CAEA,SAAS2B,CAAAA,CAAQnC,CAAAA,CAAmBgB,EAAeR,CAAAA,CAAaS,CAAAA,CAAsB,CACpF,IAAMO,EAAIxB,CAAAA,CAAM,OAAA,CAAQ,aAAA,CAClBuB,CAAAA,CAAAA,CAASP,EAAiBhB,CAAAA,CAAM,OAAA,CAAQ,SAAA,EAAawB,CAAAA,CACrDY,EAAcpC,CAAAA,CAAM,kBAAA,CAAmBQ,CAAG,CAAA,EAAG,IAAM,EAAA,CACzDO,CAAAA,CAAcf,CAAAA,CAAOgB,CAAAA,CAAKR,EAAK,KAAA,CAAOS,CAAK,CAAA,CAC3C,IAAMC,EAAW,KAAA,CAAM,IAAA,CAAKlB,CAAAA,CAAM,SAAS,EAC3C,IAAA,IAAWmB,CAAAA,IAAOD,CAAAA,CAAU,CAE1B,GADIC,CAAAA,CAAI,KAAA,GAAU,KAAA,EAASA,CAAAA,CAAI,UAAY,EAAA,EACvC,CAACnB,CAAAA,CAAM,SAAA,CAAU,SAASmB,CAAG,CAAA,CAAG,SAGpC,IAAMhB,EAAIH,CAAAA,CAAM,OAAA,CAAQmB,CAAAA,CAAI,OAAO,EAC7BE,CAAAA,CAASrB,CAAAA,CAAM,UAAA,CAAW,GAAA,CAAImB,EAAI,OAAO,CAAA,CAC3C,CAAChB,CAAAA,EAAK,CAACkB,CAAAA,EACP,CAAClB,CAAAA,CAAE,GAAA,CAAI,SAASiC,CAAW,CAAA,EAAK,CAACjC,CAAAA,CAAE,IAAI,QAAA,CAASiC,CAAW,CAAA,EAC3DhB,CAAAA,CAAcC,EAAQrB,CAAAA,CAAM,UAAA,CAAYuB,CAAAA,CAAMC,CAAC,GAAGL,CAAAA,CAAI,OAAA,CAAQH,CAAAA,CAAKC,CAAK,EAC9E,CACF,CAOA,SAASgB,CAAAA,CACPjC,EACAgB,CAAAA,CACAe,CAAAA,CACAC,CAAAA,CACM,CACN,IAAMR,CAAAA,CAAIxB,CAAAA,CAAM,OAAA,CAAQ,aAAA,CAClBkB,EAAW,KAAA,CAAM,IAAA,CAAKlB,CAAAA,CAAM,SAAS,EAC3C,IAAA,IAAWmB,CAAAA,IAAOD,CAAAA,CAAU,CAE1B,GADIC,CAAAA,CAAI,KAAA,GAAU,KAAA,EAASA,CAAAA,CAAI,UAAY,EAAA,EACvC,CAACnB,CAAAA,CAAM,SAAA,CAAU,SAASmB,CAAG,CAAA,CAAG,SACpC,IAAME,EAASrB,CAAAA,CAAM,UAAA,CAAW,GAAA,CAAImB,CAAAA,CAAI,OAAO,CAAA,CAC/C,GAAI,CAACE,EAAQ,SACb,IAAMgB,CAAAA,CAAWjB,CAAAA,CAAcC,EAAQU,CAAAA,CAAM,CAAA,CAAGP,CAAC,CAAA,CAC3Cc,EAAUlB,CAAAA,CAAcC,CAAAA,CAAQW,CAAAA,CAAM,CAAA,CAAGR,CAAC,CAAA,CAAA,CAC5CL,CAAAA,CAAI,KAAA,GAAU,KAAA,CAAQ,CAACkB,CAAAA,EAAYC,CAAAA,CAAUD,CAAAA,EAAY,CAACC,IAASnB,CAAAA,CAAI,OAAA,CAAQH,CAAG,EACxF,CACF,CAGAuB,mBAAAA,CAAyB,CACvB,OAAA,CAAAT,EACA,UAAA,CAAAI,CAAAA,CACA,eAAA,CAAiBD,CAAAA,CACjB,QAAAE,CACF,CAAC,CAAA,CAIDK,mBAAAA,CAAqB,CACnB,wBAAA,CAAyBxC,CAAAA,CAAmBgB,CAAAA,CAAqB,CAC/D,IAAMQ,CAAAA,CAAIxB,CAAAA,CAAM,OAAA,CAAQ,aAAA,CAClBc,EAAOE,CAAAA,CAAiBhB,CAAAA,CAAM,OAAA,CAAQ,SAAA,CACtCuB,EAAOT,CAAAA,CAAMU,CAAAA,CAIbiB,CAAAA,CAAUzC,CAAAA,CAAM,WAAW,KAAA,CAAMuB,CAAAA,CAAMA,CAAAA,CAAOC,CAAC,EAQrDkB,mBAAAA,CAAcD,CAAAA,CAAS,CAAA,CAAGjB,CAAAA,CAAIhB,GAAQ,CAAA,CAC7BR,CAAAA,CAAM,UAAA,CAAWuB,CAAAA,EAAQf,IAAQ,CAAA,CAAE,CAAA,EAAK,CAAA,EAAM,CAAA,GAAMA,EAAM,EAAA,CAAA,GAC7DR,CAAAA,CAAM,QAAA,CAAS,GAAA,CAAIc,EAAMd,CAAAA,CAAM,OAAA,CAAQ,aAAA,CAAgBQ,CAAG,GAC9DO,CAAAA,CAAcf,CAAAA,CAAOgB,CAAAA,CAAKR,CAAAA,CAAK,QAAQ,CAAA,EACzC,CAAC,CAAA,CAMD,IAAMmC,EAAgB,KAAA,CAAM,IAAA,CAAK3C,CAAAA,CAAM,SAAS,EAChD,IAAA,IAAWmB,CAAAA,IAAOwB,CAAAA,CAAe,CAE/B,GADIxB,CAAAA,CAAI,KAAA,GAAU,QAAA,EAAYA,CAAAA,CAAI,UAAY,EAAA,EAC1C,CAACnB,CAAAA,CAAM,SAAA,CAAU,SAASmB,CAAG,CAAA,CAAG,SACpC,IAAME,EAASrB,CAAAA,CAAM,UAAA,CAAW,GAAA,CAAImB,CAAAA,CAAI,OAAO,CAAA,CAC3CE,CAAAA,EAAUD,CAAAA,CAAcC,CAAAA,CAAQrB,EAAM,UAAA,CAAYuB,CAAAA,CAAMC,CAAC,CAAA,EAAGL,EAAI,OAAA,CAAQH,CAAG,EACjF,CACF,CACF,CAAC,CAAA","file":"observers.cjs","sourcesContent":["// 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 { EcsError } from './internal/errors.js'\nimport { asQueryInternal, ensureQueryRegistered } from './internal/query.js'\nimport type {\n ComponentLike,\n EntityId,\n ObserverEntry,\n ObserverEvent,\n Query,\n QueryMaskBundle,\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 registerComponentObserver(world, component, 'add', handler, opts)\n}\n\nexport function onRemove(\n world: World,\n component: ComponentLike,\n handler: (eid: EntityId) => void,\n opts?: ObserverOptions,\n): () => void {\n return registerComponentObserver(world, component, 'remove', handler, opts)\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 registerComponentObserver(world, component, 'set', handler, opts)\n}\n\n/**\n * Observe a query: `'add'` fires when an entity starts matching, `'remove'`\n * when it stops matching (including on destroy), `'set'` after `setComponent`\n * writes one of the query's `all`/`any` components on a matching entity.\n * `query` may be a raw component array, as for `runQuery`.\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 assertHandler(handler)\n // Register the query (and, for enter/exit queries, its source query and\n // reactive buffer) into this world so dispatch can find it. Unlike\n // `runQuery`, this never drains a reactive buffer as a side effect.\n const q = asQueryInternal(query)\n ensureQueryRegistered(state, q)\n return addObserver(state, event, -1, q.id, handler, opts)\n}\n\n// A non-function handler would be accepted here and then throw from inside\n// every later structural change — after the change was committed but before\n// reactive queries were notified — so it is rejected before any side effect.\nfunction assertHandler(handler: unknown): void {\n if (typeof handler !== 'function') throw new EcsError('aiecsjs: handler must be a function')\n}\n\nfunction registerComponentObserver(\n world: World,\n component: ComponentLike,\n event: ObserverEvent,\n handler: (eid: EntityId, value?: unknown) => void,\n opts: ObserverOptions | undefined,\n): () => void {\n const state = getWorldState(world)\n assertHandler(handler)\n const bit = getOrRegisterComponentBit(state, getComponentInfo(component))\n return addObserver(state, event, bit, -1, handler, opts)\n}\n\n// Push an observer entry; returns its unsubscribe, bound to `opts.signal`.\nfunction addObserver(\n state: WorldState,\n event: ObserverEvent,\n componentBit: number,\n queryId: number,\n handler: (eid: EntityId, value?: unknown) => void,\n opts: ObserverOptions | undefined,\n): () => void {\n const entry: ObserverEntry = { event, componentBit, queryId, handler }\n state.observers.push(entry)\n return bindAbortSignal(() => {\n const idx = state.observers.indexOf(entry)\n if (idx >= 0) state.observers.splice(idx, 1)\n }, opts?.signal)\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\n// Component-level observers of `event` registered on `bit`.\nfunction fireComponent(\n state: WorldState,\n eid: EntityId,\n bit: number,\n event: ObserverEvent,\n value?: unknown,\n): void {\n const snapshot = Array.from(state.observers)\n for (const obs of snapshot) {\n if (obs.event !== event) continue\n if (obs.componentBit !== bit) continue\n if (!state.observers.includes(obs)) continue\n obs.handler(eid, value)\n }\n}\n\n// Whether the entity mask stored in `mask` at word offset `base` matches.\nfunction bundleMatches(bundle: QueryMaskBundle, mask: Uint32Array, base: number, w: number) {\n const { withMask, anyMask, noneMask, anyHasBits } = bundle\n return matchesEntityMask(mask, base, w, withMask, anyMask, noneMask, anyHasBits)\n}\n\nfunction fireAdd(\n state: WorldState,\n eid: EntityId,\n bit: number,\n prev: Uint32Array,\n next: Uint32Array,\n): void {\n fireComponent(state, eid, bit, 'add')\n dispatchQueryObservers(state, eid, prev, next)\n}\n\n// Component-level remove. removeComponent calls this BEFORE writing the new\n// mask, so handlers can still read the outgoing value via getComponent.\nfunction fireRemove(state: WorldState, eid: EntityId, bit: number): void {\n fireComponent(state, eid, bit, 'remove')\n}\n\nfunction fireSet(state: WorldState, eid: EntityId, bit: number, value: unknown): void {\n const w = state.options.maskWordCount\n const base = ((eid as number) & state.options.indexMask) * w\n const componentId = state.componentInfoByBit[bit]?.id ?? -1\n fireComponent(state, eid, bit, 'set', value)\n const snapshot = Array.from(state.observers)\n for (const obs of snapshot) {\n if (obs.event !== 'set' || obs.queryId === -1) continue\n if (!state.observers.includes(obs)) continue\n // Query 'set' fires when the written component is one of the query's\n // `all` / `any` terms AND the entity currently matches the query.\n const q = state.queries[obs.queryId]\n const bundle = state.queryMasks.get(obs.queryId)\n if (!q || !bundle) continue\n if (!q.all.includes(componentId) && !q.any.includes(componentId)) continue\n if (bundleMatches(bundle, state.entityMask, base, w)) obs.handler(eid, value)\n }\n}\n\n// Helper: when a component changes, walk query observers and fire on match\n// transitions — 'add' when the entity starts matching, 'remove' when it stops —\n// whichever structural op (add or remove) caused it. Mirrors the reactive\n// enter/exit path (recordEntityMaskChange). `prev` / `next` are the caller's\n// private mask snapshots, so reentrant handlers cannot skew the comparison.\nfunction dispatchQueryObservers(\n state: WorldState,\n eid: EntityId,\n prev: Uint32Array,\n next: Uint32Array,\n): void {\n const w = state.options.maskWordCount\n const snapshot = Array.from(state.observers)\n for (const obs of snapshot) {\n if (obs.event === 'set' || 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 = bundleMatches(bundle, prev, 0, w)\n const isMatch = bundleMatches(bundle, next, 0, w)\n if (obs.event === 'add' ? !wasMatch && isMatch : wasMatch && !isMatch) obs.handler(eid)\n }\n}\n\n// Wire the dispatch into component.ts\nregisterObserverDispatch({\n fireAdd,\n fireRemove,\n fireRemoveQuery: dispatchQueryObservers,\n fireSet,\n})\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 idx = (eid as number) & state.options.indexMask\n const base = idx * w\n\n // Snapshot the pre-destroy mask so Phase 1 visits a stable bit list even\n // if a handler reentrant-mutates `state.entityMask`.\n const preMask = state.entityMask.slice(base, base + w)\n\n // Phase 1: component-level remove for every bit set at destroy entry that\n // is still set now. A handler that removed a sibling component via\n // removeComponent already fired that component's onRemove; firing it here\n // again would double-report it. An in-flight removeComponent of a bit\n // (whose onRemove handler destroyed the entity) is already firing its\n // onRemove too.\n forEachSetBit(preMask, 0, w, (bit) => {\n if (!((state.entityMask[base + (bit >>> 5)] ?? 0) & (1 << (bit & 31)))) return\n if (state.removing.has(idx * state.options.maxComponents + bit)) return\n fireComponent(state, eid, bit, 'remove')\n })\n\n // Phase 2: query-level remove for any query this entity matches after\n // Phase 1. Reentrant component changes made by Phase 1 handlers went\n // through add/removeComponent, which already dispatched their own query\n // transitions, so the live mask is the state the entity is leaving from.\n const querySnapshot = Array.from(state.observers)\n for (const obs of querySnapshot) {\n if (obs.event !== 'remove' || obs.queryId === -1) continue\n if (!state.observers.includes(obs)) continue\n const bundle = state.queryMasks.get(obs.queryId)\n if (bundle && bundleMatches(bundle, state.entityMask, base, w)) obs.handler(eid)\n }\n },\n})\n"]}
|
package/dist/observers.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { W as World, Q as Query, O as ObserverEvent, E as EntityId, c as ComponentLike } from './types-
|
|
1
|
+
import { W as World, Q as Query, O as ObserverEvent, E as EntityId, c as ComponentLike } from './types-C6TkQ7HW.cjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Options accepted by every observer registration. `signal` aborts the
|
|
@@ -33,6 +33,12 @@ declare function onRemove(world: World, component: ComponentLike, handler: (eid:
|
|
|
33
33
|
* in app code if you need them.
|
|
34
34
|
*/
|
|
35
35
|
declare function onSet<C extends ComponentLike>(world: World, component: C, handler: (eid: EntityId, value: unknown) => void, opts?: ObserverOptions): () => void;
|
|
36
|
+
/**
|
|
37
|
+
* Observe a query: `'add'` fires when an entity starts matching, `'remove'`
|
|
38
|
+
* when it stops matching (including on destroy), `'set'` after `setComponent`
|
|
39
|
+
* writes one of the query's `all`/`any` components on a matching entity.
|
|
40
|
+
* `query` may be a raw component array, as for `runQuery`.
|
|
41
|
+
*/
|
|
36
42
|
declare function observe(world: World, query: Query, event: ObserverEvent, handler: (eid: EntityId) => void, opts?: ObserverOptions): () => void;
|
|
37
43
|
|
|
38
44
|
export { type ObserverOptions, observe, onAdd, onRemove, onSet };
|
package/dist/observers.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { W as World, Q as Query, O as ObserverEvent, E as EntityId, c as ComponentLike } from './types-
|
|
1
|
+
import { W as World, Q as Query, O as ObserverEvent, E as EntityId, c as ComponentLike } from './types-C6TkQ7HW.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Options accepted by every observer registration. `signal` aborts the
|
|
@@ -33,6 +33,12 @@ declare function onRemove(world: World, component: ComponentLike, handler: (eid:
|
|
|
33
33
|
* in app code if you need them.
|
|
34
34
|
*/
|
|
35
35
|
declare function onSet<C extends ComponentLike>(world: World, component: C, handler: (eid: EntityId, value: unknown) => void, opts?: ObserverOptions): () => void;
|
|
36
|
+
/**
|
|
37
|
+
* Observe a query: `'add'` fires when an entity starts matching, `'remove'`
|
|
38
|
+
* when it stops matching (including on destroy), `'set'` after `setComponent`
|
|
39
|
+
* writes one of the query's `all`/`any` components on a matching entity.
|
|
40
|
+
* `query` may be a raw component array, as for `runQuery`.
|
|
41
|
+
*/
|
|
36
42
|
declare function observe(world: World, query: Query, event: ObserverEvent, handler: (eid: EntityId) => void, opts?: ObserverOptions): () => void;
|
|
37
43
|
|
|
38
44
|
export { type ObserverOptions, observe, onAdd, onRemove, onSet };
|
package/dist/observers.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {e}from'./chunk-
|
|
1
|
+
import {d as d$1,e as e$1}from'./chunk-LNZZ4WWC.js';import {Q,E,d,e,i,f as f$1,q as q$1,J}from'./chunk-27JX7WHY.js';function C(n,e){if(!e)return n;if(e.aborted)return n(),()=>{};let o=()=>{n();};e.addEventListener("abort",o,{once:true});let r=false;return ()=>{r||(r=true,e.removeEventListener("abort",o),n());}}function j(n,e,o,r){return y(n,e,"add",o,r)}function D(n,e,o,r){return y(n,e,"remove",o,r)}function H(n,e,o,r){return y(n,e,"set",o,r)}function P(n,e,o,r,i$1){let d=i(n);W(r);let t=d$1(e);return e$1(d,t),A(d,o,-1,t.id,r,i$1)}function W(n){if(typeof n!="function")throw new f$1("aiecsjs: handler must be a function")}function y(n,e,o,r,i$1){let d=i(n);W(r);let t=q$1(d,J(e));return A(d,o,t,-1,r,i$1)}function A(n,e,o,r,i,d){let t={event:e,componentBit:o,queryId:r,handler:i};return n.observers.push(t),C(()=>{let s=n.observers.indexOf(t);s>=0&&n.observers.splice(s,1);},d?.signal)}function f(n,e,o,r,i){let d=Array.from(n.observers);for(let t of d)t.event===r&&t.componentBit===o&&n.observers.includes(t)&&t.handler(e,i);}function v(n,e$1,o,r){let{withMask:i,anyMask:d,noneMask:t,anyHasBits:s}=n;return e(e$1,o,r,i,d,t,s)}function g(n,e,o,r,i){f(n,e,o,"add"),S(n,e,r,i);}function q(n,e,o){f(n,e,o,"remove");}function w(n,e,o,r){let i=n.options.maskWordCount,d=(e&n.options.indexMask)*i,t=n.componentInfoByBit[o]?.id??-1;f(n,e,o,"set",r);let s=Array.from(n.observers);for(let u of s){if(u.event!=="set"||u.queryId===-1||!n.observers.includes(u))continue;let c=n.queries[u.queryId],m=n.queryMasks.get(u.queryId);!c||!m||!c.all.includes(t)&&!c.any.includes(t)||v(m,n.entityMask,d,i)&&u.handler(e,r);}}function S(n,e,o,r){let i=n.options.maskWordCount,d=Array.from(n.observers);for(let t of d){if(t.event==="set"||t.queryId===-1||!n.observers.includes(t))continue;let s=n.queryMasks.get(t.queryId);if(!s)continue;let u=v(s,o,0,i),c=v(s,r,0,i);(t.event==="add"?!u&&c:u&&!c)&&t.handler(e);}}Q({fireAdd:g,fireRemove:q,fireRemoveQuery:S,fireSet:w});E({dispatchDestroyObservers(n,e){let o=n.options.maskWordCount,r=e&n.options.indexMask,i=r*o,d$1=n.entityMask.slice(i,i+o);d(d$1,0,o,s=>{(n.entityMask[i+(s>>>5)]??0)&1<<(s&31)&&(n.removing.has(r*n.options.maxComponents+s)||f(n,e,s,"remove"));});let t=Array.from(n.observers);for(let s of t){if(s.event!=="remove"||s.queryId===-1||!n.observers.includes(s))continue;let u=n.queryMasks.get(s.queryId);u&&v(u,n.entityMask,i,o)&&s.handler(e);}}});export{P as observe,j as onAdd,D as onRemove,H as onSet};//# sourceMappingURL=observers.js.map
|
|
2
2
|
//# sourceMappingURL=observers.js.map
|
package/dist/observers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/observers.ts"],"names":["bindAbortSignal","unsubscribe","signal","onAbort","detached","onAdd","world","component","handler","opts","registerComponentObserver","onRemove","onSet","observe","query","event","state","getWorldState","runQuery","entry","idx","info","getComponentInfo","bit","getOrRegisterComponentBit","fireAdd","eid","snapshot","obs","dispatchQueryObservers","fireRemove","fireSet","value","_prev","_current","w","base","bundle","isMatch","matchesEntityMask","registerObserverDispatch","registerObserversAPI","preMask","forEachSetBit","querySnapshot"],"mappings":"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","file":"observers.js","sourcesContent":["// 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
|
+
{"version":3,"sources":["../src/observers.ts"],"names":["bindAbortSignal","unsubscribe","signal","onAbort","detached","onAdd","world","component","handler","opts","registerComponentObserver","onRemove","onSet","observe","query","event","state","getWorldState","assertHandler","q","asQueryInternal","ensureQueryRegistered","addObserver","EcsError","bit","getOrRegisterComponentBit","getComponentInfo","componentBit","queryId","entry","idx","fireComponent","eid","value","snapshot","obs","bundleMatches","bundle","mask","base","w","withMask","anyMask","noneMask","anyHasBits","matchesEntityMask","fireAdd","prev","next","dispatchQueryObservers","fireRemove","fireSet","componentId","wasMatch","isMatch","registerObserverDispatch","registerObserversAPI","preMask","forEachSetBit","querySnapshot"],"mappings":"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","file":"observers.js","sourcesContent":["// 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 { EcsError } from './internal/errors.js'\nimport { asQueryInternal, ensureQueryRegistered } from './internal/query.js'\nimport type {\n ComponentLike,\n EntityId,\n ObserverEntry,\n ObserverEvent,\n Query,\n QueryMaskBundle,\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 registerComponentObserver(world, component, 'add', handler, opts)\n}\n\nexport function onRemove(\n world: World,\n component: ComponentLike,\n handler: (eid: EntityId) => void,\n opts?: ObserverOptions,\n): () => void {\n return registerComponentObserver(world, component, 'remove', handler, opts)\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 registerComponentObserver(world, component, 'set', handler, opts)\n}\n\n/**\n * Observe a query: `'add'` fires when an entity starts matching, `'remove'`\n * when it stops matching (including on destroy), `'set'` after `setComponent`\n * writes one of the query's `all`/`any` components on a matching entity.\n * `query` may be a raw component array, as for `runQuery`.\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 assertHandler(handler)\n // Register the query (and, for enter/exit queries, its source query and\n // reactive buffer) into this world so dispatch can find it. Unlike\n // `runQuery`, this never drains a reactive buffer as a side effect.\n const q = asQueryInternal(query)\n ensureQueryRegistered(state, q)\n return addObserver(state, event, -1, q.id, handler, opts)\n}\n\n// A non-function handler would be accepted here and then throw from inside\n// every later structural change — after the change was committed but before\n// reactive queries were notified — so it is rejected before any side effect.\nfunction assertHandler(handler: unknown): void {\n if (typeof handler !== 'function') throw new EcsError('aiecsjs: handler must be a function')\n}\n\nfunction registerComponentObserver(\n world: World,\n component: ComponentLike,\n event: ObserverEvent,\n handler: (eid: EntityId, value?: unknown) => void,\n opts: ObserverOptions | undefined,\n): () => void {\n const state = getWorldState(world)\n assertHandler(handler)\n const bit = getOrRegisterComponentBit(state, getComponentInfo(component))\n return addObserver(state, event, bit, -1, handler, opts)\n}\n\n// Push an observer entry; returns its unsubscribe, bound to `opts.signal`.\nfunction addObserver(\n state: WorldState,\n event: ObserverEvent,\n componentBit: number,\n queryId: number,\n handler: (eid: EntityId, value?: unknown) => void,\n opts: ObserverOptions | undefined,\n): () => void {\n const entry: ObserverEntry = { event, componentBit, queryId, handler }\n state.observers.push(entry)\n return bindAbortSignal(() => {\n const idx = state.observers.indexOf(entry)\n if (idx >= 0) state.observers.splice(idx, 1)\n }, opts?.signal)\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\n// Component-level observers of `event` registered on `bit`.\nfunction fireComponent(\n state: WorldState,\n eid: EntityId,\n bit: number,\n event: ObserverEvent,\n value?: unknown,\n): void {\n const snapshot = Array.from(state.observers)\n for (const obs of snapshot) {\n if (obs.event !== event) continue\n if (obs.componentBit !== bit) continue\n if (!state.observers.includes(obs)) continue\n obs.handler(eid, value)\n }\n}\n\n// Whether the entity mask stored in `mask` at word offset `base` matches.\nfunction bundleMatches(bundle: QueryMaskBundle, mask: Uint32Array, base: number, w: number) {\n const { withMask, anyMask, noneMask, anyHasBits } = bundle\n return matchesEntityMask(mask, base, w, withMask, anyMask, noneMask, anyHasBits)\n}\n\nfunction fireAdd(\n state: WorldState,\n eid: EntityId,\n bit: number,\n prev: Uint32Array,\n next: Uint32Array,\n): void {\n fireComponent(state, eid, bit, 'add')\n dispatchQueryObservers(state, eid, prev, next)\n}\n\n// Component-level remove. removeComponent calls this BEFORE writing the new\n// mask, so handlers can still read the outgoing value via getComponent.\nfunction fireRemove(state: WorldState, eid: EntityId, bit: number): void {\n fireComponent(state, eid, bit, 'remove')\n}\n\nfunction fireSet(state: WorldState, eid: EntityId, bit: number, value: unknown): void {\n const w = state.options.maskWordCount\n const base = ((eid as number) & state.options.indexMask) * w\n const componentId = state.componentInfoByBit[bit]?.id ?? -1\n fireComponent(state, eid, bit, 'set', value)\n const snapshot = Array.from(state.observers)\n for (const obs of snapshot) {\n if (obs.event !== 'set' || obs.queryId === -1) continue\n if (!state.observers.includes(obs)) continue\n // Query 'set' fires when the written component is one of the query's\n // `all` / `any` terms AND the entity currently matches the query.\n const q = state.queries[obs.queryId]\n const bundle = state.queryMasks.get(obs.queryId)\n if (!q || !bundle) continue\n if (!q.all.includes(componentId) && !q.any.includes(componentId)) continue\n if (bundleMatches(bundle, state.entityMask, base, w)) obs.handler(eid, value)\n }\n}\n\n// Helper: when a component changes, walk query observers and fire on match\n// transitions — 'add' when the entity starts matching, 'remove' when it stops —\n// whichever structural op (add or remove) caused it. Mirrors the reactive\n// enter/exit path (recordEntityMaskChange). `prev` / `next` are the caller's\n// private mask snapshots, so reentrant handlers cannot skew the comparison.\nfunction dispatchQueryObservers(\n state: WorldState,\n eid: EntityId,\n prev: Uint32Array,\n next: Uint32Array,\n): void {\n const w = state.options.maskWordCount\n const snapshot = Array.from(state.observers)\n for (const obs of snapshot) {\n if (obs.event === 'set' || 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 = bundleMatches(bundle, prev, 0, w)\n const isMatch = bundleMatches(bundle, next, 0, w)\n if (obs.event === 'add' ? !wasMatch && isMatch : wasMatch && !isMatch) obs.handler(eid)\n }\n}\n\n// Wire the dispatch into component.ts\nregisterObserverDispatch({\n fireAdd,\n fireRemove,\n fireRemoveQuery: dispatchQueryObservers,\n fireSet,\n})\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 idx = (eid as number) & state.options.indexMask\n const base = idx * w\n\n // Snapshot the pre-destroy mask so Phase 1 visits a stable bit list even\n // if a handler reentrant-mutates `state.entityMask`.\n const preMask = state.entityMask.slice(base, base + w)\n\n // Phase 1: component-level remove for every bit set at destroy entry that\n // is still set now. A handler that removed a sibling component via\n // removeComponent already fired that component's onRemove; firing it here\n // again would double-report it. An in-flight removeComponent of a bit\n // (whose onRemove handler destroyed the entity) is already firing its\n // onRemove too.\n forEachSetBit(preMask, 0, w, (bit) => {\n if (!((state.entityMask[base + (bit >>> 5)] ?? 0) & (1 << (bit & 31)))) return\n if (state.removing.has(idx * state.options.maxComponents + bit)) return\n fireComponent(state, eid, bit, 'remove')\n })\n\n // Phase 2: query-level remove for any query this entity matches after\n // Phase 1. Reentrant component changes made by Phase 1 handlers went\n // through add/removeComponent, which already dispatched their own query\n // transitions, so the live mask is the state the entity is leaving from.\n const querySnapshot = Array.from(state.observers)\n for (const obs of querySnapshot) {\n if (obs.event !== 'remove' || obs.queryId === -1) continue\n if (!state.observers.includes(obs)) continue\n const bundle = state.queryMasks.get(obs.queryId)\n if (bundle && bundleMatches(bundle, state.entityMask, base, w)) obs.handler(eid)\n }\n },\n})\n"]}
|
package/dist/relations.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
'use strict';var
|
|
1
|
+
'use strict';var chunkNPNWRCB5_cjs=require('./chunk-NPNWRCB5.cjs');function w(r){return {__kind:"relation",__id:chunkNPNWRCB5_cjs.g.relation++,__exclusive:r?.exclusive??false,__hasData:false}}chunkNPNWRCB5_cjs.h.childOf??=w({exclusive:true});var E=chunkNPNWRCB5_cjs.h.childOf;function f(r,a){if(a?.__kind!=="relation")throw new chunkNPNWRCB5_cjs.f("aiecsjs: rel must be a relation");return r.relationStorage.get(a.__id)}function h(r,a){let o=f(r,a);return o||(o={rel:a,exclusive:a.__exclusive?new Int32Array(r.capacity).fill(-1):null,incoming:a.__exclusive?new Map:null,outgoing:new Map,data:new Map},r.relationStorage.set(a.__id,o),o)}function y(r,a,o){let e=r.incoming;if(!e)return;let t=e.get(o);t||(t=new Set,e.set(o,t)),t.add(a);}function R(r,a,o){let e=r.incoming;if(!e)return;let t=e.get(o);t&&(t.delete(a),t.size===0&&e.delete(o));}function T(r,a,o,e,t){let n=chunkNPNWRCB5_cjs.j(r);if(!chunkNPNWRCB5_cjs.z(n,a))throw new chunkNPNWRCB5_cjs.f(`aiecsjs: addRelation on dead source entity ${a}`);if(!chunkNPNWRCB5_cjs.z(n,e))throw new chunkNPNWRCB5_cjs.f(`aiecsjs: addRelation on dead target entity ${e}`);let i=h(n,o),s=a&n.options.indexMask,c=e&n.options.indexMask;if(i.exclusive){if(s>=i.exclusive.length){let u=new Int32Array(Math.max(s+1,i.exclusive.length*2)).fill(-1);u.set(i.exclusive),i.exclusive=u;}let l=i.exclusive[s]??-1;if(l>=0&&l!==c){let u=i.data.get(s);u?.delete(l),u&&u.size===0&&i.data.delete(s),R(i,s,l);}i.exclusive[s]=c,l!==c&&y(i,s,c);}else {let l=i.outgoing.get(s);l||(l=[],i.outgoing.set(s,l)),l.includes(c)||l.push(c);}if(t!==void 0){let l=i.data.get(s);l||(l=new Map,i.data.set(s,l)),l.set(c,t);}}function W(r,a,o,e){let t=chunkNPNWRCB5_cjs.j(r),n=f(t,o);if(!n)return;let i=a&t.options.indexMask,s=e&t.options.indexMask;if(n.exclusive)i<n.exclusive.length&&n.exclusive[i]===s&&(n.exclusive[i]=-1,R(n,i,s));else {let l=n.outgoing.get(i);if(l){let u=l.indexOf(s);u>=0&&l.splice(u,1),l.length===0&&n.outgoing.delete(i);}}let c=n.data.get(i);c&&(c.delete(s),c.size===0&&n.data.delete(i));}function O(r,a,o){let e=chunkNPNWRCB5_cjs.i(r),t=f(e,o);if(!t)return [];let n=a&e.options.indexMask;if(t.exclusive){if(n<t.exclusive.length){let s=t.exclusive[n]??-1;if(s>=0){let c=e.generations[s]??0;return [chunkNPNWRCB5_cjs.s(s,c,e.options)]}}return []}let i=t.outgoing.get(n);return i?i.map(s=>{let c=e.generations[s]??0;return chunkNPNWRCB5_cjs.s(s,c,e.options)}):[]}function j(r,a,o,e){let t=chunkNPNWRCB5_cjs.i(r),n=f(t,o);if(!n)return;let i=a&t.options.indexMask,s=e&t.options.indexMask;return n.data.get(i)?.get(s)}chunkNPNWRCB5_cjs.F((r,a)=>{let o=a&r.options.indexMask;for(let e of r.relationStorage.values()){if(e.exclusive){if(o<e.exclusive.length){let n=e.exclusive[o]??-1;n>=0&&R(e,o,n),e.exclusive[o]=-1;}let t=e.incoming?.get(o);if(t){for(let n of t)n<e.exclusive.length&&(e.exclusive[n]=-1);e.incoming?.delete(o);}}e.outgoing.delete(o);for(let[t,n]of e.outgoing){let i=n.indexOf(o);i>=0&&(n.splice(i,1),n.length===0&&e.outgoing.delete(t));}e.data.delete(o);for(let[t,n]of e.data)n.delete(o),n.size===0&&e.data.delete(t);}});exports.ChildOf=E;exports.addRelation=T;exports.defineRelation=w;exports.getRelationData=j;exports.getRelationTargets=O;exports.removeRelation=W;//# sourceMappingURL=relations.cjs.map
|
|
2
2
|
//# sourceMappingURL=relations.cjs.map
|
package/dist/relations.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/relations.ts"],"names":["nextRelationId","defineRelation","options","ChildOf","getOrCreateStorage","state","rel","storage","linkIncoming","src","tgt","incoming","set","unlinkIncoming","addRelation","world","source","target","data","getWorldState","next","prevTgt","prevInner","list","inner","removeRelation","idx","getRelationTargets","gen","packEid","tIdx","getRelationData","registerRelationsCleanup","eid","e","cur","incomingSet"],"mappings":"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","file":"relations.cjs","sourcesContent":["// aiecsjs/relations — entity-to-entity relations (experimental in 0.1).\n\nimport { packEid, 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<unknown>): 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 incoming: rel.__exclusive ? new Map<number, Set<number>>() : 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\n// --- Exclusive reverse-index (incoming) helpers ---\n// `incoming` mirrors the `exclusive` array: incoming.get(tgt) is the set of\n// source slots whose exclusive[src] === tgt. Every place that writes a non-`-1`\n// value into `exclusive` must call `linkIncoming`, and every place that clears a\n// slot (sets it back to -1) must call `unlinkIncoming`, so the reverse index\n// never drifts from the forward array.\n\nfunction linkIncoming(storage: RelationStorage, src: number, tgt: number): void {\n const incoming = storage.incoming\n if (!incoming) return\n let set = incoming.get(tgt)\n if (!set) {\n set = new Set<number>()\n incoming.set(tgt, set)\n }\n set.add(src)\n}\n\nfunction unlinkIncoming(storage: RelationStorage, src: number, tgt: number): void {\n const incoming = storage.incoming\n if (!incoming) return\n const set = incoming.get(tgt)\n if (!set) return\n set.delete(src)\n if (set.size === 0) incoming.delete(tgt)\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<unknown>)\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 // Exclusive redirect: if this source already pointed at a different target,\n // drop that previous target's data entry so getRelationData stays consistent\n // with getRelationTargets, which reports only the current exclusive target.\n const prevTgt = storage.exclusive[src] ?? -1\n if (prevTgt >= 0 && prevTgt !== tgt) {\n const prevInner = storage.data.get(src)\n prevInner?.delete(prevTgt)\n if (prevInner && prevInner.size === 0) storage.data.delete(src)\n unlinkIncoming(storage, src, prevTgt)\n }\n storage.exclusive[src] = tgt\n if (prevTgt !== tgt) linkIncoming(storage, 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 unlinkIncoming(storage, src, tgt)\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 if (tgt >= 0) {\n const gen = state.generations[tgt] ?? 0\n return [packEid(tgt, gen, state.options)]\n }\n }\n return []\n }\n const list = storage.outgoing.get(src)\n if (!list) return []\n return list.map((tIdx) => {\n const gen = state.generations[tIdx] ?? 0\n return packEid(tIdx, gen, state.options)\n })\n}\n\n/**\n * Read the data payload attached to a relation edge.\n *\n * Returns the `data` value that was supplied to {@link addRelation} for the\n * `(source, rel, target)` triple, or `undefined` when:\n * - the relation has never been stored for this world,\n * - no edge from `source` to `target` via `rel` exists, or\n * - the edge was added without a data argument.\n *\n * **Slot-keying semantic (ABA caveat):** relation storage keys edges by raw\n * entity slot index (`entityId & indexMask`), not by the full packed EntityId\n * that includes the generation counter. This means that if entity A is\n * destroyed and a *different* entity B is later created in the same slot, B\n * will inherit any edges that A had — unless the destroy cleanup hook ran\n * (which it does when `destroyEntity` is called). Callers that cache a\n * source/target EntityId should validate liveness with `entityExists` before\n * calling `getRelationData` if slot recycling is a concern.\n */\nexport function getRelationData<T>(\n world: World,\n source: EntityId,\n rel: Relation<T>,\n target: EntityId,\n): T | undefined {\n const state = getWorldState(world)\n const storage = state.relationStorage.get(rel.__id)\n if (!storage) return undefined\n const src = (source as number) & state.options.indexMask\n const tgt = (target as number) & state.options.indexMask\n return storage.data.get(src)?.get(tgt) as T | undefined\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 // `e` as a SOURCE: clear its own exclusive slot and unlink it from whatever\n // target it currently points at, so the reverse index stays consistent.\n if (e < storage.exclusive.length) {\n const cur = storage.exclusive[e] ?? -1\n if (cur >= 0) unlinkIncoming(storage, e, cur)\n storage.exclusive[e] = -1\n }\n // `e` as a TARGET: clear every source that points at it. Use the reverse\n // index to touch only the actual incoming sources (O(incoming)) instead of\n // scanning the entire `exclusive` capacity (O(capacity)) — the bounded-cost\n // path for large sparse relation tables. Behaviour is identical to the old\n // full scan: every src with exclusive[src] === e is reset to -1.\n const incomingSet = storage.incoming?.get(e)\n if (incomingSet) {\n for (const src of incomingSet) {\n if (src < storage.exclusive.length) storage.exclusive[src] = -1\n }\n storage.incoming?.delete(e)\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/relations.ts"],"names":["defineRelation","options","ids","shared","ChildOf","storageOf","state","rel","EcsError","getOrCreateStorage","storage","linkIncoming","src","tgt","incoming","set","unlinkIncoming","addRelation","world","source","target","data","getWritableState","isAliveInternal","next","prevTgt","prevInner","list","inner","removeRelation","idx","getRelationTargets","getWorldState","gen","packEid","tIdx","getRelationData","registerRelationsCleanup","eid","e","cur","incomingSet"],"mappings":"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","file":"relations.cjs","sourcesContent":["// aiecsjs/relations — entity-to-entity relations (experimental in 0.1).\n\nimport { isAliveInternal, packEid, registerRelationsCleanup } from './internal/entity.js'\nimport { EcsError } from './internal/errors.js'\nimport { ids, shared } from './internal/registry.js'\nimport type { EntityId, Relation, RelationStorage, World, WorldState } from './internal/types.js'\nimport { getWorldState, getWritableState } from './internal/world.js'\n\nexport function defineRelation<T = void>(options?: { exclusive?: boolean }): Relation<T> {\n const id = ids.relation++\n return {\n __kind: 'relation',\n __id: id,\n __exclusive: options?.exclusive ?? false,\n __hasData: false,\n } as Relation<T>\n}\n\n// One ChildOf for every loaded copy of the package (see registry.ts), so a\n// hierarchy built through one copy is visible through another.\nshared.childOf ??= defineRelation({ exclusive: true })\nexport const ChildOf: Relation = shared.childOf\n\n// The relation's storage in this world (undefined until its first edge).\n// Rejects a value that is not a `defineRelation` handle.\nfunction storageOf(state: WorldState, rel: Relation<unknown>): RelationStorage | undefined {\n if (rel?.__kind !== 'relation') throw new EcsError('aiecsjs: rel must be a relation')\n return state.relationStorage.get(rel.__id)\n}\n\nfunction getOrCreateStorage(state: WorldState, rel: Relation<unknown>): RelationStorage {\n let storage = storageOf(state, rel)\n if (storage) return storage\n storage = {\n rel,\n exclusive: rel.__exclusive ? new Int32Array(state.capacity).fill(-1) : null,\n incoming: rel.__exclusive ? new Map<number, Set<number>>() : 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\n// --- Exclusive reverse-index (incoming) helpers ---\n// `incoming` mirrors the `exclusive` array: incoming.get(tgt) is the set of\n// source slots whose exclusive[src] === tgt. Every place that writes a non-`-1`\n// value into `exclusive` must call `linkIncoming`, and every place that clears a\n// slot (sets it back to -1) must call `unlinkIncoming`, so the reverse index\n// never drifts from the forward array.\n\nfunction linkIncoming(storage: RelationStorage, src: number, tgt: number): void {\n const incoming = storage.incoming\n if (!incoming) return\n let set = incoming.get(tgt)\n if (!set) {\n set = new Set<number>()\n incoming.set(tgt, set)\n }\n set.add(src)\n}\n\nfunction unlinkIncoming(storage: RelationStorage, src: number, tgt: number): void {\n const incoming = storage.incoming\n if (!incoming) return\n const set = incoming.get(tgt)\n if (!set) return\n set.delete(src)\n if (set.size === 0) incoming.delete(tgt)\n}\n\n/**\n * Add a relation edge from `source` to `target` (replacing `source`'s current\n * target for an exclusive relation such as `ChildOf`), optionally with a data\n * payload read back by {@link getRelationData}.\n *\n * Throws `EcsError` when `source` or `target` is not alive, mirroring\n * `addComponent`: edges are stored by slot, so an edge to a dead entity would\n * otherwise be inherited by whatever entity is later recycled into that slot.\n * Check `entityExists` first when an endpoint may be stale.\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 = getWritableState(world)\n if (!isAliveInternal(state, source)) {\n throw new EcsError(`aiecsjs: addRelation on dead source entity ${source}`)\n }\n if (!isAliveInternal(state, target)) {\n throw new EcsError(`aiecsjs: addRelation on dead target entity ${target}`)\n }\n const storage = getOrCreateStorage(state, rel as Relation<unknown>)\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 // Exclusive redirect: if this source already pointed at a different target,\n // drop that previous target's data entry so getRelationData stays consistent\n // with getRelationTargets, which reports only the current exclusive target.\n const prevTgt = storage.exclusive[src] ?? -1\n if (prevTgt >= 0 && prevTgt !== tgt) {\n const prevInner = storage.data.get(src)\n prevInner?.delete(prevTgt)\n if (prevInner && prevInner.size === 0) storage.data.delete(src)\n unlinkIncoming(storage, src, prevTgt)\n }\n storage.exclusive[src] = tgt\n if (prevTgt !== tgt) linkIncoming(storage, 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 = getWritableState(world)\n const storage = storageOf(state, rel as Relation<unknown>)\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 unlinkIncoming(storage, src, tgt)\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 = storageOf(state, rel as Relation<unknown>)\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 if (tgt >= 0) {\n const gen = state.generations[tgt] ?? 0\n return [packEid(tgt, gen, state.options)]\n }\n }\n return []\n }\n const list = storage.outgoing.get(src)\n if (!list) return []\n return list.map((tIdx) => {\n const gen = state.generations[tIdx] ?? 0\n return packEid(tIdx, gen, state.options)\n })\n}\n\n/**\n * Read the data payload attached to a relation edge.\n *\n * Returns the `data` value that was supplied to {@link addRelation} for the\n * `(source, rel, target)` triple, or `undefined` when:\n * - the relation has never been stored for this world,\n * - no edge from `source` to `target` via `rel` exists, or\n * - the edge was added without a data argument.\n *\n * **Slot keying:** relation storage keys edges by raw entity slot index\n * (`entityId & indexMask`), not by the full packed EntityId. `addRelation`\n * rejects dead endpoints and `destroyEntity` / `resetWorld` drop every edge of\n * the entities they remove, so storage never holds an edge for a dead entity.\n * A *stale* EntityId whose slot was recycled still addresses the slot's\n * current occupant here, so validate cached ids with `entityExists` (or use\n * `EntityRef`) before reading.\n */\nexport function getRelationData<T>(\n world: World,\n source: EntityId,\n rel: Relation<T>,\n target: EntityId,\n): T | undefined {\n const state = getWorldState(world)\n const storage = storageOf(state, rel as Relation<unknown>)\n if (!storage) return undefined\n const src = (source as number) & state.options.indexMask\n const tgt = (target as number) & state.options.indexMask\n return storage.data.get(src)?.get(tgt) as T | undefined\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 // `e` as a SOURCE: clear its own exclusive slot and unlink it from whatever\n // target it currently points at, so the reverse index stays consistent.\n if (e < storage.exclusive.length) {\n const cur = storage.exclusive[e] ?? -1\n if (cur >= 0) unlinkIncoming(storage, e, cur)\n storage.exclusive[e] = -1\n }\n // `e` as a TARGET: clear every source that points at it. Use the reverse\n // index to touch only the actual incoming sources (O(incoming)) instead of\n // scanning the entire `exclusive` capacity (O(capacity)) — the bounded-cost\n // path for large sparse relation tables. Behaviour is identical to the old\n // full scan: every src with exclusive[src] === e is reset to -1.\n const incomingSet = storage.incoming?.get(e)\n if (incomingSet) {\n for (const src of incomingSet) {\n if (src < storage.exclusive.length) storage.exclusive[src] = -1\n }\n storage.incoming?.delete(e)\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.d.cts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import { R as Relation, W as World, E as EntityId } from './types-
|
|
1
|
+
import { R as Relation, W as World, E as EntityId } from './types-C6TkQ7HW.cjs';
|
|
2
2
|
|
|
3
3
|
declare function defineRelation<T = void>(options?: {
|
|
4
4
|
exclusive?: boolean;
|
|
5
5
|
}): Relation<T>;
|
|
6
6
|
declare const ChildOf: Relation;
|
|
7
|
+
/**
|
|
8
|
+
* Add a relation edge from `source` to `target` (replacing `source`'s current
|
|
9
|
+
* target for an exclusive relation such as `ChildOf`), optionally with a data
|
|
10
|
+
* payload read back by {@link getRelationData}.
|
|
11
|
+
*
|
|
12
|
+
* Throws `EcsError` when `source` or `target` is not alive, mirroring
|
|
13
|
+
* `addComponent`: edges are stored by slot, so an edge to a dead entity would
|
|
14
|
+
* otherwise be inherited by whatever entity is later recycled into that slot.
|
|
15
|
+
* Check `entityExists` first when an endpoint may be stale.
|
|
16
|
+
*/
|
|
7
17
|
declare function addRelation<T>(world: World, source: EntityId, rel: Relation<T>, target: EntityId, data?: T): void;
|
|
8
18
|
declare function removeRelation(world: World, source: EntityId, rel: Relation, target: EntityId): void;
|
|
9
19
|
declare function getRelationTargets(world: World, source: EntityId, rel: Relation): readonly EntityId[];
|
|
@@ -16,14 +26,13 @@ declare function getRelationTargets(world: World, source: EntityId, rel: Relatio
|
|
|
16
26
|
* - no edge from `source` to `target` via `rel` exists, or
|
|
17
27
|
* - the edge was added without a data argument.
|
|
18
28
|
*
|
|
19
|
-
* **Slot
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* calling `getRelationData` if slot recycling is a concern.
|
|
29
|
+
* **Slot keying:** relation storage keys edges by raw entity slot index
|
|
30
|
+
* (`entityId & indexMask`), not by the full packed EntityId. `addRelation`
|
|
31
|
+
* rejects dead endpoints and `destroyEntity` / `resetWorld` drop every edge of
|
|
32
|
+
* the entities they remove, so storage never holds an edge for a dead entity.
|
|
33
|
+
* A *stale* EntityId whose slot was recycled still addresses the slot's
|
|
34
|
+
* current occupant here, so validate cached ids with `entityExists` (or use
|
|
35
|
+
* `EntityRef`) before reading.
|
|
27
36
|
*/
|
|
28
37
|
declare function getRelationData<T>(world: World, source: EntityId, rel: Relation<T>, target: EntityId): T | undefined;
|
|
29
38
|
|
package/dist/relations.d.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import { R as Relation, W as World, E as EntityId } from './types-
|
|
1
|
+
import { R as Relation, W as World, E as EntityId } from './types-C6TkQ7HW.js';
|
|
2
2
|
|
|
3
3
|
declare function defineRelation<T = void>(options?: {
|
|
4
4
|
exclusive?: boolean;
|
|
5
5
|
}): Relation<T>;
|
|
6
6
|
declare const ChildOf: Relation;
|
|
7
|
+
/**
|
|
8
|
+
* Add a relation edge from `source` to `target` (replacing `source`'s current
|
|
9
|
+
* target for an exclusive relation such as `ChildOf`), optionally with a data
|
|
10
|
+
* payload read back by {@link getRelationData}.
|
|
11
|
+
*
|
|
12
|
+
* Throws `EcsError` when `source` or `target` is not alive, mirroring
|
|
13
|
+
* `addComponent`: edges are stored by slot, so an edge to a dead entity would
|
|
14
|
+
* otherwise be inherited by whatever entity is later recycled into that slot.
|
|
15
|
+
* Check `entityExists` first when an endpoint may be stale.
|
|
16
|
+
*/
|
|
7
17
|
declare function addRelation<T>(world: World, source: EntityId, rel: Relation<T>, target: EntityId, data?: T): void;
|
|
8
18
|
declare function removeRelation(world: World, source: EntityId, rel: Relation, target: EntityId): void;
|
|
9
19
|
declare function getRelationTargets(world: World, source: EntityId, rel: Relation): readonly EntityId[];
|
|
@@ -16,14 +26,13 @@ declare function getRelationTargets(world: World, source: EntityId, rel: Relatio
|
|
|
16
26
|
* - no edge from `source` to `target` via `rel` exists, or
|
|
17
27
|
* - the edge was added without a data argument.
|
|
18
28
|
*
|
|
19
|
-
* **Slot
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* calling `getRelationData` if slot recycling is a concern.
|
|
29
|
+
* **Slot keying:** relation storage keys edges by raw entity slot index
|
|
30
|
+
* (`entityId & indexMask`), not by the full packed EntityId. `addRelation`
|
|
31
|
+
* rejects dead endpoints and `destroyEntity` / `resetWorld` drop every edge of
|
|
32
|
+
* the entities they remove, so storage never holds an edge for a dead entity.
|
|
33
|
+
* A *stale* EntityId whose slot was recycled still addresses the slot's
|
|
34
|
+
* current occupant here, so validate cached ids with `entityExists` (or use
|
|
35
|
+
* `EntityRef`) before reading.
|
|
27
36
|
*/
|
|
28
37
|
declare function getRelationData<T>(world: World, source: EntityId, rel: Relation<T>, target: EntityId): T | undefined;
|
|
29
38
|
|
package/dist/relations.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {F,j,s}from'./chunk-
|
|
1
|
+
import {h as h$1,F,g,j as j$1,z,f as f$1,i,s}from'./chunk-27JX7WHY.js';function w(r){return {__kind:"relation",__id:g.relation++,__exclusive:r?.exclusive??false,__hasData:false}}h$1.childOf??=w({exclusive:true});var E=h$1.childOf;function f(r,a){if(a?.__kind!=="relation")throw new f$1("aiecsjs: rel must be a relation");return r.relationStorage.get(a.__id)}function h(r,a){let o=f(r,a);return o||(o={rel:a,exclusive:a.__exclusive?new Int32Array(r.capacity).fill(-1):null,incoming:a.__exclusive?new Map:null,outgoing:new Map,data:new Map},r.relationStorage.set(a.__id,o),o)}function y(r,a,o){let e=r.incoming;if(!e)return;let t=e.get(o);t||(t=new Set,e.set(o,t)),t.add(a);}function R(r,a,o){let e=r.incoming;if(!e)return;let t=e.get(o);t&&(t.delete(a),t.size===0&&e.delete(o));}function T(r,a,o,e,t){let n=j$1(r);if(!z(n,a))throw new f$1(`aiecsjs: addRelation on dead source entity ${a}`);if(!z(n,e))throw new f$1(`aiecsjs: addRelation on dead target entity ${e}`);let i=h(n,o),s=a&n.options.indexMask,c=e&n.options.indexMask;if(i.exclusive){if(s>=i.exclusive.length){let u=new Int32Array(Math.max(s+1,i.exclusive.length*2)).fill(-1);u.set(i.exclusive),i.exclusive=u;}let l=i.exclusive[s]??-1;if(l>=0&&l!==c){let u=i.data.get(s);u?.delete(l),u&&u.size===0&&i.data.delete(s),R(i,s,l);}i.exclusive[s]=c,l!==c&&y(i,s,c);}else {let l=i.outgoing.get(s);l||(l=[],i.outgoing.set(s,l)),l.includes(c)||l.push(c);}if(t!==void 0){let l=i.data.get(s);l||(l=new Map,i.data.set(s,l)),l.set(c,t);}}function W(r,a,o,e){let t=j$1(r),n=f(t,o);if(!n)return;let i=a&t.options.indexMask,s=e&t.options.indexMask;if(n.exclusive)i<n.exclusive.length&&n.exclusive[i]===s&&(n.exclusive[i]=-1,R(n,i,s));else {let l=n.outgoing.get(i);if(l){let u=l.indexOf(s);u>=0&&l.splice(u,1),l.length===0&&n.outgoing.delete(i);}}let c=n.data.get(i);c&&(c.delete(s),c.size===0&&n.data.delete(i));}function O(r,a,o){let e=i(r),t=f(e,o);if(!t)return [];let n=a&e.options.indexMask;if(t.exclusive){if(n<t.exclusive.length){let s$1=t.exclusive[n]??-1;if(s$1>=0){let c=e.generations[s$1]??0;return [s(s$1,c,e.options)]}}return []}let i$1=t.outgoing.get(n);return i$1?i$1.map(s$1=>{let c=e.generations[s$1]??0;return s(s$1,c,e.options)}):[]}function j(r,a,o,e){let t=i(r),n=f(t,o);if(!n)return;let i$1=a&t.options.indexMask,s=e&t.options.indexMask;return n.data.get(i$1)?.get(s)}F((r,a)=>{let o=a&r.options.indexMask;for(let e of r.relationStorage.values()){if(e.exclusive){if(o<e.exclusive.length){let n=e.exclusive[o]??-1;n>=0&&R(e,o,n),e.exclusive[o]=-1;}let t=e.incoming?.get(o);if(t){for(let n of t)n<e.exclusive.length&&(e.exclusive[n]=-1);e.incoming?.delete(o);}}e.outgoing.delete(o);for(let[t,n]of e.outgoing){let i=n.indexOf(o);i>=0&&(n.splice(i,1),n.length===0&&e.outgoing.delete(t));}e.data.delete(o);for(let[t,n]of e.data)n.delete(o),n.size===0&&e.data.delete(t);}});export{E as ChildOf,T as addRelation,w as defineRelation,j as getRelationData,O as getRelationTargets,W as removeRelation};//# sourceMappingURL=relations.js.map
|
|
2
2
|
//# sourceMappingURL=relations.js.map
|