@codehz/ecs 0.8.1 → 0.9.0

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 (50) hide show
  1. package/README.en.md +26 -3
  2. package/README.md +28 -3
  3. package/dist/builder.d.mts +296 -46
  4. package/dist/index.d.mts +2 -2
  5. package/dist/index.mjs +2 -2
  6. package/dist/testing.d.mts +1 -1
  7. package/dist/testing.mjs +1 -1
  8. package/dist/world.mjs +452 -179
  9. package/dist/world.mjs.map +1 -1
  10. package/examples/debug-observability.ts +92 -0
  11. package/examples/inventory-system-relations.ts +1 -1
  12. package/examples/parent-child-hierarchy.ts +18 -38
  13. package/package.json +1 -1
  14. package/skills/ecs/SKILL.md +9 -4
  15. package/src/__tests__/component/singleton.test.ts +40 -1
  16. package/src/__tests__/core/archetype.test.ts +155 -13
  17. package/src/__tests__/core/bitset.test.ts +12 -0
  18. package/src/__tests__/entity/entity.test.ts +33 -0
  19. package/src/__tests__/entity/id-system.test.ts +40 -0
  20. package/src/__tests__/perf/comprehensive.perf.test.ts +6 -9
  21. package/src/__tests__/perf/serialization.perf.test.ts +242 -0
  22. package/src/__tests__/perf/{dontfragment-wildcard.perf.test.ts → sparse-wildcard.perf.test.ts} +13 -16
  23. package/src/__tests__/query/caching.test.ts +62 -0
  24. package/src/__tests__/query/filter.test.ts +16 -22
  25. package/src/__tests__/query/perf.test.ts +3 -5
  26. package/src/__tests__/relations/hierarchy.test.ts +208 -0
  27. package/src/__tests__/relations/{dont-fragment → sparse}/basic.test.ts +64 -69
  28. package/src/__tests__/relations/{dont-fragment → sparse}/query-notification.test.ts +17 -9
  29. package/src/__tests__/serialization/bounds.test.ts +134 -1
  30. package/src/__tests__/world/commands.test.ts +337 -0
  31. package/src/__tests__/world/debug-stats.test.ts +206 -0
  32. package/src/__tests__/world/multi-component-hooks.test.ts +44 -0
  33. package/src/__tests__/world/serialize.test.ts +17 -0
  34. package/src/__tests__/world/wildcard-relation-hooks.test.ts +127 -0
  35. package/src/archetype/archetype.ts +96 -46
  36. package/src/archetype/helpers.ts +7 -29
  37. package/src/archetype/store.ts +35 -20
  38. package/src/commands/buffer.ts +5 -2
  39. package/src/commands/changeset.ts +0 -31
  40. package/src/component/registry.ts +64 -63
  41. package/src/entity/index.ts +6 -3
  42. package/src/index.ts +13 -0
  43. package/src/query/filter.ts +4 -10
  44. package/src/query/query.ts +12 -12
  45. package/src/storage/serialization.ts +29 -2
  46. package/src/types/index.ts +71 -0
  47. package/src/world/commands.ts +44 -56
  48. package/src/world/hooks.ts +8 -0
  49. package/src/world/serialization.ts +32 -18
  50. package/src/world/world.ts +387 -20
