Skip to content

Commit 3fcdd3b

Browse files
committed
feat(world): add getOptional method for safe component access
1 parent a5e5fbf commit 3fcdd3b

File tree

3 files changed

+133
-2
lines changed

3 files changed

+133
-2
lines changed

src/archetype.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export class Archetype {
107107
// Add component data for regular components (those in the archetype signature)
108108
for (const componentType of this.componentTypes) {
109109
const data = componentData.get(componentType);
110-
this.getComponentData(componentType).push(data === undefined ? MISSING_COMPONENT : data);
110+
this.getComponentData(componentType).push(!componentData.has(componentType) ? MISSING_COMPONENT : data);
111111
}
112112

113113
// Add dontFragment relations separately
@@ -316,7 +316,10 @@ export class Archetype {
316316
// First check if it's in the archetype signature
317317
if (this.componentTypes.includes(componentType)) {
318318
const data = this.getComponentData(componentType)[index]!;
319-
return data === MISSING_COMPONENT ? (undefined as T) : data;
319+
if (data === MISSING_COMPONENT) {
320+
throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
321+
}
322+
return data as T;
320323
}
321324

322325
// Check dontFragment relations
@@ -329,6 +332,36 @@ export class Archetype {
329332
}
330333
}
331334

335+
/**
336+
* Get optional component data for a specific entity and component type
337+
* @param entityId The entity
338+
* @param componentType The component type
339+
* @returns { value: T } if component exists, undefined otherwise
340+
*/
341+
getOptional<T>(entityId: EntityId, componentType: EntityId<T>): { value: T } | undefined {
342+
const index = this.entityToIndex.get(entityId);
343+
if (index === undefined) {
344+
throw new Error(`Entity ${entityId} is not in this archetype`);
345+
}
346+
347+
// First check if it's in the archetype signature
348+
if (this.componentTypes.includes(componentType)) {
349+
const data = this.getComponentData(componentType)[index]!;
350+
if (data === MISSING_COMPONENT) {
351+
return undefined;
352+
}
353+
return { value: data as T };
354+
}
355+
356+
// Check dontFragment relations
357+
const dontFragmentData = this.dontFragmentRelations.get(entityId);
358+
if (dontFragmentData && dontFragmentData.has(componentType)) {
359+
return { value: dontFragmentData.get(componentType) };
360+
}
361+
362+
return undefined;
363+
}
364+
332365
/**
333366
* Set component data for a specific entity and component type
334367
* @param entityId The entity

src/get-optional.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, expect, it } from "bun:test";
2+
import { component, relation } from "./entity";
3+
import { World } from "./world";
4+
5+
describe("World.getOptional", () => {
6+
it("should return { value: T } when component exists", () => {
7+
const world = new World();
8+
const PositionId = component<{ x: number; y: number }>();
9+
const entity = world.new();
10+
world.set(entity, PositionId, { x: 10, y: 20 });
11+
world.sync();
12+
13+
const result = world.getOptional(entity, PositionId);
14+
expect(result).toEqual({ value: { x: 10, y: 20 } });
15+
});
16+
17+
it("should return undefined when component does not exist", () => {
18+
const world = new World();
19+
const PositionId = component<{ x: number; y: number }>();
20+
const VelocityId = component<{ x: number; y: number }>();
21+
const entity = world.new();
22+
world.set(entity, PositionId, { x: 10, y: 20 });
23+
world.sync();
24+
25+
const result = world.getOptional(entity, VelocityId);
26+
expect(result).toBeUndefined();
27+
});
28+
29+
it("should distinguish between component value being undefined and component not existing", () => {
30+
const world = new World();
31+
const UndefinedComponent = component<undefined>();
32+
const entity = world.new();
33+
world.set(entity, UndefinedComponent, undefined);
34+
world.sync();
35+
36+
// Exists with undefined value
37+
expect(world.getOptional(entity, UndefinedComponent)).toEqual({ value: undefined });
38+
39+
// Not existing
40+
const Other = component<number>();
41+
expect(world.getOptional(entity, Other)).toBeUndefined();
42+
});
43+
44+
it("should throw error when entity does not exist", () => {
45+
const world = new World();
46+
const PositionId = component<{ x: number; y: number }>();
47+
const entity = 1234 as any; // non-existent entity
48+
49+
expect(() => world.getOptional(entity, PositionId)).toThrow("Entity 1234 does not exist");
50+
});
51+
52+
it("should return undefined for wildcard relations", () => {
53+
const world = new World();
54+
const Rel = component<number>();
55+
const target = world.new();
56+
const entity = world.new();
57+
world.set(entity, relation(Rel, target), 100);
58+
world.sync();
59+
60+
const wildcard = relation(Rel, "*");
61+
expect(world.getOptional(entity, wildcard as any)).toBeUndefined();
62+
});
63+
64+
it("should work with dontFragment relations", () => {
65+
const world = new World();
66+
const DFRel = component<number>({ dontFragment: true });
67+
const target = world.new();
68+
const entity = world.new();
69+
world.set(entity, relation(DFRel, target), 42);
70+
world.sync();
71+
72+
const relId = relation(DFRel, target);
73+
expect(world.getOptional(entity, relId)).toEqual({ value: 42 });
74+
75+
const otherTarget = world.new();
76+
const otherRelId = relation(DFRel, otherTarget);
77+
expect(world.getOptional(entity, otherRelId)).toBeUndefined();
78+
});
79+
});

src/world.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,25 @@ export class World {
401401
return archetype.get(entityId, componentType);
402402
}
403403

404+
/**
405+
* Get optional component data for a specific entity and component type
406+
* @param entityId The entity
407+
* @param componentType The component type
408+
* @returns { value: T } if component exists, undefined otherwise
409+
*/
410+
getOptional<T>(entityId: EntityId, componentType: EntityId<T>): { value: T } | undefined {
411+
const archetype = this.entityToArchetype.get(entityId);
412+
if (!archetype) {
413+
throw new Error(`Entity ${entityId} does not exist`);
414+
}
415+
416+
if (isWildcardRelationId(componentType)) {
417+
return undefined;
418+
}
419+
420+
return archetype.getOptional(entityId, componentType);
421+
}
422+
404423
/**
405424
* Register a lifecycle hook for component or wildcard relation events
406425
*/

0 commit comments

Comments
 (0)