diff --git a/src/observable/observe.ts b/src/observable/observe.ts index eb7f4f2..6001016 100644 --- a/src/observable/observe.ts +++ b/src/observable/observe.ts @@ -3,6 +3,7 @@ import type { Query } from "../query"; import type { Entity } from "../entity"; import type { WorldEvent, QueryUpdate, RelationshipUpdate } from "./events"; import type { RelationshipDef } from "../relationship"; +import type { ComponentDef } from "../component"; // ── Internal state ─────────────────────────────────── interface QueryObserverState { @@ -24,6 +25,9 @@ export class ObservableLayer { private _observers: QueryObserverState[] = []; private _relObservers: RelationshipObserverState[] = []; + // ── Observer index: component key → observers that care ── + private _compIndex = new Map>(); + // ── Query observers ───────────────────────────── observe(query: Query): Subject { @@ -38,6 +42,7 @@ export class ObservableLayer { subject: new Subject(), }; this._observers.push(state); + this._indexObserver(state); return state.subject; } @@ -83,24 +88,100 @@ export class ObservableLayer { ): void { this.events$.next(event); - for (const o of this._observers) { - this._updateObserver(o, event, queryMatches); - } + this._dispatchToObservers(event, queryMatches); + for (const o of this._relObservers) { this._updateRelObserver(o, event); } } - // ── Private ────────────────────────────────────── + // ── Private: observer indexing ─────────────────── + + /** Add an observer to the component index. */ + private _indexObserver(state: QueryObserverState): void { + for (const def of state.query.with) { + this._addToIndex(def._key, state); + } + for (const def of state.query.not) { + this._addToIndex(def._key, state); + } + // Also index under a well-known symbol for spawn/destroy events + // (those always fan out to all observers). + this._addToIndex(ANY_KEY, state); + } + + /** Remove an observer from the component index. */ + private _unindexObserver(state: QueryObserverState): void { + for (const def of state.query.with) { + this._remFromIndex(def._key, state); + } + for (const def of state.query.not) { + this._remFromIndex(def._key, state); + } + this._remFromIndex(ANY_KEY, state); + } + + private _addToIndex(key: symbol, state: QueryObserverState): void { + let set = this._compIndex.get(key); + if (!set) { + set = new Set(); + this._compIndex.set(key, set); + } + set.add(state); + } + + private _remFromIndex(key: symbol, state: QueryObserverState): void { + const set = this._compIndex.get(key); + if (!set) return; + set.delete(state); + if (set.size === 0) this._compIndex.delete(key); + } + + /** Dispatch to only the relevant observers. */ + private _dispatchToObservers( + event: WorldEvent, + queryMatches: (query: Query, e: Entity) => boolean, + ): void { + if (!("entity" in event)) return; + + // Determine which component keys are relevant + let keys: symbol[] = []; + + switch (event.type) { + case "spawned": + case "destroyed": + // Check all observers (via the ANY_KEY set) + keys = [ANY_KEY]; + break; + + case "componentAdded": + case "componentRemoved": + case "componentChanged": + keys = [event.component._key]; + break; + } + + // Collect unique observers to update (deduplicate across keys) + const seen = new Set(); + for (const key of keys) { + const set = this._compIndex.get(key); + if (!set) continue; + for (const o of set) { + if (!seen.has(o)) { + seen.add(o); + this._updateObserver(o, event, queryMatches); + } + } + } + } + + // ── Private: observer update logic ─────────────── private _updateObserver( obs: QueryObserverState, event: WorldEvent, queryMatches: (query: Query, e: Entity) => boolean, ): void { - // Only entity-bearing events affect queries - if (!("entity" in event)) return; - const e = event.entity!; const wasMatched = obs.matched.has(e); const nowMatches = queryMatches(obs.query, e); @@ -167,17 +248,12 @@ export class ObservableLayer { } case "destroyed": { - // World emits relationshipRemoved for each edge before destroy, - // so those fire first. Then we arrive here — just clean up. const removed: { source: Entity; target: Entity }[] = []; for (const key of obs.edges) { const [si, ti] = key.split(":").map(Number); const idx = event.entity & 0xfffff; if (si === idx || ti === idx) { - removed.push({ - source: si as Entity, - target: ti as Entity, - }); + removed.push({ source: si as Entity, target: ti as Entity }); } } for (const r of removed) { @@ -199,6 +275,7 @@ export class ObservableLayer { o.matched.clear(); } this._observers = []; + this._compIndex.clear(); for (const o of this._relObservers) { o.subject.complete(); @@ -211,6 +288,7 @@ export class ObservableLayer { this.events$.complete(); for (const o of this._observers) o.subject.complete(); this._observers = []; + this._compIndex.clear(); for (const o of this._relObservers) o.subject.complete(); this._relObservers = []; } @@ -218,6 +296,9 @@ export class ObservableLayer { // ── Helpers ───────────────────────────────────── +/** Sentinel key for observers that must be notified on spawn/destroy. */ +const ANY_KEY = Symbol("any"); + function queriesEqual(a: Query, b: Query): boolean { if (a.with.length !== b.with.length) return false; if (a.not.length !== b.not.length) return false;