@codehz/ecs 0.7.2 → 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.
Files changed (81) hide show
  1. package/examples/advanced-scheduling.ts +96 -0
  2. package/examples/collision-detection.ts +229 -0
  3. package/examples/inventory-system-relations.ts +108 -0
  4. package/examples/parent-child-hierarchy.ts +206 -0
  5. package/examples/serialization.ts +337 -0
  6. package/examples/simple.ts +96 -0
  7. package/examples/spatial-grid.ts +276 -0
  8. package/examples/state-machine.ts +273 -0
  9. package/examples/tag-filtering.ts +266 -0
  10. package/package.json +58 -12
  11. package/src/__tests__/commands/buffer-limits.test.ts +72 -0
  12. package/src/__tests__/commands/buffer.test.ts +195 -0
  13. package/src/__tests__/component/singleton.test.ts +148 -0
  14. package/src/__tests__/core/archetype.test.ts +247 -0
  15. package/src/__tests__/core/bitset.test.ts +171 -0
  16. package/src/__tests__/core/changeset.test.ts +254 -0
  17. package/src/__tests__/core/multi-map.test.ts +74 -0
  18. package/src/__tests__/entity/component-registry.test.ts +66 -0
  19. package/src/__tests__/entity/entity.test.ts +520 -0
  20. package/src/__tests__/entity/id-manager.test.ts +157 -0
  21. package/src/__tests__/entity/id-system.test.ts +260 -0
  22. package/src/__tests__/perf/comprehensive.perf.test.ts +300 -0
  23. package/src/__tests__/perf/sync-hotpath.perf.test.ts +79 -0
  24. package/src/__tests__/query/basic.test.ts +341 -0
  25. package/src/__tests__/query/caching.test.ts +112 -0
  26. package/src/__tests__/query/filter.test.ts +111 -0
  27. package/src/__tests__/query/optional.test.ts +231 -0
  28. package/src/__tests__/query/perf.test.ts +99 -0
  29. package/src/__tests__/relations/dont-fragment/basic.test.ts +496 -0
  30. package/src/__tests__/relations/dont-fragment/query-notification.test.ts +125 -0
  31. package/src/__tests__/relations/wildcard.test.ts +179 -0
  32. package/src/__tests__/serialization/bounds.test.ts +237 -0
  33. package/src/__tests__/testing/assertions.test.ts +224 -0
  34. package/src/__tests__/testing/entity-builder.test.ts +84 -0
  35. package/src/__tests__/testing/snapshot.test.ts +150 -0
  36. package/src/__tests__/testing/world-fixture.test.ts +73 -0
  37. package/src/__tests__/world/component-hooks.test.ts +185 -0
  38. package/src/__tests__/world/component-management.test.ts +447 -0
  39. package/src/__tests__/world/entity-management.test.ts +86 -0
  40. package/src/__tests__/world/get-optional.test.ts +96 -0
  41. package/src/__tests__/world/multi-component-hooks.test.ts +502 -0
  42. package/src/__tests__/world/perf.test.ts +93 -0
  43. package/src/__tests__/world/query.test.ts +223 -0
  44. package/src/__tests__/world/serialize.test.ts +83 -0
  45. package/src/__tests__/world/wildcard-relation-hooks.test.ts +332 -0
  46. package/src/archetype/archetype.ts +472 -0
  47. package/src/archetype/helpers.ts +186 -0
  48. package/src/archetype/store.ts +33 -0
  49. package/src/commands/buffer.ts +110 -0
  50. package/src/commands/changeset.ts +104 -0
  51. package/src/component/entity-store.ts +223 -0
  52. package/src/component/registry.ts +657 -0
  53. package/src/component/type-utils.ts +9 -0
  54. package/src/entity/index.ts +63 -0
  55. package/src/entity/manager.ts +115 -0
  56. package/src/entity/relation.ts +319 -0
  57. package/src/entity/types.ts +135 -0
  58. package/src/index.ts +41 -0
  59. package/src/query/filter.ts +75 -0
  60. package/src/query/query.ts +313 -0
  61. package/src/query/registry.ts +101 -0
  62. package/src/storage/serialization.ts +130 -0
  63. package/src/testing/index.ts +634 -0
  64. package/src/types/index.ts +99 -0
  65. package/src/utils/bit-set.ts +133 -0
  66. package/src/utils/multi-map.ts +96 -0
  67. package/src/utils/utils.ts +19 -0
  68. package/src/world/builder.ts +100 -0
  69. package/src/world/commands.ts +378 -0
  70. package/src/world/hooks.ts +358 -0
  71. package/src/world/references.ts +38 -0
  72. package/src/world/serialization.ts +122 -0
  73. package/src/world/world.ts +1201 -0
  74. /package/{builder.d.mts → dist/builder.d.mts} +0 -0
  75. /package/{index.d.mts → dist/index.d.mts} +0 -0
  76. /package/{index.mjs → dist/index.mjs} +0 -0
  77. /package/{testing.d.mts → dist/testing.d.mts} +0 -0
  78. /package/{testing.mjs → dist/testing.mjs} +0 -0
  79. /package/{testing.mjs.map → dist/testing.mjs.map} +0 -0
  80. /package/{world.mjs → dist/world.mjs} +0 -0
  81. /package/{world.mjs.map → dist/world.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
+ });