@codehz/ecs 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{builder.d.mts → dist/builder.d.mts} +4 -2
- package/{world.mjs → dist/world.mjs} +9 -30
- package/dist/world.mjs.map +1 -0
- package/examples/advanced-scheduling.ts +96 -0
- package/examples/collision-detection.ts +229 -0
- package/examples/inventory-system-relations.ts +108 -0
- package/examples/parent-child-hierarchy.ts +206 -0
- package/examples/serialization.ts +337 -0
- package/examples/simple.ts +96 -0
- package/examples/spatial-grid.ts +276 -0
- package/examples/state-machine.ts +273 -0
- package/examples/tag-filtering.ts +266 -0
- package/package.json +58 -12
- package/src/__tests__/commands/buffer-limits.test.ts +72 -0
- package/src/__tests__/commands/buffer.test.ts +195 -0
- package/src/__tests__/component/singleton.test.ts +148 -0
- package/src/__tests__/core/archetype.test.ts +247 -0
- package/src/__tests__/core/bitset.test.ts +171 -0
- package/src/__tests__/core/changeset.test.ts +254 -0
- package/src/__tests__/core/multi-map.test.ts +74 -0
- package/src/__tests__/entity/component-registry.test.ts +66 -0
- package/src/__tests__/entity/entity.test.ts +520 -0
- package/src/__tests__/entity/id-manager.test.ts +157 -0
- package/src/__tests__/entity/id-system.test.ts +260 -0
- package/src/__tests__/perf/comprehensive.perf.test.ts +300 -0
- package/src/__tests__/perf/sync-hotpath.perf.test.ts +79 -0
- package/src/__tests__/query/basic.test.ts +341 -0
- package/src/__tests__/query/caching.test.ts +112 -0
- package/src/__tests__/query/filter.test.ts +111 -0
- package/src/__tests__/query/optional.test.ts +231 -0
- package/src/__tests__/query/perf.test.ts +99 -0
- package/src/__tests__/relations/dont-fragment/basic.test.ts +496 -0
- package/src/__tests__/relations/dont-fragment/query-notification.test.ts +125 -0
- package/src/__tests__/relations/wildcard.test.ts +179 -0
- package/src/__tests__/serialization/bounds.test.ts +237 -0
- package/src/__tests__/testing/assertions.test.ts +224 -0
- package/src/__tests__/testing/entity-builder.test.ts +84 -0
- package/src/__tests__/testing/snapshot.test.ts +150 -0
- package/src/__tests__/testing/world-fixture.test.ts +73 -0
- package/src/__tests__/world/component-hooks.test.ts +185 -0
- package/src/__tests__/world/component-management.test.ts +447 -0
- package/src/__tests__/world/entity-management.test.ts +86 -0
- package/src/__tests__/world/get-optional.test.ts +96 -0
- package/src/__tests__/world/multi-component-hooks.test.ts +502 -0
- package/src/__tests__/world/perf.test.ts +93 -0
- package/src/__tests__/world/query.test.ts +223 -0
- package/src/__tests__/world/serialize.test.ts +83 -0
- package/src/__tests__/world/wildcard-relation-hooks.test.ts +332 -0
- package/src/archetype/archetype.ts +472 -0
- package/src/archetype/helpers.ts +186 -0
- package/src/archetype/store.ts +33 -0
- package/src/commands/buffer.ts +110 -0
- package/src/commands/changeset.ts +104 -0
- package/src/component/entity-store.ts +223 -0
- package/src/component/registry.ts +657 -0
- package/src/component/type-utils.ts +9 -0
- package/src/entity/index.ts +63 -0
- package/src/entity/manager.ts +115 -0
- package/src/entity/relation.ts +319 -0
- package/src/entity/types.ts +135 -0
- package/src/index.ts +41 -0
- package/src/query/filter.ts +75 -0
- package/src/query/query.ts +313 -0
- package/src/query/registry.ts +101 -0
- package/src/storage/serialization.ts +130 -0
- package/src/testing/index.ts +634 -0
- package/src/types/index.ts +99 -0
- package/src/utils/bit-set.ts +133 -0
- package/src/utils/multi-map.ts +96 -0
- package/src/utils/utils.ts +19 -0
- package/src/world/builder.ts +100 -0
- package/src/world/commands.ts +378 -0
- package/src/world/hooks.ts +358 -0
- package/src/world/references.ts +38 -0
- package/src/world/serialization.ts +122 -0
- package/src/world/world.ts +1201 -0
- package/world.mjs.map +0 -1
- /package/{index.d.mts → dist/index.d.mts} +0 -0
- /package/{index.mjs → dist/index.mjs} +0 -0
- /package/{testing.d.mts → dist/testing.d.mts} +0 -0
- /package/{testing.mjs → dist/testing.mjs} +0 -0
- /package/{testing.mjs.map → dist/testing.mjs.map} +0 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { ComponentId, EntityId } from "../../entity";
|
|
3
|
+
import {
|
|
4
|
+
component,
|
|
5
|
+
COMPONENT_ID_MAX,
|
|
6
|
+
ComponentIdAllocator,
|
|
7
|
+
createComponentId,
|
|
8
|
+
createEntityId,
|
|
9
|
+
decodeRelationId,
|
|
10
|
+
ENTITY_ID_START,
|
|
11
|
+
EntityIdManager,
|
|
12
|
+
getComponentIdByName,
|
|
13
|
+
getComponentNameById,
|
|
14
|
+
getComponentOptions,
|
|
15
|
+
getDetailedIdType,
|
|
16
|
+
getIdType,
|
|
17
|
+
inspectEntityId,
|
|
18
|
+
INVALID_COMPONENT_ID,
|
|
19
|
+
isCascadeDeleteComponent,
|
|
20
|
+
isComponentId,
|
|
21
|
+
isDontFragmentComponent,
|
|
22
|
+
isEntityId,
|
|
23
|
+
isExclusiveComponent,
|
|
24
|
+
isRelationId,
|
|
25
|
+
isWildcardRelationId,
|
|
26
|
+
relation,
|
|
27
|
+
} from "../../entity";
|
|
28
|
+
|
|
29
|
+
describe("Entity ID System", () => {
|
|
30
|
+
describe("Component IDs", () => {
|
|
31
|
+
it("should create valid component IDs", () => {
|
|
32
|
+
expect(createComponentId(1)).toBe(createComponentId(1));
|
|
33
|
+
expect(createComponentId(2)).toBe(createComponentId(2));
|
|
34
|
+
expect(createComponentId(COMPONENT_ID_MAX)).toBe(createComponentId(COMPONENT_ID_MAX));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should reject invalid component IDs", () => {
|
|
38
|
+
expect(() => createComponentId(0)).toThrow();
|
|
39
|
+
expect(() => createComponentId(-1)).toThrow();
|
|
40
|
+
expect(() => createComponentId(COMPONENT_ID_MAX + 1)).toThrow();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should identify component IDs correctly", () => {
|
|
44
|
+
expect(isComponentId(createComponentId(1))).toBe(true);
|
|
45
|
+
expect(isComponentId(createComponentId(2))).toBe(true);
|
|
46
|
+
expect(isComponentId(createComponentId(COMPONENT_ID_MAX))).toBe(true);
|
|
47
|
+
expect(isComponentId(createEntityId(ENTITY_ID_START))).toBe(false);
|
|
48
|
+
expect(isComponentId(relation(createComponentId(1), createEntityId(ENTITY_ID_START)))).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("Entity IDs", () => {
|
|
53
|
+
it("should create valid entity IDs", () => {
|
|
54
|
+
expect(createEntityId(ENTITY_ID_START)).toBe(createEntityId(ENTITY_ID_START));
|
|
55
|
+
expect(createEntityId(ENTITY_ID_START + 1)).toBe(createEntityId(ENTITY_ID_START + 1));
|
|
56
|
+
expect(createEntityId(10000)).toBe(createEntityId(10000));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should reject invalid entity IDs", () => {
|
|
60
|
+
expect(() => createEntityId(ENTITY_ID_START - 1)).toThrow();
|
|
61
|
+
expect(() => createEntityId(0)).toThrow();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should identify entity IDs correctly", () => {
|
|
65
|
+
expect(isEntityId(createEntityId(ENTITY_ID_START))).toBe(true);
|
|
66
|
+
expect(isEntityId(createEntityId(10000))).toBe(true);
|
|
67
|
+
expect(isEntityId(createComponentId(1))).toBe(false);
|
|
68
|
+
expect(isEntityId(relation(createComponentId(1), createEntityId(ENTITY_ID_START)))).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("Relation IDs", () => {
|
|
73
|
+
it("should create valid relation IDs with entities", () => {
|
|
74
|
+
const compId = createComponentId(5);
|
|
75
|
+
const entId = createEntityId(ENTITY_ID_START + 10);
|
|
76
|
+
const relationId = relation(compId, entId);
|
|
77
|
+
|
|
78
|
+
expect(relationId).toBeLessThan(0);
|
|
79
|
+
expect(isRelationId(relationId)).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should create valid relation IDs with components", () => {
|
|
83
|
+
const compId1 = createComponentId(5);
|
|
84
|
+
const compId2 = createComponentId(10);
|
|
85
|
+
const relationId = relation(compId1, compId2);
|
|
86
|
+
|
|
87
|
+
expect(relationId).toBeLessThan(0);
|
|
88
|
+
expect(isRelationId(relationId)).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should reject invalid relation creation", () => {
|
|
92
|
+
const entId = createEntityId(ENTITY_ID_START);
|
|
93
|
+
expect(() => relation(1024 as ComponentId, entId)).toThrow(); // invalid component id
|
|
94
|
+
expect(() => relation(createComponentId(5), -1 as EntityId)).toThrow(); // invalid target id
|
|
95
|
+
expect(() => relation(createComponentId(5), relation(createComponentId(1), createEntityId(1025)))).toThrow(); // relation as target
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should decode relation IDs with entities correctly", () => {
|
|
99
|
+
const compId = createComponentId(42);
|
|
100
|
+
const entId = createEntityId(ENTITY_ID_START + 123);
|
|
101
|
+
const relationId = relation(compId, entId);
|
|
102
|
+
|
|
103
|
+
const decoded = decodeRelationId(relationId);
|
|
104
|
+
expect(decoded.componentId).toBe(compId);
|
|
105
|
+
expect(decoded.targetId).toBe(entId);
|
|
106
|
+
expect(decoded.type).toBe("entity");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should decode relation IDs with components correctly", () => {
|
|
110
|
+
const compId1 = createComponentId(42);
|
|
111
|
+
const compId2 = createComponentId(100);
|
|
112
|
+
const relationId = relation(compId1, compId2);
|
|
113
|
+
|
|
114
|
+
const decoded = decodeRelationId(relationId);
|
|
115
|
+
expect(decoded.componentId).toBe(compId1);
|
|
116
|
+
expect(decoded.targetId).toBe(compId2);
|
|
117
|
+
expect(decoded.type).toBe("component");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should create valid wildcard relation IDs", () => {
|
|
121
|
+
const compId = createComponentId(5);
|
|
122
|
+
const relationId = relation(compId, "*");
|
|
123
|
+
|
|
124
|
+
expect(relationId).toBeLessThan(0);
|
|
125
|
+
expect(isRelationId(relationId)).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should identify wildcard relation IDs correctly", () => {
|
|
129
|
+
const compId = createComponentId(5);
|
|
130
|
+
const wildcardRelationId = relation(compId, "*");
|
|
131
|
+
const entityRelationId = relation(compId, createEntityId(ENTITY_ID_START));
|
|
132
|
+
const componentRelationId = relation(compId, createComponentId(10));
|
|
133
|
+
const entityId = createEntityId(ENTITY_ID_START);
|
|
134
|
+
const componentId = createComponentId(1);
|
|
135
|
+
|
|
136
|
+
expect(isWildcardRelationId(wildcardRelationId)).toBe(true);
|
|
137
|
+
expect(isWildcardRelationId(entityRelationId)).toBe(false);
|
|
138
|
+
expect(isWildcardRelationId(componentRelationId)).toBe(false);
|
|
139
|
+
expect(isWildcardRelationId(entityId)).toBe(false);
|
|
140
|
+
expect(isWildcardRelationId(componentId)).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should decode wildcard relation IDs correctly", () => {
|
|
144
|
+
const compId = createComponentId(42);
|
|
145
|
+
const relationId = relation(compId, "*");
|
|
146
|
+
|
|
147
|
+
const decoded = decodeRelationId(relationId);
|
|
148
|
+
expect(decoded.componentId).toBe(compId);
|
|
149
|
+
expect(decoded.targetId).toBe(0 as EntityId);
|
|
150
|
+
expect(decoded.type).toBe("wildcard");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("ID Type Detection", () => {
|
|
155
|
+
it("should correctly identify ID types", () => {
|
|
156
|
+
expect(getIdType(createComponentId(1))).toBe("component");
|
|
157
|
+
expect(getIdType(createComponentId(500))).toBe("component");
|
|
158
|
+
expect(getIdType(createEntityId(ENTITY_ID_START))).toBe("entity");
|
|
159
|
+
expect(getIdType(createEntityId(10000))).toBe("entity");
|
|
160
|
+
expect(getIdType(relation(createComponentId(1), createEntityId(ENTITY_ID_START)))).toBe("entity-relation");
|
|
161
|
+
expect(getIdType(relation(createComponentId(1), createComponentId(2)))).toBe("component-relation");
|
|
162
|
+
expect(getIdType(relation(createComponentId(1), "*"))).toBe("wildcard-relation");
|
|
163
|
+
|
|
164
|
+
// Invalid IDs
|
|
165
|
+
expect(getIdType(INVALID_COMPONENT_ID as EntityId)).toBe("invalid");
|
|
166
|
+
expect(getIdType(-999999 as EntityId)).toBe("invalid");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should provide detailed ID type information", () => {
|
|
170
|
+
// Component ID
|
|
171
|
+
const compResult = getDetailedIdType(createComponentId(42));
|
|
172
|
+
expect(compResult.type).toBe("component");
|
|
173
|
+
expect(compResult.componentId).toBeUndefined();
|
|
174
|
+
expect(compResult.targetId).toBeUndefined();
|
|
175
|
+
|
|
176
|
+
// Entity ID
|
|
177
|
+
const entityResult = getDetailedIdType(createEntityId(ENTITY_ID_START + 100));
|
|
178
|
+
expect(entityResult.type).toBe("entity");
|
|
179
|
+
expect(entityResult.componentId).toBeUndefined();
|
|
180
|
+
expect(entityResult.targetId).toBeUndefined();
|
|
181
|
+
|
|
182
|
+
// Entity relation
|
|
183
|
+
const entityRelationId = relation(createComponentId(5), createEntityId(ENTITY_ID_START + 200));
|
|
184
|
+
const entityRelationResult = getDetailedIdType(entityRelationId);
|
|
185
|
+
expect(entityRelationResult.type).toBe("entity-relation");
|
|
186
|
+
expect(entityRelationResult.componentId).toBe(createComponentId(5));
|
|
187
|
+
expect(entityRelationResult.targetId).toBe(createEntityId(ENTITY_ID_START + 200));
|
|
188
|
+
|
|
189
|
+
// Component relation
|
|
190
|
+
const compRelationId = relation(createComponentId(10), createComponentId(20));
|
|
191
|
+
const compRelationResult = getDetailedIdType(compRelationId);
|
|
192
|
+
expect(compRelationResult.type).toBe("component-relation");
|
|
193
|
+
expect(compRelationResult.componentId).toBe(createComponentId(10));
|
|
194
|
+
expect(compRelationResult.targetId).toBe(createComponentId(20));
|
|
195
|
+
|
|
196
|
+
// Wildcard relation
|
|
197
|
+
const wildcardRelationId = relation(createComponentId(15), "*");
|
|
198
|
+
const wildcardRelationResult = getDetailedIdType(wildcardRelationId);
|
|
199
|
+
expect(wildcardRelationResult.type).toBe("wildcard-relation");
|
|
200
|
+
expect(wildcardRelationResult.componentId).toBe(createComponentId(15));
|
|
201
|
+
expect(wildcardRelationResult.targetId).toBe(0 as EntityId);
|
|
202
|
+
|
|
203
|
+
// Invalid IDs
|
|
204
|
+
const invalidResult = getDetailedIdType(INVALID_COMPONENT_ID as EntityId);
|
|
205
|
+
expect(invalidResult.type).toBe("invalid");
|
|
206
|
+
expect(invalidResult.componentId).toBeUndefined();
|
|
207
|
+
expect(invalidResult.targetId).toBeUndefined();
|
|
208
|
+
|
|
209
|
+
const invalidRelationResult = getDetailedIdType(-999999 as EntityId);
|
|
210
|
+
expect(invalidRelationResult.type).toBe("invalid");
|
|
211
|
+
expect(invalidRelationResult.componentId).toBeUndefined();
|
|
212
|
+
expect(invalidRelationResult.targetId).toBeUndefined();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("ID Inspection", () => {
|
|
217
|
+
it("should inspect invalid component ID", () => {
|
|
218
|
+
expect(inspectEntityId(INVALID_COMPONENT_ID as EntityId)).toBe("Invalid Component ID (0)");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("should inspect component IDs", () => {
|
|
222
|
+
expect(inspectEntityId(createComponentId(1))).toBe("Component ID (1)");
|
|
223
|
+
expect(inspectEntityId(createComponentId(42))).toBe("Component ID (42)");
|
|
224
|
+
expect(inspectEntityId(createComponentId(COMPONENT_ID_MAX))).toBe(`Component ID (${COMPONENT_ID_MAX})`);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should inspect entity IDs", () => {
|
|
228
|
+
expect(inspectEntityId(createEntityId(ENTITY_ID_START))).toBe(`Entity ID (${ENTITY_ID_START})`);
|
|
229
|
+
expect(inspectEntityId(createEntityId(10000))).toBe("Entity ID (10000)");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should inspect relation IDs with entities", () => {
|
|
233
|
+
const compId = createComponentId(5);
|
|
234
|
+
const entId = createEntityId(ENTITY_ID_START + 10);
|
|
235
|
+
const relationId = relation(compId, entId);
|
|
236
|
+
|
|
237
|
+
expect(inspectEntityId(relationId)).toBe("Relation ID: Component ID (5) -> Entity ID (1034)");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("should inspect relation IDs with components", () => {
|
|
241
|
+
const compId1 = createComponentId(10);
|
|
242
|
+
const compId2 = createComponentId(20);
|
|
243
|
+
const relationId = relation(compId1, compId2);
|
|
244
|
+
|
|
245
|
+
expect(inspectEntityId(relationId)).toBe("Relation ID: Component ID (10) -> Component ID (20)");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("should handle invalid relation IDs gracefully", () => {
|
|
249
|
+
// Create an invalid relation ID that looks like a relation but has invalid components
|
|
250
|
+
const invalidRelationId = -999999 as EntityId;
|
|
251
|
+
expect(inspectEntityId(invalidRelationId)).toBe("Invalid Relation ID (-999999)");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("should inspect wildcard relation IDs", () => {
|
|
255
|
+
const compId = createComponentId(15);
|
|
256
|
+
const relationId = relation(compId, "*");
|
|
257
|
+
|
|
258
|
+
expect(inspectEntityId(relationId)).toBe("Relation ID: Component ID (15) -> Wildcard (*)");
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("Bit Operations Safety", () => {
|
|
263
|
+
it("should handle large entity IDs within safe integer range", () => {
|
|
264
|
+
// 2^42 - 1 is within safe integer (2^53 - 1)
|
|
265
|
+
const largeEntityId = (1 << 42) - 1 + ENTITY_ID_START;
|
|
266
|
+
expect(Number.isSafeInteger(largeEntityId)).toBe(true);
|
|
267
|
+
|
|
268
|
+
const compId = createComponentId(1023);
|
|
269
|
+
const relationId = relation(compId, largeEntityId as EntityId);
|
|
270
|
+
expect(Number.isSafeInteger(relationId)).toBe(true);
|
|
271
|
+
|
|
272
|
+
const decoded = decodeRelationId(relationId);
|
|
273
|
+
expect(decoded.componentId).toBe(compId);
|
|
274
|
+
expect(decoded.targetId).toBe(largeEntityId as EntityId);
|
|
275
|
+
expect(decoded.type).toBe("entity");
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("EntityIdManager", () => {
|
|
281
|
+
describe("Allocation", () => {
|
|
282
|
+
it("should allocate sequential entity IDs starting from ENTITY_ID_START", () => {
|
|
283
|
+
const manager = new EntityIdManager();
|
|
284
|
+
expect(manager.allocate()).toBe(createEntityId(ENTITY_ID_START));
|
|
285
|
+
expect(manager.allocate()).toBe(createEntityId(ENTITY_ID_START + 1));
|
|
286
|
+
expect(manager.allocate()).toBe(createEntityId(ENTITY_ID_START + 2));
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("should reuse IDs from freelist before allocating new ones", () => {
|
|
290
|
+
const manager = new EntityIdManager();
|
|
291
|
+
manager.allocate(); // 1024
|
|
292
|
+
const id2 = manager.allocate(); // 1025
|
|
293
|
+
manager.allocate(); // 1026
|
|
294
|
+
|
|
295
|
+
manager.deallocate(id2);
|
|
296
|
+
expect(manager.allocate()).toBe(id2); // Should reuse 1025
|
|
297
|
+
expect(manager.allocate()).toBe(createEntityId(ENTITY_ID_START + 3)); // Then 1027
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe("Deallocation", () => {
|
|
302
|
+
it("should add deallocated IDs to freelist", () => {
|
|
303
|
+
const manager = new EntityIdManager();
|
|
304
|
+
const id = manager.allocate();
|
|
305
|
+
expect(manager.getFreelistSize()).toBe(0);
|
|
306
|
+
|
|
307
|
+
manager.deallocate(id);
|
|
308
|
+
expect(manager.getFreelistSize()).toBe(1);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("should reject deallocation of invalid entity IDs", () => {
|
|
312
|
+
const manager = new EntityIdManager();
|
|
313
|
+
expect(() => manager.deallocate(1000 as EntityId)).toThrow(); // Below ENTITY_ID_START
|
|
314
|
+
expect(() => manager.deallocate(createComponentId(5))).toThrow(); // Component ID
|
|
315
|
+
expect(() => manager.deallocate(relation(createComponentId(1), createEntityId(1025)))).toThrow(); // Relation ID
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("should reject deallocation of unallocated IDs", () => {
|
|
319
|
+
const manager = new EntityIdManager();
|
|
320
|
+
expect(() => manager.deallocate((ENTITY_ID_START + 100) as EntityId)).toThrow();
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe("Freelist Management", () => {
|
|
325
|
+
it("should maintain correct freelist size", () => {
|
|
326
|
+
const manager = new EntityIdManager();
|
|
327
|
+
const ids: EntityId[] = [];
|
|
328
|
+
|
|
329
|
+
// Allocate 5 IDs
|
|
330
|
+
for (let i = 0; i < 5; i++) {
|
|
331
|
+
ids.push(manager.allocate());
|
|
332
|
+
}
|
|
333
|
+
expect(manager.getFreelistSize()).toBe(0);
|
|
334
|
+
|
|
335
|
+
// Deallocate 3 IDs
|
|
336
|
+
manager.deallocate(ids[1]!);
|
|
337
|
+
manager.deallocate(ids[3]!);
|
|
338
|
+
manager.deallocate(ids[4]!);
|
|
339
|
+
expect(manager.getFreelistSize()).toBe(3);
|
|
340
|
+
|
|
341
|
+
// Allocate 2 more (should reuse)
|
|
342
|
+
manager.allocate(); // Reuse ids[1]
|
|
343
|
+
manager.allocate(); // Reuse ids[3]
|
|
344
|
+
expect(manager.getFreelistSize()).toBe(1); // ids[4] still in freelist
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("should handle multiple deallocate/allocate cycles", () => {
|
|
348
|
+
const manager = new EntityIdManager();
|
|
349
|
+
const allocated: EntityId[] = [];
|
|
350
|
+
|
|
351
|
+
// Allocate 10, deallocate all, allocate 10 again
|
|
352
|
+
for (let i = 0; i < 10; i++) {
|
|
353
|
+
allocated.push(manager.allocate());
|
|
354
|
+
}
|
|
355
|
+
allocated.forEach((id) => manager.deallocate(id));
|
|
356
|
+
expect(manager.getFreelistSize()).toBe(10);
|
|
357
|
+
|
|
358
|
+
const newAllocated: EntityId[] = [];
|
|
359
|
+
for (let i = 0; i < 10; i++) {
|
|
360
|
+
newAllocated.push(manager.allocate());
|
|
361
|
+
}
|
|
362
|
+
expect(manager.getFreelistSize()).toBe(0);
|
|
363
|
+
// Should have reused all previous IDs
|
|
364
|
+
expect(new Set(newAllocated)).toEqual(new Set(allocated));
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
describe("Overflow Protection", () => {
|
|
369
|
+
it("should throw error on ID overflow", () => {
|
|
370
|
+
const manager = new EntityIdManager();
|
|
371
|
+
// Mock nextId to near max
|
|
372
|
+
(manager as any).nextId = Number.MAX_SAFE_INTEGER - 1;
|
|
373
|
+
(manager as any).freelist.length = 0;
|
|
374
|
+
|
|
375
|
+
expect(() => manager.allocate()).toThrow("Entity ID overflow");
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe("ComponentIdManager", () => {
|
|
381
|
+
describe("Allocation", () => {
|
|
382
|
+
it("should allocate sequential component IDs starting from 1", () => {
|
|
383
|
+
const manager = new ComponentIdAllocator();
|
|
384
|
+
expect(manager.allocate()).toBe(createComponentId(1));
|
|
385
|
+
expect(manager.allocate()).toBe(createComponentId(2));
|
|
386
|
+
expect(manager.allocate()).toBe(createComponentId(3));
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("should allocate up to COMPONENT_ID_MAX", () => {
|
|
390
|
+
const manager = new ComponentIdAllocator();
|
|
391
|
+
for (let i = 1; i <= COMPONENT_ID_MAX; i++) {
|
|
392
|
+
expect(manager.allocate()).toBe(createComponentId(i));
|
|
393
|
+
}
|
|
394
|
+
expect(manager.hasAvailableIds()).toBe(false);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("should throw error when exceeding maximum component IDs", () => {
|
|
398
|
+
const manager = new ComponentIdAllocator();
|
|
399
|
+
// Allocate all available IDs
|
|
400
|
+
for (let i = 1; i <= COMPONENT_ID_MAX; i++) {
|
|
401
|
+
manager.allocate();
|
|
402
|
+
}
|
|
403
|
+
expect(() => manager.allocate()).toThrow("Component ID overflow");
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
describe("State Queries", () => {
|
|
408
|
+
it("should report correct next ID", () => {
|
|
409
|
+
const manager = new ComponentIdAllocator();
|
|
410
|
+
expect(manager.getNextId()).toBe(1);
|
|
411
|
+
manager.allocate();
|
|
412
|
+
expect(manager.getNextId()).toBe(2);
|
|
413
|
+
manager.allocate();
|
|
414
|
+
expect(manager.getNextId()).toBe(3);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("should correctly report available IDs", () => {
|
|
418
|
+
const manager = new ComponentIdAllocator();
|
|
419
|
+
expect(manager.hasAvailableIds()).toBe(true);
|
|
420
|
+
|
|
421
|
+
// Allocate all but one
|
|
422
|
+
for (let i = 1; i < COMPONENT_ID_MAX; i++) {
|
|
423
|
+
manager.allocate();
|
|
424
|
+
}
|
|
425
|
+
expect(manager.hasAvailableIds()).toBe(true);
|
|
426
|
+
|
|
427
|
+
// Allocate the last one
|
|
428
|
+
manager.allocate();
|
|
429
|
+
expect(manager.hasAvailableIds()).toBe(false);
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe("Component Options", () => {
|
|
435
|
+
it("should store and retrieve component options", () => {
|
|
436
|
+
const exclusiveComp = component({ exclusive: true });
|
|
437
|
+
const cascadeComp = component({ cascadeDelete: true });
|
|
438
|
+
const bothComp = component({ exclusive: true, cascadeDelete: true });
|
|
439
|
+
const normalComp = component();
|
|
440
|
+
|
|
441
|
+
const exclusiveOpts = getComponentOptions(exclusiveComp);
|
|
442
|
+
expect(exclusiveOpts.exclusive).toBe(true);
|
|
443
|
+
expect(exclusiveOpts.cascadeDelete).toBe(undefined);
|
|
444
|
+
|
|
445
|
+
const cascadeOpts = getComponentOptions(cascadeComp);
|
|
446
|
+
expect(cascadeOpts.exclusive).toBe(undefined);
|
|
447
|
+
expect(cascadeOpts.cascadeDelete).toBe(true);
|
|
448
|
+
|
|
449
|
+
const bothOpts = getComponentOptions(bothComp);
|
|
450
|
+
expect(bothOpts.exclusive).toBe(true);
|
|
451
|
+
expect(bothOpts.cascadeDelete).toBe(true);
|
|
452
|
+
|
|
453
|
+
const normalOpts = getComponentOptions(normalComp);
|
|
454
|
+
expect(normalOpts.name).toBe(undefined);
|
|
455
|
+
expect(normalOpts.exclusive).toBe(undefined);
|
|
456
|
+
expect(normalOpts.cascadeDelete).toBe(undefined);
|
|
457
|
+
expect(normalOpts.dontFragment).toBe(undefined);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("should support name in options object", () => {
|
|
461
|
+
const namedComp = component({ name: "TestComponent", exclusive: true });
|
|
462
|
+
|
|
463
|
+
const options = getComponentOptions(namedComp);
|
|
464
|
+
expect(options?.name).toBe("TestComponent");
|
|
465
|
+
expect(options?.exclusive).toBe(true);
|
|
466
|
+
|
|
467
|
+
expect(getComponentNameById(namedComp)).toBe("TestComponent");
|
|
468
|
+
expect(getComponentIdByName("TestComponent")).toBe(namedComp);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("should check if component is exclusive", () => {
|
|
472
|
+
const exclusiveComp = component({ exclusive: true });
|
|
473
|
+
const normalComp = component();
|
|
474
|
+
|
|
475
|
+
expect(isExclusiveComponent(exclusiveComp)).toBe(true);
|
|
476
|
+
expect(isExclusiveComponent(normalComp)).toBe(false);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("should check if component is cascade delete", () => {
|
|
480
|
+
const cascadeComp = component({ cascadeDelete: true });
|
|
481
|
+
const normalComp = component();
|
|
482
|
+
|
|
483
|
+
expect(isCascadeDeleteComponent(cascadeComp)).toBe(true);
|
|
484
|
+
expect(isCascadeDeleteComponent(normalComp)).toBe(false);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("should check if component is dontFragment", () => {
|
|
488
|
+
const dontFragmentComp = component({ dontFragment: true });
|
|
489
|
+
const normalComp = component();
|
|
490
|
+
|
|
491
|
+
expect(isDontFragmentComponent(dontFragmentComp)).toBe(true);
|
|
492
|
+
expect(isDontFragmentComponent(normalComp)).toBe(false);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("should support cascadeDelete and dontFragment set simultaneously", () => {
|
|
496
|
+
const combinedComp = component({ cascadeDelete: true, dontFragment: true });
|
|
497
|
+
|
|
498
|
+
const options = getComponentOptions(combinedComp);
|
|
499
|
+
expect(options.cascadeDelete).toBe(true);
|
|
500
|
+
expect(options.dontFragment).toBe(true);
|
|
501
|
+
|
|
502
|
+
expect(isCascadeDeleteComponent(combinedComp)).toBe(true);
|
|
503
|
+
expect(isDontFragmentComponent(combinedComp)).toBe(true);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("should store and retrieve component merge callback", () => {
|
|
507
|
+
const merge = (prev: number[], next: number[]) => [...prev, ...next];
|
|
508
|
+
const mailboxComp = component<number[]>({ merge });
|
|
509
|
+
|
|
510
|
+
const options = getComponentOptions(mailboxComp);
|
|
511
|
+
expect(options.merge).toBeDefined();
|
|
512
|
+
expect(options.merge?.([1], [2, 3])).toEqual([1, 2, 3]);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it("should throw error for invalid component ID", () => {
|
|
516
|
+
expect(() => getComponentOptions(0 as ComponentId)).toThrow("Invalid component ID");
|
|
517
|
+
expect(() => getComponentOptions(1025 as ComponentId)).toThrow("Invalid component ID");
|
|
518
|
+
expect(() => getComponentOptions(-1 as ComponentId)).toThrow("Invalid component ID");
|
|
519
|
+
});
|
|
520
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { EntityIdManager } from "../../entity/manager";
|
|
3
|
+
import type { EntityId } from "../../testing";
|
|
4
|
+
|
|
5
|
+
describe("EntityIdManager", () => {
|
|
6
|
+
it("should allocate entity IDs sequentially", () => {
|
|
7
|
+
const manager = new EntityIdManager();
|
|
8
|
+
|
|
9
|
+
const id1 = manager.allocate();
|
|
10
|
+
const id2 = manager.allocate();
|
|
11
|
+
const id3 = manager.allocate();
|
|
12
|
+
|
|
13
|
+
// Entity IDs start at 1024
|
|
14
|
+
expect(Number(id1 as unknown as number)).toBe(1024);
|
|
15
|
+
expect(Number(id2 as unknown as number)).toBe(1025);
|
|
16
|
+
expect(Number(id3 as unknown as number)).toBe(1026);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should reuse freed entity IDs (LIFO)", () => {
|
|
20
|
+
const manager = new EntityIdManager();
|
|
21
|
+
|
|
22
|
+
const id1 = manager.allocate();
|
|
23
|
+
const id2 = manager.allocate();
|
|
24
|
+
const id3 = manager.allocate();
|
|
25
|
+
|
|
26
|
+
// Free in order
|
|
27
|
+
manager.deallocate(id1);
|
|
28
|
+
manager.deallocate(id2);
|
|
29
|
+
manager.deallocate(id3);
|
|
30
|
+
|
|
31
|
+
// Should reuse in LIFO order (last freed is first reused)
|
|
32
|
+
expect(manager.allocate()).toBe(id3);
|
|
33
|
+
expect(manager.allocate()).toBe(id2);
|
|
34
|
+
expect(manager.allocate()).toBe(id1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should handle large number of allocations", () => {
|
|
38
|
+
const manager = new EntityIdManager();
|
|
39
|
+
const ids = new Set<number>();
|
|
40
|
+
|
|
41
|
+
// Allocate 10000 IDs
|
|
42
|
+
for (let i = 0; i < 10000; i++) {
|
|
43
|
+
ids.add(manager.allocate());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
expect(ids.size).toBe(10000);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should handle interleaved allocate and free", () => {
|
|
50
|
+
const manager = new EntityIdManager();
|
|
51
|
+
const allocated: number[] = [];
|
|
52
|
+
|
|
53
|
+
// Allocate some IDs
|
|
54
|
+
for (let i = 0; i < 100; i++) {
|
|
55
|
+
allocated.push(manager.allocate());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
expect(manager.getFreelistSize()).toBe(0);
|
|
59
|
+
|
|
60
|
+
// Free every third ID
|
|
61
|
+
const toReuse = [];
|
|
62
|
+
for (let i = 0; i < allocated.length; i += 3) {
|
|
63
|
+
manager.deallocate(allocated[i]! as EntityId<any>);
|
|
64
|
+
toReuse.push(allocated[i]!);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const freelistSize = manager.getFreelistSize();
|
|
68
|
+
expect(freelistSize).toBe(Math.ceil(allocated.length / 3));
|
|
69
|
+
|
|
70
|
+
// Allocate new IDs - should reuse freed ones
|
|
71
|
+
for (let i = 0; i < toReuse.length; i++) {
|
|
72
|
+
const newId = manager.allocate();
|
|
73
|
+
expect(toReuse).toContain(newId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
expect(manager.getFreelistSize()).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should maintain freelist as LIFO stack", () => {
|
|
80
|
+
const manager = new EntityIdManager();
|
|
81
|
+
|
|
82
|
+
const id1 = manager.allocate();
|
|
83
|
+
const id2 = manager.allocate();
|
|
84
|
+
const id3 = manager.allocate();
|
|
85
|
+
|
|
86
|
+
// Free in specific order
|
|
87
|
+
manager.deallocate(id1);
|
|
88
|
+
manager.deallocate(id2);
|
|
89
|
+
manager.deallocate(id3);
|
|
90
|
+
|
|
91
|
+
expect(manager.getFreelistSize()).toBe(3);
|
|
92
|
+
|
|
93
|
+
// Last freed (id3) should be popped first (LIFO)
|
|
94
|
+
const reused1 = manager.allocate();
|
|
95
|
+
expect(reused1).toBe(id3);
|
|
96
|
+
|
|
97
|
+
const reused2 = manager.allocate();
|
|
98
|
+
expect(reused2).toBe(id2);
|
|
99
|
+
|
|
100
|
+
const reused3 = manager.allocate();
|
|
101
|
+
expect(reused3).toBe(id1);
|
|
102
|
+
|
|
103
|
+
expect(manager.getFreelistSize()).toBe(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should throw when deallocating invalid entity ID", () => {
|
|
107
|
+
const manager = new EntityIdManager();
|
|
108
|
+
manager.allocate();
|
|
109
|
+
|
|
110
|
+
// Deallocating negative ID or ID that was never allocated
|
|
111
|
+
expect(() => manager.deallocate(0 as unknown as ReturnType<typeof manager.allocate>)).toThrow(
|
|
112
|
+
/valid entity|deallocate/i,
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should serialize and deserialize state", () => {
|
|
117
|
+
const manager = new EntityIdManager();
|
|
118
|
+
|
|
119
|
+
const id1 = manager.allocate();
|
|
120
|
+
manager.allocate();
|
|
121
|
+
|
|
122
|
+
manager.deallocate(id1);
|
|
123
|
+
|
|
124
|
+
const state = manager.serializeState();
|
|
125
|
+
expect(state.nextId).toBe(1026);
|
|
126
|
+
expect(state.freelist).toContain(id1);
|
|
127
|
+
|
|
128
|
+
const newManager = new EntityIdManager();
|
|
129
|
+
newManager.deserializeState(state);
|
|
130
|
+
|
|
131
|
+
expect(newManager.getNextId()).toBe(1026);
|
|
132
|
+
expect(newManager.getFreelistSize()).toBe(1);
|
|
133
|
+
|
|
134
|
+
// Should reuse the freed ID
|
|
135
|
+
const reused = newManager.allocate();
|
|
136
|
+
expect(reused).toBe(id1);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should allocate new IDs after reusing freelist", () => {
|
|
140
|
+
const manager = new EntityIdManager();
|
|
141
|
+
|
|
142
|
+
const id1 = manager.allocate();
|
|
143
|
+
const id2 = manager.allocate();
|
|
144
|
+
manager.allocate();
|
|
145
|
+
|
|
146
|
+
manager.deallocate(id1);
|
|
147
|
+
manager.deallocate(id2);
|
|
148
|
+
|
|
149
|
+
// Reuse freed IDs
|
|
150
|
+
expect(manager.allocate()).toBe(id2);
|
|
151
|
+
expect(manager.allocate()).toBe(id1);
|
|
152
|
+
|
|
153
|
+
// Next allocation should be a new ID
|
|
154
|
+
const newId = manager.allocate();
|
|
155
|
+
expect(Number(newId as unknown as number)).toBe(1027);
|
|
156
|
+
});
|
|
157
|
+
});
|