@@ -117,7 +117,7 @@ describe("Comprehensive ECS performance benchmarks", () => {
117
117
  }
118
118
  world.sync();
119
119
 
120
- const movementQuery = world.createQuery([Position, Velocity]);
120
+ using movementQuery = world.createQuery([Position, Velocity]);
121
121
 
122
122
  // Pure iteration (no writes)
123
123
  const readAvg = benchmark("10k entities: forEach read-only query", 2, 6, () => {
@@ -137,8 +137,6 @@ describe("Comprehensive ECS performance benchmarks", () => {
137
137
  });
138
138
  });
139
139
 
140
- movementQuery.dispose();
141
-
142
140
  console.log(`Sum X (to prevent optimization): ${sumX}`);
143
141
  expect(readAvg).toBeLessThan(20);
144
142
  expect(updateAvg).toBeLessThan(20);
@@ -195,7 +193,7 @@ describe("Comprehensive ECS performance benchmarks", () => {
195
193
  }
196
194
  world.sync();
197
195
 
198
- const movementQuery = world.createQuery([Position, Health]);
196
+ using movementQuery = world.createQuery([Position, Health]);
199
197
 
200
198
  const mixedAvg = benchmark("2k entities: mixed ops (update 90%, spawn 5%, delete 5%) + sync", 2, 6, (round) => {
201
199
  const deleteCount = Math.floor(entities.length * 0.05);
@@ -226,7 +224,6 @@ describe("Comprehensive ECS performance benchmarks", () => {
226
224
  world.sync();
227
225
  });
228
226
 
229
- movementQuery.dispose();
230
227
  expect(mixedAvg).toBeLessThan(300);
231
228
  });
232
229
 
@@ -265,12 +262,12 @@ describe("Comprehensive ECS performance benchmarks", () => {
265
262
  });
266
263
 
267
264
  /**
268
- * Benchmark 7: dontFragment relation updates (the existing benchmark scenario)
265
+ * Benchmark 7: sparse relation updates
269
266
  */
270
- it("should handle dontFragment exclusive relation flips efficiently", () => {
267
+ it("should handle exclusive sparse relation flips efficiently", () => {
271
268
  const world = new World();
272
269
  const Position = component<{ x: number; y: number }>();
273
- const ChildOf = component({ dontFragment: true, exclusive: true });
270
+ const ChildOf = component({ sparse: true, exclusive: true });
274
271
 
275
272
  const parentA = world.new();
276
273
  const parentB = world.new();
@@ -285,7 +282,7 @@ describe("Comprehensive ECS performance benchmarks", () => {
285
282
  }
286
283
  world.sync();
287
284
 
288
- const relationFlipAvg = benchmark("4k entities: exclusive dontFragment relation flip + sync", 2, 8, (round) => {
285
+ const relationFlipAvg = benchmark("4k entities: exclusive sparse relation flip + sync", 2, 8, (round) => {
289
286
  const target = round % 2 === 0 ? parentB : parentA;
290
287
  for (let i = 0; i < entities.length; i++) {
291
288
  world.set(entities[i]!, relation(ChildOf, target));
@@ -0,0 +1,242 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { component, relation, type EntityId } from "../../entity";
3
+ import { World } from "../../world/world";
4
+
5
+ function benchmark(label: string, warmupRounds: number, measuredRounds: number, fn: (round: number) => void): number {
6
+ const durations: number[] = [];
7
+
8
+ const totalRounds = warmupRounds + measuredRounds;
9
+ for (let round = 0; round < totalRounds; round++) {
10
+ const start = performance.now();
11
+ fn(round);
12
+ const duration = performance.now() - start;
13
+ if (round >= warmupRounds) {
14
+ durations.push(duration);
15
+ }
16
+ }
17
+
18
+ const average = durations.reduce((sum, duration) => sum + duration, 0) / durations.length;
19
+ console.log(
20
+ `${label}: avg ${average.toFixed(2)}ms after ${warmupRounds} warmup rounds (${durations
21
+ .map((d) => d.toFixed(2))
22
+ .join("ms, ")}ms per measured round)`,
23
+ );
24
+ return average;
25
+ }
26
+
27
+ /**
28
+ * Serialization performance benchmarks.
29
+ *
30
+ * These benchmarks validate the post-optimization serialization path
31
+ * (see optimization plan for src/world/serialization.ts):
32
+ *
33
+ * Key optimizations exercised:
34
+ * - Column-oriented direct export from Archetype (bypasses per-entity Map + dump())
35
+ * - Per-archetype pre-encoding of component type IDs
36
+ * - ID encoding cache (encodeEntityIdCached) for repeated component/relation IDs
37
+ * - Removal of redundant per-entity work in deserialization
38
+ *
39
+ * Target scale: 8k–12k entities across multiple archetypes + relations.
40
+ * This is large enough to show meaningful differences while keeping test runtime reasonable.
41
+ */
42
+ describe("Serialization performance (post-optimization baseline)", () => {
43
+ it("should serialize and deserialize large mixed worlds efficiently", () => {
44
+ const world = new World();
45
+
46
+ // Named components (realistic serialization path with name lookup)
47
+ const Position = component<{ x: number; y: number }>("Position");
48
+ const Velocity = component<{ vx: number; vy: number }>("Velocity");
49
+ const Health = component<{ hp: number; maxHp: number }>("Health");
50
+ const Name = component<{ value: string }>("Name");
51
+ const Inventory = component<{ items: string[] }>("Inventory");
52
+
53
+ // Entity-valued component (creates entity references)
54
+ const Target = component<{ entity: EntityId }>("Target");
55
+
56
+ // Relations
57
+ const ChildOf = component<void>("ChildOf");
58
+
59
+ const entityCount = 12_000;
60
+ const entities: EntityId[] = [];
61
+
62
+ // Distribute entities across several archetypes for realistic archetype diversity
63
+ // Archetype A: Position + Velocity + Health
64
+ // Archetype B: Position + Name + Inventory
65
+ // Archetype C: Position + Velocity + Target (entity-valued component)
66
+ // Archetype D: Position only (minimal)
67
+
68
+ const parents: EntityId[] = [];
69
+
70
+ for (let i = 0; i < entityCount; i++) {
71
+ const entity = world.new();
72
+ entities.push(entity);
73
+
74
+ const archetypeKind = i % 4;
75
+
76
+ world.set(entity, Position, { x: i, y: i * 2 });
77
+
78
+ if (archetypeKind === 0) {
79
+ // Archetype A
80
+ world.set(entity, Velocity, { vx: 1, vy: 0.5 });
81
+ world.set(entity, Health, { hp: 100, maxHp: 100 });
82
+ } else if (archetypeKind === 1) {
83
+ // Archetype B
84
+ world.set(entity, Name, { value: `Entity-${i}` });
85
+ world.set(entity, Inventory, { items: ["sword", "potion"] });
86
+ } else if (archetypeKind === 2) {
87
+ // Archetype C — has entity reference
88
+ world.set(entity, Velocity, { vx: 0.2, vy: -1 });
89
+ // Point to a previous entity (creates realistic entity-valued component)
90
+ const targetIdx = Math.max(0, i - 7);
91
+ world.set(entity, Target, { entity: entities[targetIdx]! });
92
+ } else {
93
+ // Archetype D — minimal
94
+ // Only Position
95
+ }
96
+
97
+ // Every 17th entity becomes a parent and gets some children via relations
98
+ if (i % 17 === 0) {
99
+ parents.push(entity);
100
+ }
101
+ }
102
+
103
+ // Add relations (ChildOf) — creates entity-relation IDs that must be encoded
104
+ for (let i = 0; i < entityCount; i++) {
105
+ const parentIdx = Math.floor(i / 8) % Math.max(1, parents.length);
106
+ const parent = parents[parentIdx] ?? entities[0]!;
107
+ if (parent !== entities[i]) {
108
+ world.set(entities[i]!, relation(ChildOf, parent));
109
+ }
110
+ }
111
+
112
+ world.sync();
113
+
114
+ expect(entities.length).toBe(entityCount);
115
+
116
+ const warmup = 2;
117
+ const measured = 5;
118
+
119
+ // === Serialize ===
120
+ let lastSnapshot: ReturnType<World["serialize"]> | null = null;
121
+
122
+ const serializeAvg = benchmark(
123
+ `serialize ${entityCount} entities (mixed archetypes + relations)`,
124
+ warmup,
125
+ measured,
126
+ () => {
127
+ lastSnapshot = world.serialize();
128
+ },
129
+ );
130
+
131
+ expect(lastSnapshot).toBeDefined();
132
+ expect(lastSnapshot!.entities.length).toBeGreaterThanOrEqual(entityCount * 0.9); // rough sanity
133
+
134
+ // Measure rough heap impact of a serialize call.
135
+ // Note: Bun may require --smol or explicit GC for more stable allocation numbers.
136
+ if (typeof Bun !== "undefined" && Bun.gc) {
137
+ Bun.gc(true);
138
+ }
139
+ const memBefore = process.memoryUsage();
140
+ void world.serialize();
141
+ const memAfter = process.memoryUsage();
142
+ const heapDeltaMB = ((memAfter.heapUsed - memBefore.heapUsed) / 1024 / 1024).toFixed(2);
143
+ console.log(
144
+ `serialize heap delta (one call): ~${heapDeltaMB} MB (rss delta: ${((memAfter.rss - memBefore.rss) / 1024 / 1024).toFixed(2)} MB)`,
145
+ );
146
+
147
+ // === Deserialize (new World from snapshot) ===
148
+ const deserializeAvg = benchmark(
149
+ `deserialize ${entityCount} entities (new World(snapshot))`,
150
+ warmup,
151
+ measured,
152
+ () => {
153
+ // We create and immediately let go of the world to measure allocation + construction cost
154
+ const restored = new World(lastSnapshot!);
155
+ // Touch one value to prevent dead-code elimination in theory
156
+ if (restored.exists(entities[0]!)) {
157
+ void restored.get(entities[0]!, Position);
158
+ }
159
+ },
160
+ );
161
+
162
+ // === Full JSON roundtrip (very common user pattern) ===
163
+ const jsonRoundtripAvg = benchmark(
164
+ `full JSON roundtrip (stringify + parse + new World) — ${entityCount} entities`,
165
+ warmup,
166
+ measured,
167
+ () => {
168
+ const json = JSON.stringify(world.serialize());
169
+ const parsed = JSON.parse(json);
170
+ const restored = new World(parsed);
171
+ if (restored.exists(entities[42]!)) {
172
+ void restored.get(entities[42]!, Position);
173
+ }
174
+ },
175
+ );
176
+
177
+ // Loose upper bounds — these act as regression guards.
178
+ // The numbers are intentionally generous to account for CI machine variance.
179
+ // The main value is the detailed console output for manual before/after comparison.
180
+ expect(serializeAvg).toBeLessThan(80); // ~12k entities serialize
181
+ expect(deserializeAvg).toBeLessThan(120); // new World(snapshot) tends to be heavier
182
+ expect(jsonRoundtripAvg).toBeLessThan(200); // includes JSON + full deserialize
183
+
184
+ // Final sanity: the last deserialized world should still have most entities
185
+ const finalRestored = new World(lastSnapshot!);
186
+ expect(finalRestored.exists(entities[0]!)).toBe(true);
187
+ expect(finalRestored.exists(entities[entityCount - 1]!)).toBe(true);
188
+ });
189
+
190
+ it("should handle worlds with heavy entity-relation usage", () => {
191
+ const world = new World();
192
+
193
+ const Position = component<{ x: number; y: number }>("Pos");
194
+ const Owes = component<{ amount: number }>("Owes"); // used for relations
195
+
196
+ const entityCount = 8_000;
197
+ const entities: EntityId[] = [];
198
+
199
+ for (let i = 0; i < entityCount; i++) {
200
+ const e = world.new();
201
+ entities.push(e);
202
+ world.set(e, Position, { x: i, y: i });
203
+ }
204
+
205
+ // Create a dense web of entity-relations (every entity owes 3 others)
206
+ for (let i = 0; i < entityCount; i++) {
207
+ for (let j = 1; j <= 3; j++) {
208
+ const target = entities[(i + j * 17) % entityCount]!;
209
+ if (target !== entities[i]) {
210
+ world.set(entities[i]!, relation(Owes, target), { amount: (i + j) % 100 });
211
+ }
212
+ }
213
+ }
214
+
215
+ world.sync();
216
+
217
+ const warmup = 1;
218
+ const measured = 4;
219
+
220
+ const serializeAvg = benchmark(
221
+ `serialize ${entityCount} entities (dense entity-relations)`,
222
+ warmup,
223
+ measured,
224
+ () => {
225
+ void world.serialize();
226
+ },
227
+ );
228
+
229
+ // Deserialize with many relations exercises decode + reference tracking
230
+ let snapshot: ReturnType<World["serialize"]> | undefined;
231
+
232
+ const deserializeAvg = benchmark(`deserialize ${entityCount} entities (dense relations)`, warmup, measured, () => {
233
+ if (!snapshot) snapshot = world.serialize();
234
+ const w = new World(snapshot);
235
+ void w;
236
+ });
237
+
238
+ // Very loose bounds — this scenario is intentionally expensive
239
+ expect(serializeAvg).toBeLessThan(150);
240
+ expect(deserializeAvg).toBeLessThan(220);
241
+ });
242
+ });
@@ -3,10 +3,10 @@ import { component, relation, type EntityId } from "../../entity";
3
3
  import { World } from "../../world/world";
4
4
 
5
5
  /**
6
- * Focused performance tests for the refactored DontFragmentStore.
6
+ * Focused performance tests for sparse relation storage.
7
7
  *
8
- * These tests specifically exercise the X-class paths that motivated the refactor:
9
- * - Wildcard queries over dontFragment relations (relation(Comp, "*"))
8
+ * These tests specifically exercise the hot paths:
9
+ * - Wildcard queries over sparse relations (relation(Comp, "*"))
10
10
  * - Frequent exclusive relation flips (the classic ChildOf pattern)
11
11
  * - hasRelationWithComponentId / archetype filtering cost
12
12
  */
@@ -31,11 +31,11 @@ function benchmark(label: string, warmupRounds: number, measuredRounds: number,
31
31
  return average;
32
32
  }
33
33
 
34
- describe("DontFragment + Wildcard Performance (post-refactor)", () => {
35
- it("should handle large numbers of entities with exclusive dontFragment + wildcard queries efficiently", () => {
34
+ describe("Sparse + Wildcard Performance", () => {
35
+ it("should handle large numbers of entities with exclusive sparse relations + wildcard queries efficiently", () => {
36
36
  const world = new World();
37
37
  const Position = component<{ x: number; y: number }>();
38
- const ChildOf = component({ dontFragment: true, exclusive: true });
38
+ const ChildOf = component({ sparse: true, exclusive: true });
39
39
 
40
40
  const parentCount = 20;
41
41
  const parents: EntityId[] = [];
@@ -56,9 +56,9 @@ describe("DontFragment + Wildcard Performance (post-refactor)", () => {
56
56
  world.sync();
57
57
 
58
58
  const wildcard = relation(ChildOf, "*");
59
- const q = world.createQuery([Position, wildcard]);
59
+ using q = world.createQuery([Position, wildcard]);
60
60
 
61
- const avg = benchmark("10k entities: wildcard query over exclusive dontFragment", 2, 6, () => {
61
+ const avg = benchmark("10k entities: wildcard query over exclusive sparse relations", 2, 6, () => {
62
62
  let count = 0;
63
63
  q.forEach([Position, wildcard], (_entity, _pos, rels) => {
64
64
  count += rels.length; // force materialization
@@ -66,16 +66,13 @@ describe("DontFragment + Wildcard Performance (post-refactor)", () => {
66
66
  expect(count).toBeGreaterThan(0);
67
67
  });
68
68
 
69
- q.dispose();
70
-
71
- // These numbers will be tuned after the implementation stabilizes.
72
- // The goal is to verify we did not regress the hot wildcard + dontFragment path.
73
- expect(avg).toBeLessThan(50); // generous upper bound post-refactor
69
+ // The goal is to verify we did not regress the hot wildcard + sparse storage path.
70
+ expect(avg).toBeLessThan(50); // generous upper bound
74
71
  });
75
72
 
76
- it("should support frequent exclusive dontFragment flips without leaking relations", () => {
73
+ it("should support frequent exclusive sparse relation flips without leaking relations", () => {
77
74
  const world = new World();
78
- const ChildOf = component({ dontFragment: true, exclusive: true });
75
+ const ChildOf = component({ sparse: true, exclusive: true });
79
76
 
80
77
  const parentA = world.new();
81
78
  const parentB = world.new();
@@ -90,7 +87,7 @@ describe("DontFragment + Wildcard Performance (post-refactor)", () => {
90
87
  world.sync();
91
88
 
92
89
  // Flip many times
93
- const flipAvg = benchmark("2k entities: exclusive dontFragment flip (100 rounds)", 1, 3, (round) => {
90
+ const flipAvg = benchmark("2k entities: exclusive sparse relation flip (100 rounds)", 1, 3, (round) => {
94
91
  const target = round % 2 === 0 ? parentB : parentA;
95
92
  for (const e of entities) {
96
93
  world.set(e, relation(ChildOf, target));
@@ -109,4 +109,66 @@ describe("Query", () => {
109
109
  expect(query1).not.toBe(query3);
110
110
  });
111
111
  });
112
+
113
+ describe("Query disposal and archetype removal notification", () => {
114
+ type Pos = { x: number };
115
+ const PosC = component<Pos>();
116
+
117
+ it("should support dispose and disposed flag, and release via registry refcount", () => {
118
+ const world = new World();
119
+ const q1 = world.createQuery([PosC]);
120
+ expect(q1.disposed).toBe(false);
121
+
122
+ q1.dispose();
123
+ // After dispose, further use may be no-op but flag set when ref hits 0
124
+ // Note: createQuery reuses, so need unique to drop ref
125
+ });
126
+
127
+ it("should trigger removeArchetype on query when archetypes are cleaned up", () => {
128
+ const world = new World();
129
+ const q = world.createQuery([PosC]);
130
+
131
+ // Spawn then delete all of archetype to trigger cleanup
132
+ const e1 = world.spawn().with(PosC, { x: 1 }).build();
133
+ const e2 = world.spawn().with(PosC, { x: 2 }).build();
134
+ world.sync();
135
+
136
+ expect(q.getEntities().length).toBe(2);
137
+
138
+ world.delete(e1);
139
+ world.delete(e2);
140
+ world.sync();
141
+
142
+ // Archetype should be removed, query notified (no crash, internal removeArchetype called)
143
+ expect(q.getEntities().length).toBe(0);
144
+
145
+ // Also test Symbol.dispose via fixture or direct (dispose marks)
146
+ q.dispose();
147
+ expect(q.disposed).toBe(true);
148
+ });
149
+
150
+ it("should directly cover Query removeArchetype and dispose internal branches", () => {
151
+ const world = new World();
152
+ const q = world.createQuery([PosC]);
153
+ // Force an archetype in cache then remove it (covers splice path)
154
+ const dummy: any = {};
155
+ (q as any).cachedArchetypes.push(dummy);
156
+ (q as any).removeArchetype(dummy);
157
+ expect((q as any).cachedArchetypes).not.toContain(dummy);
158
+
159
+ // cover disposed early return
160
+ (q as any).isDisposed = true;
161
+ (q as any).removeArchetype({} as any); // no-op
162
+ (q as any).checkNewArchetype({} as any); // no-op
163
+
164
+ // cover _disposeInternal again
165
+ (q as any)._disposeInternal();
166
+
167
+ // cover disposed getter (line ~310)
168
+ void (q as any).disposed;
169
+
170
+ // cover Symbol.dispose method
171
+ (q as any)[Symbol.dispose]();
172
+ });
173
+ });
112
174
  });
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
  import { Archetype } from "../../archetype/archetype";
3
- import { DontFragmentStoreImpl } from "../../archetype/store";
3
+ import { SparseStoreImpl } from "../../archetype/store";
4
4
  import type { ComponentId, EntityId } from "../../entity";
5
5
  import { relation } from "../../entity";
6
6
  import { matchesComponentTypes, matchesFilter, type QueryFilter } from "../../query/filter";
@@ -11,34 +11,31 @@ const velocityComponent = 2 as ComponentId<{ dx: number; dy: number }>;
11
11
  const healthComponent = 3 as ComponentId<{ value: number }>;
12
12
  const relationComponent = 4 as ComponentId<{ strength: number }>;
13
13
 
14
- // Helper function to create a real DontFragmentStore for testing.
15
- const createDontFragmentRelations = () => new DontFragmentStoreImpl();
14
+ // Helper function to create a real SparseStore for testing.
15
+ const createSparseStore = () => new SparseStoreImpl();
16
16
 
17
17
  describe("Query Filter Functions", () => {
18
18
  describe("matchesComponentTypes", () => {
19
19
  it("should return true when archetype contains all required component types", () => {
20
- const archetype = new Archetype([positionComponent, velocityComponent], createDontFragmentRelations());
20
+ const archetype = new Archetype([positionComponent, velocityComponent], createSparseStore());
21
21
  const componentTypes = [positionComponent, velocityComponent];
22
22
  expect(matchesComponentTypes(archetype, componentTypes)).toBe(true);
23
23
  });
24
24
 
25
25
  it("should return true when archetype contains required component types and more", () => {
26
- const archetype = new Archetype(
27
- [positionComponent, velocityComponent, healthComponent],
28
- createDontFragmentRelations(),
29
- );
26
+ const archetype = new Archetype([positionComponent, velocityComponent, healthComponent], createSparseStore());
30
27
  const componentTypes = [positionComponent, velocityComponent];
31
28
  expect(matchesComponentTypes(archetype, componentTypes)).toBe(true);
32
29
  });
33
30
 
34
31
  it("should return false when archetype is missing a required component type", () => {
35
- const archetype = new Archetype([positionComponent], createDontFragmentRelations());
32
+ const archetype = new Archetype([positionComponent], createSparseStore());
36
33
  const componentTypes = [positionComponent, velocityComponent];
37
34
  expect(matchesComponentTypes(archetype, componentTypes)).toBe(false);
38
35
  });
39
36
 
40
37
  it("should return true for empty component types array", () => {
41
- const archetype = new Archetype([positionComponent], createDontFragmentRelations());
38
+ const archetype = new Archetype([positionComponent], createSparseStore());
42
39
  const componentTypes: EntityId<any>[] = [];
43
40
  expect(matchesComponentTypes(archetype, componentTypes)).toBe(true);
44
41
  });
@@ -46,41 +43,38 @@ describe("Query Filter Functions", () => {
46
43
 
47
44
  describe("matchesFilter", () => {
48
45
  it("should return true when no negative component types are specified", () => {
49
- const archetype = new Archetype([positionComponent, velocityComponent], createDontFragmentRelations());
46
+ const archetype = new Archetype([positionComponent, velocityComponent], createSparseStore());
50
47
  const filter: QueryFilter = {};
51
48
  expect(matchesFilter(archetype, filter)).toBe(true);
52
49
  });
53
50
 
54
51
  it("should return true when archetype does not contain any negative component types", () => {
55
- const archetype = new Archetype([positionComponent, velocityComponent], createDontFragmentRelations());
52
+ const archetype = new Archetype([positionComponent, velocityComponent], createSparseStore());
56
53
  const filter: QueryFilter = { negativeComponentTypes: [healthComponent] };
57
54
  expect(matchesFilter(archetype, filter)).toBe(true);
58
55
  });
59
56
 
60
57
  it("should return false when archetype contains a negative component type", () => {
61
- const archetype = new Archetype(
62
- [positionComponent, velocityComponent, healthComponent],
63
- createDontFragmentRelations(),
64
- );
58
+ const archetype = new Archetype([positionComponent, velocityComponent, healthComponent], createSparseStore());
65
59
  const filter: QueryFilter = { negativeComponentTypes: [healthComponent] };
66
60
  expect(matchesFilter(archetype, filter)).toBe(false);
67
61
  });
68
62
 
69
63
  it("should return false when archetype contains any of multiple negative component types", () => {
70
- const archetype = new Archetype([positionComponent, healthComponent], createDontFragmentRelations());
64
+ const archetype = new Archetype([positionComponent, healthComponent], createSparseStore());
71
65
  const filter: QueryFilter = { negativeComponentTypes: [velocityComponent, healthComponent] };
72
66
  expect(matchesFilter(archetype, filter)).toBe(false);
73
67
  });
74
68
 
75
69
  it("should return true when archetype contains none of multiple negative component types", () => {
76
- const archetype = new Archetype([positionComponent], createDontFragmentRelations());
70
+ const archetype = new Archetype([positionComponent], createSparseStore());
77
71
  const filter: QueryFilter = { negativeComponentTypes: [velocityComponent, healthComponent] };
78
72
  expect(matchesFilter(archetype, filter)).toBe(true);
79
73
  });
80
74
 
81
75
  it("should return false when archetype contains a negative wildcard relation component", () => {
82
76
  const wildcardRelation = relation(relationComponent, "*");
83
- const archetype = new Archetype([positionComponent, wildcardRelation], createDontFragmentRelations());
77
+ const archetype = new Archetype([positionComponent, wildcardRelation], createSparseStore());
84
78
  const filter: QueryFilter = { negativeComponentTypes: [wildcardRelation] };
85
79
  expect(matchesFilter(archetype, filter)).toBe(false);
86
80
  });
@@ -88,7 +82,7 @@ describe("Query Filter Functions", () => {
88
82
  it("should return false when archetype contains a specific relation matching negative wildcard filter", () => {
89
83
  const wildcardRelation = relation(relationComponent, "*");
90
84
  const otherRelation = relation(relationComponent, 1025 as EntityId);
91
- const archetype = new Archetype([positionComponent, otherRelation], createDontFragmentRelations());
85
+ const archetype = new Archetype([positionComponent, otherRelation], createSparseStore());
92
86
  const filter: QueryFilter = { negativeComponentTypes: [wildcardRelation] };
93
87
  expect(matchesFilter(archetype, filter)).toBe(false);
94
88
  });
@@ -96,7 +90,7 @@ describe("Query Filter Functions", () => {
96
90
  it("should return true when archetype does not contain any relations with the wildcard component", () => {
97
91
  const wildcardRelation = relation(relationComponent, "*");
98
92
  const otherComponent = 5 as EntityId<{ other: number }>;
99
- const archetype = new Archetype([positionComponent, otherComponent], createDontFragmentRelations());
93
+ const archetype = new Archetype([positionComponent, otherComponent], createSparseStore());
100
94
  const filter: QueryFilter = { negativeComponentTypes: [wildcardRelation] };
101
95
  expect(matchesFilter(archetype, filter)).toBe(true);
102
96
  });
@@ -104,7 +98,7 @@ describe("Query Filter Functions", () => {
104
98
  it("should return false when archetype contains wildcard relation matching negative filter", () => {
105
99
  const wildcardRelation = relation(relationComponent, "*");
106
100
  const matchingRelation = relation(relationComponent, 1026 as EntityId);
107
- const archetype = new Archetype([positionComponent, matchingRelation], createDontFragmentRelations());
101
+ const archetype = new Archetype([positionComponent, matchingRelation], createSparseStore());
108
102
  const filter: QueryFilter = { negativeComponentTypes: [wildcardRelation] };
109
103
  expect(matchesFilter(archetype, filter)).toBe(false);
110
104
  });
@@ -51,8 +51,8 @@ function performanceTest() {
51
51
  console.log(`Entity creation time: ${(endCreate - startCreate).toFixed(2)}ms`);
52
52
 
53
53
  // Create queries
54
- const positionVelocityQuery = world.createQuery([positionComponent, velocityComponent]);
55
- const healthQuery = world.createQuery([healthComponent]);
54
+ using positionVelocityQuery = world.createQuery([positionComponent, velocityComponent]);
55
+ using _healthQuery = world.createQuery([healthComponent]);
56
56
 
57
57
  // Test getEntitiesWithComponents performance
58
58
  console.log("\nTesting getEntitiesWithComponents performance...");
@@ -89,9 +89,7 @@ function performanceTest() {
89
89
  });
90
90
  console.log(`forEach iterated over ${forEachCount} entities`);
91
91
 
92
- // Cleanup
93
- positionVelocityQuery.dispose();
94
- healthQuery.dispose();
92
+ // Cleanup handled by using declarations for positionVelocityQuery / healthQuery
95
93
 
96
94
  console.log("\nPerformance test completed!");
97
95
  }