From 5d125167ccabb05e0eaa7ff983205128f4ca8ce9 Mon Sep 17 00:00:00 2001 From: hypercross Date: Tue, 2 Jun 2026 06:39:07 +0800 Subject: [PATCH] refactor: make getRelatedTo return an iterator Convert `getRelatedTo` from returning an array to returning an `IterableIterator`. This improves memory efficiency by yielding entities lazily instead of allocating a new array on every call. --- src/bt/runner.ts | 24 ++++++++++++------------ src/world.ts | 15 ++++++++++----- test/relationships.test.ts | 18 +++++++++--------- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/bt/runner.ts b/src/bt/runner.ts index f2386e7..4735574 100644 --- a/src/bt/runner.ts +++ b/src/bt/runner.ts @@ -52,8 +52,8 @@ function clearSubtree(world: World, entity: Entity): void { } } -function childrenOf(world: World, parent: Entity): Entity[] { - return world.getRelatedTo(parent, ChildOf); +function* childrenOf(world: World, parent: Entity): IterableIterator { + yield* world.getRelatedTo(parent, ChildOf); } function parentOf(world: World, child: Entity): Entity | null { @@ -230,10 +230,9 @@ export class TaskRunner { } private _executeRandom(entity: Entity): void { - const children = childrenOf(this._world, entity); - - // Check if any child already reached a terminal - for (const child of children) { + // Single pass: check terminals and collect eligible children + const eligible: Entity[] = []; + for (const child of childrenOf(this._world, entity)) { if (isTerminal(this._world, child)) { const status = terminalStatus(this._world, child)!; this._finish( @@ -246,13 +245,14 @@ export class TaskRunner { ); return; } + if ( + !this._world.has(child, Running) && + !this._world.has(child, Scheduled) + ) { + eligible.push(child); + } } - // Pick a random child that isn't already running - const eligible = children.filter( - (c) => !this._world.has(c, Running) && !this._world.has(c, Scheduled), - ); - if (eligible.length > 0) { const pick = eligible[Math.floor(Math.random() * eligible.length)]; this._world.add(pick, Scheduled); @@ -261,7 +261,7 @@ export class TaskRunner { } private _executeRepeat(entity: Entity): void { - const children = childrenOf(this._world, entity); + const children = [...childrenOf(this._world, entity)]; // Repeat expects exactly one child if (children.length === 0) return; diff --git a/src/world.ts b/src/world.ts index b04f2c8..e4756b1 100644 --- a/src/world.ts +++ b/src/world.ts @@ -388,17 +388,22 @@ export class World { /** * Get all source entities that point to `target` via this relationship. */ - getRelatedTo(target: Entity, rel: RelationshipDef): Entity[] { + *getRelatedTo( + target: Entity, + rel: RelationshipDef, + ): IterableIterator { const ti = entityIndex(target); - if (!this._isAlive(ti, target)) return []; + if (!this._isAlive(ti, target)) return; const rev = this._relReverse.get(rel._key); - if (!rev) return []; + if (!rev) return; const sources = rev.get(ti); - if (!sources) return []; + if (!sources) return; - return [...sources].map((si) => makeEntity(si, this._generations[si])); + for (const si of sources) { + yield makeEntity(si, this._generations[si]); + } } /** diff --git a/test/relationships.test.ts b/test/relationships.test.ts index d2f93bb..33340a4 100644 --- a/test/relationships.test.ts +++ b/test/relationships.test.ts @@ -55,7 +55,7 @@ describe("Relationships", () => { world.relate(a, ChildOf, parent); world.relate(b, ChildOf, parent); - const children = world.getRelatedTo(parent, ChildOf); + const children = [...world.getRelatedTo(parent, ChildOf)]; expect(children).toHaveLength(2); expect(children).toContain(a); expect(children).toContain(b); @@ -63,7 +63,7 @@ describe("Relationships", () => { it("getRelatedTo returns empty when no edges", () => { const e = world.spawn(); - expect(world.getRelatedTo(e, ChildOf)).toEqual([]); + expect([...world.getRelatedTo(e, ChildOf)]).toEqual([]); }); it("unrelate removes the relationship", () => { @@ -74,7 +74,7 @@ describe("Relationships", () => { world.unrelate(child, ChildOf); expect(world.getRelated(child, ChildOf)).toBeUndefined(); - expect(world.getRelatedTo(parent, ChildOf)).toEqual([]); + expect([...world.getRelatedTo(parent, ChildOf)]).toEqual([]); }); it("unrelate is idempotent", () => { @@ -89,13 +89,13 @@ describe("Relationships", () => { world.relate(a, ChildOf, b); expect(world.getRelated(a, ChildOf)).toBe(b); - expect(world.getRelatedTo(b, ChildOf)).toContain(a); + expect([...world.getRelatedTo(b, ChildOf)]).toContain(a); world.relate(a, ChildOf, c); expect(world.getRelated(a, ChildOf)).toBe(c); // a should no longer point to b - expect(world.getRelatedTo(b, ChildOf)).toEqual([]); - expect(world.getRelatedTo(c, ChildOf)).toContain(a); + expect([...world.getRelatedTo(b, ChildOf)]).toEqual([]); + expect([...world.getRelatedTo(c, ChildOf)]).toContain(a); }); }); @@ -243,7 +243,7 @@ describe("Destroy cleanup", () => { world.destroy(child); expect(world.getRelated(child, ChildOf)).toBeUndefined(); - expect(world.getRelatedTo(parent, ChildOf)).toEqual([]); + expect([...world.getRelatedTo(parent, ChildOf)]).toEqual([]); }); it("removes edges when target is destroyed", () => { @@ -252,7 +252,7 @@ describe("Destroy cleanup", () => { world.relate(child, ChildOf, parent); world.destroy(parent); - expect(world.getRelatedTo(parent, ChildOf)).toEqual([]); + expect([...world.getRelatedTo(parent, ChildOf)]).toEqual([]); expect(world.getRelated(child, ChildOf)).toBeUndefined(); }); @@ -376,6 +376,6 @@ describe("Dead entity safety", () => { it("getRelatedTo returns empty for dead entity", () => { const e = world.spawn(); world.destroy(e); - expect(world.getRelatedTo(e, ChildOf)).toEqual([]); + expect([...world.getRelatedTo(e, ChildOf)]).toEqual([]); }); });