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.
This commit is contained in:
hypercross 2026-06-02 06:39:07 +08:00
parent 2469cdc7cb
commit 5d125167cc
3 changed files with 31 additions and 26 deletions

View File

@ -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<Entity> {
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;

View File

@ -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<Entity> {
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]);
}
}
/**

View File

@ -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([]);
});
});