Skip to content

Commit 9f82912

Browse files
committed
refactor(entity): enhance ID type handling and relations
- Updated EntityId type to include type information for better type safety. - Introduced RelationId type to unify relation ID handling. - Refactored relation function to accept ComponentId and improve type checks. - Enhanced ID type detection functions for better clarity and accuracy. - Adjusted tests to reflect changes in ID handling and ensure correctness.
1 parent 0b0a42a commit 9f82912

File tree

4 files changed

+68
-49
lines changed

4 files changed

+68
-49
lines changed

src/archetype.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { EntityId, WildcardRelationId } from "./entity";
2-
import { decodeRelationId, getIdType } from "./entity";
1+
import type { EntityId, RelationId, WildcardRelationId } from "./entity";
2+
import { decodeRelationId, getDetailedIdType, getIdType, isWildcardRelationId } from "./entity";
33
import type { ComponentTuple } from "./types";
44
import { getOrComputeCache } from "./utils";
55

@@ -158,20 +158,20 @@ export class Archetype {
158158
}
159159
}
160160

161-
if (getIdType(componentType) === "wildcard-relation") {
161+
if (isWildcardRelationId(componentType)) {
162162
const decoded = decodeRelationId(componentType);
163163
const componentId = decoded.componentId;
164164
const relations: [EntityId<unknown>, any][] = [];
165165

166166
for (const relType of this.componentTypes) {
167-
const relDecoded = decodeRelationId(relType);
167+
const relDetailed = getDetailedIdType(relType);
168168
if (
169-
relDecoded.componentId === componentId &&
170-
(getIdType(relType) === "entity-relation" || getIdType(relType) === "component-relation")
169+
(relDetailed.type === "entity-relation" || relDetailed.type === "component-relation") &&
170+
relDetailed.componentId === componentId
171171
) {
172172
const dataArray = this.componentData.get(relType);
173173
if (dataArray && dataArray[index] !== undefined) {
174-
relations.push([relDecoded.targetId, dataArray[index]]);
174+
relations.push([relDetailed.targetId, dataArray[index]]);
175175
}
176176
}
177177
}
@@ -258,17 +258,15 @@ export class Archetype {
258258
// For wildcard relations, cache the matching relation types
259259
// For regular components, cache the data array reference
260260
return componentTypes.map((compType) => {
261-
if (getIdType(compType) === "wildcard-relation") {
262-
// Decode the wildcard relation to get the component ID
263-
const decoded = decodeRelationId(compType);
264-
const componentId = decoded.componentId;
261+
const detailedType = getDetailedIdType(compType);
262+
if (detailedType.type === "wildcard-relation") {
263+
const componentId = detailedType.componentId;
265264

266265
// Find all concrete relation componentTypes in this archetype that match the wildcard
267266
const matchingRelations = this.componentTypes.filter((ct) => {
268-
const ctType = getIdType(ct);
269-
if (ctType !== "entity-relation" && ctType !== "component-relation") return false;
270-
const decodedCt = decodeRelationId(ct);
271-
return decodedCt.componentId === componentId;
267+
const detailedCt = getDetailedIdType(ct);
268+
if (detailedCt.type !== "entity-relation" && detailedCt.type !== "component-relation") return false;
269+
return detailedCt.componentId === componentId;
272270
});
273271

274272
return matchingRelations;
@@ -291,7 +289,7 @@ export class Archetype {
291289
for (const relType of matchingRelations) {
292290
const dataArray = this.componentData.get(relType);
293291
if (dataArray && dataArray[entityIndex] !== undefined) {
294-
const decodedRel = decodeRelationId(relType);
292+
const decodedRel = decodeRelationId(relType as RelationId<any>);
295293
relations.push([decodedRel.targetId, dataArray[entityIndex]]);
296294
}
297295
}

src/entity.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from "bun:test";
2-
import type { EntityId } from "./entity";
2+
import type { ComponentId, EntityId } from "./entity";
33
import {
44
COMPONENT_ID_MAX,
55
ComponentIdManager,
@@ -83,7 +83,7 @@ describe("Entity ID System", () => {
8383

8484
it("should reject invalid relation creation", () => {
8585
const entId = createEntityId(ENTITY_ID_START);
86-
expect(() => relation(1024 as EntityId, entId)).toThrow(); // invalid component id
86+
expect(() => relation(1024 as ComponentId, entId)).toThrow(); // invalid component id
8787
expect(() => relation(createComponentId(5), -1 as EntityId)).toThrow(); // invalid target id
8888
expect(() => relation(createComponentId(5), relation(createComponentId(1), createEntityId(1025)))).toThrow(); // relation as target
8989
});

src/entity.ts

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
declare const __entityIdBrand: unique symbol;
55

66
/**
7-
* Brand for wildcard relation IDs
7+
* Brand for EntityId type information
88
*/
9-
declare const __wildcardRelationBrand: unique symbol;
9+
declare const __entityIdTypeBrand: unique symbol;
1010

1111
/**
1212
* Entity ID type for ECS architecture
@@ -15,9 +15,16 @@ declare const __wildcardRelationBrand: unique symbol;
1515
* - Entity IDs: 1024+
1616
* - Relation IDs: negative numbers encoding component and entity associations
1717
*/
18-
export type EntityId<T = void> = number & { readonly [__entityIdBrand]: T };
18+
export type EntityId<T = void, U = unknown> = number & {
19+
readonly [__entityIdBrand]: T;
20+
readonly [__entityIdTypeBrand]: U;
21+
};
1922

20-
export type WildcardRelationId<T = void> = EntityId<T> & { readonly [__wildcardRelationBrand]: true };
23+
export type ComponentId<T = void> = EntityId<T, "component">;
24+
export type EntityRelationId<T = void> = EntityId<T, "entity-relation">;
25+
export type ComponentRelationId<T = void> = EntityId<T, "component-relation">;
26+
export type WildcardRelationId<T = void> = EntityId<T, "wildcard-relation">;
27+
export type RelationId<T = void> = EntityRelationId<T> | ComponentRelationId<T> | WildcardRelationId<T>;
2128

2229
/**
2330
* Constants for ID ranges
@@ -36,11 +43,11 @@ export const WILDCARD_TARGET_ID = 0;
3643
* Create a component ID
3744
* @param id Component identifier (1-1023)
3845
*/
39-
export function createComponentId<T = void>(id: number): EntityId<T> {
46+
export function createComponentId<T = void>(id: number): ComponentId<T> {
4047
if (id < 1 || id > COMPONENT_ID_MAX) {
4148
throw new Error(`Component ID must be between 1 and ${COMPONENT_ID_MAX}`);
4249
}
43-
return id as EntityId<T>;
50+
return id as ComponentId<T>;
4451
}
4552

4653
/**
@@ -57,16 +64,24 @@ export function createEntityId(id: number): EntityId {
5764
/**
5865
* Type for relation ID based on component and target types
5966
*/
60-
type RelationIdType<T, U> = U extends void ? EntityId<T> : T extends void ? EntityId<U> : EntityId<never>;
67+
// type RelationIdType<T, U> = U extends void ? EntityId<T> : T extends void ? EntityId<U> : EntityId<never>;
68+
type RelationIdType<T, R> =
69+
R extends ComponentId<infer U>
70+
? U extends void
71+
? ComponentRelationId<T>
72+
: ComponentRelationId<T & U>
73+
: R extends EntityId<any>
74+
? EntityRelationId<T>
75+
: never;
6176

6277
/**
6378
* Create a relation ID by associating a component with another ID (entity or component)
6479
* @param componentId The component ID (0-1023)
6580
* @param targetId The target ID (entity, component, or '*' for wildcard)
6681
*/
67-
export function relation<T>(componentId: EntityId<T>, targetId: "*"): WildcardRelationId<T>;
68-
export function relation<T, U>(componentId: EntityId<T>, targetId: EntityId<U>): RelationIdType<T, U>;
69-
export function relation<T>(componentId: EntityId<T>, targetId: EntityId<any> | "*"): EntityId<any> {
82+
export function relation<T>(componentId: ComponentId<T>, targetId: "*"): WildcardRelationId<T>;
83+
export function relation<T, R extends EntityId<any>>(componentId: ComponentId<T>, targetId: R): RelationIdType<T, R>;
84+
export function relation<T>(componentId: ComponentId<T>, targetId: EntityId<any> | "*"): EntityId<any> {
7085
if (!isComponentId(componentId)) {
7186
throw new Error("First argument must be a valid component ID");
7287
}
@@ -88,26 +103,26 @@ export function relation<T>(componentId: EntityId<T>, targetId: EntityId<any> |
88103
/**
89104
* Check if an ID is a component ID
90105
*/
91-
export function isComponentId(id: EntityId<any>): boolean {
106+
export function isComponentId<T>(id: EntityId<T>): id is ComponentId<T> {
92107
return id >= 1 && id <= COMPONENT_ID_MAX;
93108
}
94109

95110
/**
96111
* Check if an ID is an entity ID
97112
*/
98-
export function isEntityId(id: EntityId<any>): boolean {
113+
export function isEntityId<T>(id: EntityId<T>): id is EntityId<T> {
99114
return id >= ENTITY_ID_START;
100115
}
101116

102117
/**
103118
* Check if an ID is a relation ID
104119
*/
105-
export function isRelationId<T>(id: EntityId<T>): boolean {
120+
export function isRelationId<T>(id: EntityId<T>): id is RelationId<T> {
106121
return id < 0;
107122
}
108123

109124
/**
110-
* Check if a entity ID is a wildcard relation id
125+
* Check if an ID is a wildcard relation id
111126
*/
112127
export function isWildcardRelationId<T>(id: EntityId<T>): id is WildcardRelationId<T> {
113128
if (!isRelationId(id)) {
@@ -123,8 +138,8 @@ export function isWildcardRelationId<T>(id: EntityId<T>): id is WildcardRelation
123138
* @param relationId The relation ID (must be negative)
124139
* @returns Object with componentId, targetId, and relation type
125140
*/
126-
export function decodeRelationId(relationId: EntityId<any>): {
127-
componentId: EntityId<any>;
141+
export function decodeRelationId(relationId: RelationId<any>): {
142+
componentId: ComponentId<any>;
128143
targetId: EntityId<any>;
129144
type: "entity" | "component" | "wildcard";
130145
} {
@@ -133,8 +148,8 @@ export function decodeRelationId(relationId: EntityId<any>): {
133148
}
134149
const absId = -relationId;
135150

136-
const componentId = Math.floor(absId / RELATION_SHIFT) as EntityId;
137-
const targetId = (absId % RELATION_SHIFT) as EntityId;
151+
const componentId = Math.floor(absId / RELATION_SHIFT) as ComponentId<any>;
152+
const targetId = (absId % RELATION_SHIFT) as EntityId<any>;
138153

139154
// Determine type based on targetId range
140155
if (targetId === WILDCARD_TARGET_ID) {
@@ -188,11 +203,17 @@ export function getIdType(
188203
* @param id The EntityId to analyze
189204
* @returns Detailed type information including relation subtypes
190205
*/
191-
export function getDetailedIdType(id: EntityId<any>): {
192-
type: "component" | "entity" | "entity-relation" | "component-relation" | "wildcard-relation" | "invalid";
193-
componentId?: EntityId<any>;
194-
targetId?: EntityId<any>;
195-
} {
206+
export function getDetailedIdType(id: EntityId<any>):
207+
| {
208+
type: "component" | "entity" | "invalid";
209+
componentId?: never;
210+
targetId?: never;
211+
}
212+
| {
213+
type: "entity-relation" | "component-relation" | "wildcard-relation";
214+
componentId: ComponentId<any>;
215+
targetId: EntityId<any>;
216+
} {
196217
if (isComponentId(id)) {
197218
return { type: "component" };
198219
}
@@ -351,13 +372,13 @@ export class ComponentIdManager {
351372
* Allocate a new component ID
352373
* Increments counter sequentially from 1
353374
*/
354-
allocate<T = void>(): EntityId<T> {
375+
allocate<T = void>(): ComponentId<T> {
355376
if (this.nextId > COMPONENT_ID_MAX) {
356377
throw new Error(`Component ID overflow: maximum ${COMPONENT_ID_MAX} components allowed`);
357378
}
358379
const id = this.nextId;
359380
this.nextId++;
360-
return id as EntityId<T>;
381+
return id as ComponentId<T>;
361382
}
362383

363384
/**

src/query-filter.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import { describe, it, expect } from "bun:test";
1+
import { describe, expect, it } from "bun:test";
22
import { Archetype } from "./archetype";
3-
import type { EntityId } from "./entity";
3+
import type { ComponentId, EntityId } from "./entity";
44
import { relation } from "./entity";
55
import { matchesComponentTypes, matchesFilter, type QueryFilter } from "./query-filter";
66

77
// Mock component IDs for testing
8-
const positionComponent = 1 as EntityId<{ x: number; y: number }>;
9-
const velocityComponent = 2 as EntityId<{ dx: number; dy: number }>;
10-
const healthComponent = 3 as EntityId<{ value: number }>;
11-
const relationComponent = 4 as EntityId<{ strength: number }>;
8+
const positionComponent = 1 as ComponentId<{ x: number; y: number }>;
9+
const velocityComponent = 2 as ComponentId<{ dx: number; dy: number }>;
10+
const healthComponent = 3 as ComponentId<{ value: number }>;
11+
const relationComponent = 4 as ComponentId<{ strength: number }>;
1212

1313
describe("Query Filter Functions", () => {
1414
describe("matchesComponentTypes", () => {

0 commit comments

Comments
 (0)