@codehz/ecs 0.9.0 → 0.10.1

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.
@@ -216,7 +216,7 @@ function main() {
216
216
  console.log("=========================================================\n");
217
217
 
218
218
  // Create singleton SpatialGrid
219
- world.set(SpatialGrid, { cells: new Map(), cellSize: 64 });
219
+ world.singleton(SpatialGrid).set({ cells: new Map(), cellSize: 64 });
220
220
  console.log("SpatialGrid singleton created (cellSize=64)");
221
221
 
222
222
  // Create 1 player near the center
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codehz/ecs",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "license": "MIT",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -3,6 +3,8 @@ import { ComponentEntityStore } from "../../component/entity-store";
3
3
  import { component, createEntityId, relation, type EntityId } from "../../entity";
4
4
  import { World } from "../../world/world";
5
5
 
6
+ function expectType<T>(_value: T): void {}
7
+
6
8
  describe("World - Singleton Component", () => {
7
9
  type GlobalConfig = { debug: boolean; version: string };
8
10
  type GameState = { score: number; level: number };
@@ -10,29 +12,125 @@ describe("World - Singleton Component", () => {
10
12
  const GlobalConfigId = component<GlobalConfig>();
11
13
  const GameStateId = component<GameState>();
12
14
 
13
- it("should set singleton component using shorthand syntax", () => {
15
+ it("should set singleton component through the explicit handle", () => {
14
16
  const world = new World();
15
17
  const config: GlobalConfig = { debug: true, version: "1.0.0" };
18
+ const singleton = world.singleton(GlobalConfigId);
16
19
 
17
- // Use singleton syntax: set(componentId, data)
18
- world.set(GlobalConfigId, config);
20
+ singleton.set(config);
19
21
  world.sync();
20
22
 
21
- // Verify it was set on the component entity itself
22
23
  expect(world.has(GlobalConfigId)).toBe(true);
23
24
  expect(world.get(GlobalConfigId)).toEqual(config);
24
25
  });
25
26
 
26
- it("should update singleton component using shorthand syntax", () => {
27
+ it("should interpret 2-argument set on a component entity as a void component set", () => {
28
+ const world = new World();
29
+ const singleton = world.singleton(GlobalConfigId);
30
+ const Marker = component<void>();
31
+ const originalWarn = console.warn;
32
+ const warnings: string[] = [];
33
+
34
+ console.warn = (...args: unknown[]) => {
35
+ warnings.push(args.join(" "));
36
+ };
37
+
38
+ try {
39
+ world.set(GlobalConfigId, Marker);
40
+ } finally {
41
+ console.warn = originalWarn;
42
+ }
43
+
44
+ world.sync();
45
+
46
+ expect(world.has(GlobalConfigId, Marker)).toBe(true);
47
+ expect(singleton.has()).toBe(false);
48
+ expect(warnings).toHaveLength(0);
49
+ });
50
+
51
+ it("should support the deprecated singleton data shorthand for non-number values", () => {
52
+ const world = new World();
53
+ const config: GlobalConfig = { debug: true, version: "1.0.0" };
54
+ const originalWarn = console.warn;
55
+ const warnings: string[] = [];
56
+
57
+ if (false) {
58
+ expectType<void>(world.set(GlobalConfigId, config));
59
+ }
60
+
61
+ console.warn = (...args: unknown[]) => {
62
+ warnings.push(args.join(" "));
63
+ };
64
+
65
+ try {
66
+ world.set(GlobalConfigId, config);
67
+ } finally {
68
+ console.warn = originalWarn;
69
+ }
70
+
71
+ world.sync();
72
+
73
+ expect(world.get(GlobalConfigId)).toEqual(config);
74
+ expect(warnings).toHaveLength(1);
75
+ expect(warnings[0]).toContain("deprecated");
76
+ expect(warnings[0]).toContain("world.singleton(componentId).set(value)");
77
+ });
78
+
79
+ it("should not expose the deprecated shorthand for numeric singleton types at the type level", () => {
80
+ const world = new World();
81
+ const Score = component<number>();
82
+
83
+ if (false) {
84
+ // @ts-expect-error Numeric singleton shorthand is intentionally unsupported.
85
+ expectType<void>(world.set(Score, 123));
86
+ }
87
+
88
+ expect(true).toBe(true);
89
+ });
90
+
91
+ it("should manage singleton data through an explicit handle", () => {
92
+ const world = new World();
93
+ const config = world.singleton(GlobalConfigId);
94
+
95
+ expect(config.getOptional()).toBeUndefined();
96
+ expect(config.has()).toBe(false);
97
+
98
+ config.set({ debug: true, version: "1.0.0" });
99
+ world.sync();
100
+
101
+ expect(config.has()).toBe(true);
102
+ expect(config.get()).toEqual({ debug: true, version: "1.0.0" });
103
+
104
+ config.remove();
105
+ world.sync();
106
+
107
+ expect(config.has()).toBe(false);
108
+ expect(config.getOptional()).toBeUndefined();
109
+ });
110
+
111
+ it("should support void singleton components through an explicit handle", () => {
112
+ const world = new World();
113
+ const Tag = component<void>();
114
+ const tag = world.singleton(Tag);
115
+
116
+ tag.set();
117
+ world.sync();
118
+
119
+ expect(tag.has()).toBe(true);
120
+ expect(world.has(Tag)).toBe(true);
121
+ });
122
+
123
+ it("should update singleton component through the explicit handle", () => {
27
124
  const world = new World();
28
125
  const config1: GlobalConfig = { debug: true, version: "1.0.0" };
29
126
  const config2: GlobalConfig = { debug: false, version: "2.0.0" };
127
+ const singleton = world.singleton(GlobalConfigId);
30
128
 
31
- world.set(GlobalConfigId, config1);
129
+ singleton.set(config1);
32
130
  world.sync();
33
131
  expect(world.get(GlobalConfigId)).toEqual(config1);
34
132
 
35
- world.set(GlobalConfigId, config2);
133
+ singleton.set(config2);
36
134
  world.sync();
37
135
  expect(world.get(GlobalConfigId)).toEqual(config2);
38
136
  });
@@ -42,15 +140,12 @@ describe("World - Singleton Component", () => {
42
140
  const world2 = new World();
43
141
  const config: GlobalConfig = { debug: true, version: "1.0.0" };
44
142
 
45
- // Singleton syntax
46
- world1.set(GlobalConfigId, config);
143
+ world1.singleton(GlobalConfigId).set(config);
47
144
  world1.sync();
48
145
 
49
- // Traditional syntax
50
146
  world2.set(GlobalConfigId, GlobalConfigId, config);
51
147
  world2.sync();
52
148
 
53
- // Both should have the same result
54
149
  expect(world1.get(GlobalConfigId)).toEqual(world2.get(GlobalConfigId));
55
150
  });
56
151
 
@@ -59,8 +154,8 @@ describe("World - Singleton Component", () => {
59
154
  const config: GlobalConfig = { debug: true, version: "1.0.0" };
60
155
  const state: GameState = { score: 100, level: 5 };
61
156
 
62
- world.set(GlobalConfigId, config);
63
- world.set(GameStateId, state);
157
+ world.singleton(GlobalConfigId).set(config);
158
+ world.singleton(GameStateId).set(state);
64
159
  world.sync();
65
160
 
66
161
  expect(world.get(GlobalConfigId)).toEqual(config);
@@ -78,19 +173,17 @@ describe("World - Singleton Component", () => {
78
173
  }).toThrow("does not exist");
79
174
  });
80
175
 
81
- it("should check singleton component existence using shorthand syntax", () => {
176
+ it("should check singleton component existence through the explicit handle", () => {
82
177
  const world = new World();
83
178
  const config: GlobalConfig = { debug: true, version: "1.0.0" };
179
+ const singleton = world.singleton(GlobalConfigId);
84
180
 
85
- // Before setting, should return false
86
- expect(world.has(GlobalConfigId)).toBe(false);
181
+ expect(singleton.has()).toBe(false);
87
182
 
88
- // Set singleton component
89
- world.set(GlobalConfigId, config);
183
+ singleton.set(config);
90
184
  world.sync();
91
185
 
92
- // After setting, should return true
93
- expect(world.has(GlobalConfigId)).toBe(true);
186
+ expect(singleton.has()).toBe(true);
94
187
  });
95
188
 
96
189
  it("should be equivalent to has(comp, comp)", () => {
@@ -98,29 +191,26 @@ describe("World - Singleton Component", () => {
98
191
  const world2 = new World();
99
192
  const config: GlobalConfig = { debug: true, version: "1.0.0" };
100
193
 
101
- // Singleton syntax
102
- world1.set(GlobalConfigId, config);
194
+ world1.singleton(GlobalConfigId).set(config);
103
195
  world1.sync();
104
196
 
105
- // Traditional syntax
106
197
  world2.set(GlobalConfigId, GlobalConfigId, config);
107
198
  world2.sync();
108
199
 
109
- // Both should have the same result
110
200
  expect(world1.has(GlobalConfigId)).toBe(world2.has(GlobalConfigId, GlobalConfigId));
111
201
  expect(world1.has(GlobalConfigId)).toBe(true);
112
202
  });
113
203
 
114
- it("should remove singleton component using shorthand syntax", () => {
204
+ it("should remove singleton component through the explicit handle", () => {
115
205
  const world = new World();
116
206
  const config: GlobalConfig = { debug: true, version: "1.0.0" };
207
+ const singleton = world.singleton(GlobalConfigId);
117
208
 
118
- world.set(GlobalConfigId, config);
209
+ singleton.set(config);
119
210
  world.sync();
120
211
  expect(world.has(GlobalConfigId)).toBe(true);
121
212
 
122
- // Remove using singleton syntax
123
- world.remove(GlobalConfigId);
213
+ singleton.remove();
124
214
  world.sync();
125
215
  expect(world.has(GlobalConfigId)).toBe(false);
126
216
  });
@@ -130,19 +220,16 @@ describe("World - Singleton Component", () => {
130
220
  const world2 = new World();
131
221
  const config: GlobalConfig = { debug: true, version: "1.0.0" };
132
222
 
133
- // Set on both worlds
134
- world1.set(GlobalConfigId, config);
223
+ world1.singleton(GlobalConfigId).set(config);
135
224
  world1.sync();
136
225
  world2.set(GlobalConfigId, GlobalConfigId, config);
137
226
  world2.sync();
138
227
 
139
- // Remove using different syntax
140
- world1.remove(GlobalConfigId); // Singleton syntax
141
- world2.remove(GlobalConfigId, GlobalConfigId); // Traditional syntax
228
+ world1.singleton(GlobalConfigId).remove();
229
+ world2.remove(GlobalConfigId, GlobalConfigId);
142
230
  world1.sync();
143
231
  world2.sync();
144
232
 
145
- // Both should have the same result
146
233
  expect(world1.has(GlobalConfigId)).toBe(world2.has(GlobalConfigId, GlobalConfigId));
147
234
  expect(world1.has(GlobalConfigId)).toBe(false);
148
235
  });
@@ -239,8 +239,7 @@ describe("Serialization edge cases", () => {
239
239
  it("should serialize and deserialize singleton components (covers componentEntities paths)", () => {
240
240
  const world = new World();
241
241
  const Config = component<{ debug: boolean }>();
242
- // Singleton shorthand populates internal component entity
243
- world.set(Config, { debug: true });
242
+ world.singleton(Config).set({ debug: true });
244
243
  world.sync();
245
244
 
246
245
  const snapshot = world.serialize();
@@ -347,7 +346,7 @@ describe("Serialization edge cases", () => {
347
346
  it("should ignore componentEntities snapshot entries whose id is not a real component entity (covers continue guard)", () => {
348
347
  const world = new World();
349
348
  const Config = component<{ debug: boolean }>();
350
- world.set(Config, { debug: true });
349
+ world.singleton(Config).set({ debug: true });
351
350
  world.sync();
352
351
 
353
352
  const snap: any = world.serialize();
@@ -111,15 +111,16 @@ describe("World - Component Management", () => {
111
111
  const Inbox = component<string[]>({
112
112
  merge: (prev, next) => [...prev, ...next],
113
113
  });
114
+ const inbox = world.singleton(Inbox);
114
115
 
115
- world.set(Inbox, ["A"]);
116
- world.set(Inbox, ["B"]);
116
+ inbox.set(["A"]);
117
+ inbox.set(["B"]);
117
118
  world.sync();
118
119
  expect(world.get(Inbox)).toEqual(["A", "B"]);
119
120
 
120
- world.remove(Inbox);
121
- world.set(Inbox, ["C"]);
122
- world.set(Inbox, ["D"]);
121
+ inbox.remove();
122
+ inbox.set(["C"]);
123
+ inbox.set(["D"]);
123
124
  world.sync();
124
125
  expect(world.get(Inbox)).toEqual(["C", "D"]);
125
126
  });
package/src/index.ts CHANGED
@@ -32,6 +32,8 @@ export type {
32
32
  } from "./storage/serialization";
33
33
  export { EntityBuilder } from "./world/builder";
34
34
  export type { ComponentDef } from "./world/builder";
35
+ export { SingletonHandle } from "./world/singleton";
36
+ export type { SingletonHandleOps } from "./world/singleton";
35
37
  export { World } from "./world/world";
36
38
 
37
39
  // Query class
@@ -0,0 +1,283 @@
1
+ import { Archetype } from "../archetype/archetype";
2
+ import type { SparseStore } from "../archetype/store";
3
+ import { normalizeComponentTypes } from "../component/type-utils";
4
+ import type { EntityId } from "../entity";
5
+ import {
6
+ getComponentIdFromRelationId,
7
+ getDetailedIdType,
8
+ isSparseRelation,
9
+ isSparseWildcard,
10
+ isWildcardRelationId,
11
+ } from "../entity";
12
+ import { matchesFilter } from "../query/filter";
13
+ import type { QueryRegistry } from "../query/registry";
14
+ import type { LifecycleHookEntry } from "../types";
15
+ import { getOrCompute } from "../utils/utils";
16
+ import { filterRegularComponentTypes } from "./commands";
17
+
18
+ /**
19
+ * Context provided to ArchetypeManager for notifying dependent systems
20
+ * (query caching and lifecycle hooks) without creating tight coupling or cycles.
21
+ * Follows the same callback/context injection pattern used by CommandProcessorContext,
22
+ * HooksContext, and WorldDeserializationContext.
23
+ */
24
+ export interface ArchetypeManagerContext {
25
+ queryRegistry: QueryRegistry;
26
+ hooks: Set<LifecycleHookEntry>;
27
+ /** Called only when debug collectors are active (mirrors original guard in World) */
28
+ recordArchetypeCreated?: () => void;
29
+ recordArchetypeRemoved?: () => void;
30
+ }
31
+
32
+ /**
33
+ * Encapsulates all archetype storage, indexing, creation, removal, and reverse
34
+ * referencing logic that was previously scattered as private methods + maps
35
+ * directly on the World class.
36
+ *
37
+ * Responsibilities:
38
+ * - Archetype memoization by signature
39
+ * - Component-type reverse index (archetypesByComponent)
40
+ * - Entity → current Archetype map
41
+ * - Reverse "who references this entity via component/relation" index
42
+ * - Creation + removal with proper notifications to QueryRegistry + hook matching
43
+ * - Cleanup of empty archetypes after entity cascades
44
+ *
45
+ * This extraction shrinks World while keeping the same behavior and hot-path characteristics.
46
+ */
47
+ export class ArchetypeManager {
48
+ // Public for performance (hot paths access these maps frequently).
49
+ // This intentionally breaks encapsulation a bit for speed, as requested.
50
+ readonly archetypes: Archetype[] = [];
51
+ readonly archetypeBySignature = new Map<string, Archetype>();
52
+ readonly entityToArchetype = new Map<EntityId, Archetype>();
53
+ readonly archetypesByComponent = new Map<EntityId<any>, Set<Archetype>>();
54
+ readonly entityToReferencingArchetypes = new Map<EntityId, Set<Archetype>>();
55
+
56
+ private readonly sparseStore: SparseStore;
57
+ private readonly ctx: ArchetypeManagerContext;
58
+
59
+ constructor(ctx: ArchetypeManagerContext, sparseStore: SparseStore) {
60
+ this.ctx = ctx;
61
+ this.sparseStore = sparseStore;
62
+ }
63
+
64
+ // ------------------------------------------------------------------
65
+ // Public / package-internal surface used by World and its close collaborators
66
+ // (commands.ts applyChangeset, serialization deserialization context, etc.)
67
+ // ------------------------------------------------------------------
68
+
69
+ /** Primary entry point — memoized archetype creation/lookup. */
70
+ ensureArchetype(componentTypes: Iterable<EntityId<any>>): Archetype {
71
+ const regularTypes = filterRegularComponentTypes(componentTypes);
72
+ const sortedTypes = normalizeComponentTypes(regularTypes);
73
+ const hashKey = this.createArchetypeSignature(sortedTypes);
74
+
75
+ return getOrCompute(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
76
+ }
77
+
78
+ getArchetypeForEntity(entityId: EntityId): Archetype | undefined {
79
+ return this.entityToArchetype.get(entityId);
80
+ }
81
+
82
+ setEntityToArchetype(entityId: EntityId, archetype: Archetype): void {
83
+ this.entityToArchetype.set(entityId, archetype);
84
+ }
85
+
86
+ // Query helpers (moved from World for cohesion)
87
+ getMatchingArchetypes(componentTypes: EntityId<any>[]): Archetype[] {
88
+ if (componentTypes.length === 0) {
89
+ return [...this.archetypes];
90
+ }
91
+
92
+ const regularComponents: EntityId<any>[] = [];
93
+ const wildcardRelations: { componentId: EntityId<any>; relationId: EntityId<any> }[] = [];
94
+
95
+ for (const componentType of componentTypes) {
96
+ if (isWildcardRelationId(componentType)) {
97
+ const componentId = getComponentIdFromRelationId(componentType);
98
+ if (componentId !== undefined) {
99
+ wildcardRelations.push({ componentId, relationId: componentType });
100
+ }
101
+ } else {
102
+ regularComponents.push(componentType);
103
+ }
104
+ }
105
+
106
+ let matchingArchetypes = this.getArchetypesWithComponents(regularComponents);
107
+
108
+ for (const { componentId, relationId } of wildcardRelations) {
109
+ const markerSet = this.archetypesByComponent.get(relationId);
110
+ const archetypesWithMarker = markerSet ? Array.from(markerSet) : [];
111
+ matchingArchetypes =
112
+ matchingArchetypes.length === 0
113
+ ? archetypesWithMarker
114
+ : matchingArchetypes.filter((a) => markerSet?.has(a) || a.hasRelationWithComponentId(componentId));
115
+ }
116
+
117
+ return matchingArchetypes;
118
+ }
119
+
120
+ getArchetypesWithComponents(componentTypes: EntityId<any>[]): Archetype[] {
121
+ if (componentTypes.length === 0) return [...this.archetypes];
122
+ if (componentTypes.length === 1) {
123
+ const set = this.archetypesByComponent.get(componentTypes[0]!);
124
+ return set ? Array.from(set) : [];
125
+ }
126
+
127
+ // Sort by Set size, intersect starting from the smallest
128
+ const sets = componentTypes
129
+ .map((type) => this.archetypesByComponent.get(type))
130
+ .filter((s): s is Set<Archetype> => s !== undefined && s.size > 0)
131
+ .sort((a, b) => a.size - b.size);
132
+
133
+ if (sets.length === 0) return [];
134
+ if (sets.length < componentTypes.length) return []; // One component has no matching archetypes
135
+
136
+ const smallest = sets[0]!;
137
+
138
+ // 2-component fast path
139
+ if (sets.length === 2) {
140
+ const other = sets[1]!;
141
+ return Array.from(smallest).filter((a) => other.has(a));
142
+ }
143
+
144
+ // Multi-component intersection
145
+ let result = new Set(smallest);
146
+ for (let i = 1; i < sets.length; i++) {
147
+ for (const item of result) {
148
+ if (!sets[i]!.has(item)) result.delete(item);
149
+ }
150
+ if (result.size === 0) return [];
151
+ }
152
+ return Array.from(result);
153
+ }
154
+
155
+ // ------------------------------------------------------------------
156
+ // Internal creation / removal (core of the original cluster)
157
+ // ------------------------------------------------------------------
158
+
159
+ private createArchetypeSignature(componentTypes: EntityId<any>[]): string {
160
+ return componentTypes.join(",");
161
+ }
162
+
163
+ /** Deduplicated version of the original pair of methods. */
164
+ private updateReferencingIndex(componentType: EntityId<any>, archetype: Archetype, isAdd: boolean): void {
165
+ const detailedType = getDetailedIdType(componentType);
166
+ let entityId: EntityId | undefined;
167
+
168
+ if (detailedType.type === "entity") {
169
+ entityId = componentType as EntityId;
170
+ } else if (detailedType.type === "entity-relation") {
171
+ entityId = detailedType.targetId;
172
+ }
173
+
174
+ if (entityId !== undefined) {
175
+ let refs = this.entityToReferencingArchetypes.get(entityId);
176
+ if (isAdd) {
177
+ if (!refs) {
178
+ refs = new Set();
179
+ this.entityToReferencingArchetypes.set(entityId, refs);
180
+ }
181
+ refs.add(archetype);
182
+ } else {
183
+ if (refs) {
184
+ refs.delete(archetype);
185
+ if (refs.size === 0) {
186
+ this.entityToReferencingArchetypes.delete(entityId);
187
+ }
188
+ }
189
+ }
190
+ }
191
+ }
192
+
193
+ private createNewArchetype(componentTypes: EntityId<any>[]): Archetype {
194
+ const newArchetype = new Archetype(componentTypes, this.sparseStore);
195
+ this.archetypes.push(newArchetype);
196
+
197
+ this.ctx.recordArchetypeCreated?.();
198
+
199
+ for (const componentType of componentTypes) {
200
+ let archetypes = this.archetypesByComponent.get(componentType);
201
+ if (!archetypes) {
202
+ archetypes = new Set();
203
+ this.archetypesByComponent.set(componentType, archetypes);
204
+ }
205
+ archetypes.add(newArchetype);
206
+
207
+ // Update reverse index (deduped)
208
+ this.updateReferencingIndex(componentType, newArchetype, true);
209
+ }
210
+
211
+ this.ctx.queryRegistry.onNewArchetype(newArchetype);
212
+ this.updateArchetypeHookMatches(newArchetype);
213
+
214
+ return newArchetype;
215
+ }
216
+
217
+ private updateArchetypeHookMatches(archetype: Archetype): void {
218
+ for (const entry of this.ctx.hooks) {
219
+ if (this.archetypeMatchesHook(archetype, entry)) {
220
+ archetype.matchingMultiHooks.add(entry);
221
+ if (entry.matchedArchetypes) {
222
+ entry.matchedArchetypes.add(archetype);
223
+ }
224
+ }
225
+ }
226
+ }
227
+
228
+ public archetypeMatchesHook(archetype: Archetype, entry: LifecycleHookEntry): boolean {
229
+ return (
230
+ entry.requiredComponents.every((c: EntityId<any>) => {
231
+ if (isWildcardRelationId(c)) {
232
+ if (isSparseWildcard(c)) return true;
233
+ const componentId = getComponentIdFromRelationId(c);
234
+ return componentId !== undefined && archetype.hasRelationWithComponentId(componentId);
235
+ }
236
+ return archetype.componentTypeSet.has(c) || isSparseRelation(c);
237
+ }) && matchesFilter(archetype, entry.filter)
238
+ );
239
+ }
240
+
241
+ /** Called during cascade deletion cleanup. */
242
+ cleanupArchetypesReferencingEntity(entityId: EntityId): void {
243
+ const refs = this.entityToReferencingArchetypes.get(entityId);
244
+ if (!refs) return;
245
+
246
+ for (const archetype of refs) {
247
+ if (archetype.getEntities().length === 0) {
248
+ this.removeArchetype(archetype);
249
+ }
250
+ }
251
+ // removeArchetype already cleans up the reverse index entries for the archetypes themselves
252
+ this.entityToReferencingArchetypes.delete(entityId);
253
+ }
254
+
255
+ private removeArchetype(archetype: Archetype): void {
256
+ const index = this.archetypes.indexOf(archetype);
257
+ if (index !== -1) {
258
+ // swap-and-pop: O(1) removal
259
+ const last = this.archetypes[this.archetypes.length - 1]!;
260
+ this.archetypes[index] = last;
261
+ this.archetypes.pop();
262
+ }
263
+
264
+ this.ctx.recordArchetypeRemoved?.();
265
+
266
+ this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
267
+
268
+ for (const componentType of archetype.componentTypes) {
269
+ const archetypes = this.archetypesByComponent.get(componentType);
270
+ if (archetypes) {
271
+ archetypes.delete(archetype);
272
+ if (archetypes.size === 0) {
273
+ this.archetypesByComponent.delete(componentType);
274
+ }
275
+ }
276
+
277
+ // Clean up reverse index (deduped)
278
+ this.updateReferencingIndex(componentType, archetype, false);
279
+ }
280
+
281
+ this.ctx.queryRegistry.onArchetypeRemoved(archetype);
282
+ }
283
+ }