@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.
@@ -1,26 +1,19 @@
1
- import { Archetype } from "../archetype/archetype";
1
+ import type { Archetype } from "../archetype/archetype";
2
2
  import { SparseStoreImpl } from "../archetype/store";
3
- import { CommandBuffer, type Command } from "../commands/buffer";
4
- import { ComponentChangeset } from "../commands/changeset";
3
+ import { CommandBuffer } from "../commands/buffer";
5
4
  import { ComponentEntityStore } from "../component/entity-store";
6
5
  import { normalizeComponentTypes } from "../component/type-utils";
7
6
  import type { ComponentId, EntityId, WildcardRelationId } from "../entity";
8
7
  import {
9
- ENTITY_ID_START,
10
8
  EntityIdManager,
11
9
  RELATION_SHIFT,
12
10
  getComponentIdFromRelationId,
13
- getDetailedIdType,
14
- getTargetIdFromRelationId,
15
11
  isCascadeDeleteRelation,
16
- isEntityRelation,
17
- isExclusiveComponent,
18
12
  isSparseRelation,
19
- isSparseWildcard,
20
13
  isWildcardRelationId,
21
14
  relation,
22
15
  } from "../entity";
23
- import { matchesFilter, serializeQueryFilter, type QueryFilter } from "../query/filter";
16
+ import { serializeQueryFilter, type QueryFilter } from "../query/filter";
24
17
  import type { Query } from "../query/query";
25
18
  import { QueryRegistry } from "../query/registry";
26
19
  import type { SerializedWorld } from "../storage/serialization";
@@ -34,84 +27,89 @@ import type {
34
27
  SyncDebugStats,
35
28
  } from "../types";
36
29
  import { isOptionalEntityId } from "../types";
37
- import { getOrCompute } from "../utils/utils";
30
+ import { ArchetypeManager } from "./archetype-manager";
38
31
  import { EntityBuilder } from "./builder";
39
- import {
40
- applyChangeset,
41
- filterRegularComponentTypes,
42
- maybeRemoveWildcardMarker,
43
- processCommands,
44
- removeMatchingRelations,
45
- type CommandProcessorContext,
46
- } from "./commands";
32
+ import { CommandExecutor, type CommandExecutorContext } from "./command-executor";
33
+ import { DebugStatsManager } from "./debug-stats";
47
34
  import {
48
35
  collectMultiHookComponents,
49
- debugHookExecutionCounter,
50
36
  triggerLifecycleHooks,
51
37
  triggerRemoveHooksForEntityDeletion,
52
38
  type HooksContext,
53
39
  } from "./hooks";
54
40
  import {
55
- getEntityReferences,
56
- trackEntityReference,
57
- untrackEntityReference,
58
- type EntityReferencesMap,
59
- } from "./references";
41
+ assertEntityExists,
42
+ assertSetComponentTypeValid,
43
+ resolveRemoveOperation,
44
+ resolveSetOperation,
45
+ } from "./operations";
46
+ import { getEntityReferences, type EntityReferencesMap } from "./references";
60
47
  import { deserializeWorld, serializeWorld } from "./serialization";
48
+ import { SingletonHandle } from "./singleton";
61
49
 
62
50
  /**
63
51
  * World class for ECS architecture
64
52
  * Manages entities and components
65
53
  */
66
54
  export class World {
55
+ private static readonly DEPRECATED_SINGLETON_SET_SHORTHAND_WARNING =
56
+ "world.set(componentId, value) for singleton components is deprecated; use world.singleton(componentId).set(value) or world.set(componentId, componentId, value) instead.";
57
+
67
58
  // Core data structures for entity and archetype management
68
59
  private entityIdManager = new EntityIdManager();
69
- private archetypes: Archetype[] = [];
70
- private archetypeBySignature = new Map<string, Archetype>();
71
- private entityToArchetype = new Map<EntityId, Archetype>();
72
- private archetypesByComponent = new Map<EntityId<any>, Set<Archetype>>();
73
60
  private entityReferences: EntityReferencesMap = new Map();
74
- /** Reverse index: entity ID → set of archetypes whose componentTypes include that entity ID */
75
- private entityToReferencingArchetypes = new Map<EntityId, Set<Archetype>>();
76
61
  /** Sparse relation storage (for components created with `sparse: true`), shared with all Archetype instances */
77
62
  private readonly sparseStore = new SparseStoreImpl();
78
63
  /** Component entity (singleton) storage */
79
64
  private readonly componentEntities = new ComponentEntityStore();
80
65
 
66
+ // Archetype storage, indexes, creation/removal, and referencing are now encapsulated
67
+ // in ArchetypeManager (extracted to reduce World line count and improve cohesion).
68
+ private archetypeManager!: ArchetypeManager;
69
+
70
+ // Temporary forwarding accessors so the rest of World (and its internal collaborators)
71
+ // can continue using the familiar names with almost zero call-site changes.
72
+ // These can be removed in a follow-up cleanup pass if desired.
73
+ private get archetypes() {
74
+ return this.archetypeManager.archetypes;
75
+ }
76
+ private get entityToArchetype() {
77
+ return this.archetypeManager.entityToArchetype;
78
+ }
79
+ private get archetypesByComponent() {
80
+ return this.archetypeManager.archetypesByComponent;
81
+ }
82
+ private get entityToReferencingArchetypes() {
83
+ return this.archetypeManager.entityToReferencingArchetypes;
84
+ }
85
+
81
86
  // Query registry – manages caching, ref counts, and archetype notifications
82
87
  private readonly queryRegistry = new QueryRegistry();
83
88
 
84
89
  // Lifecycle hooks (declared before cached contexts that reference them)
85
90
  private hooks: Set<LifecycleHookEntry> = new Set();
86
91
 
87
- // Debug observability collectors (armed only when non-empty)
88
- private readonly _debugCollectors = new Set<(stats: SyncDebugStats) => void>();
89
-
90
- // Transient counters for the current armed sync (reset each time)
91
- private _debugMigrations = 0;
92
- private _debugArchetypesCreated = 0;
93
- private _debugArchetypesRemoved = 0;
94
-
95
- // Command execution
96
- private commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
97
-
98
- // Reusable instances to reduce per-frame allocations
99
- private readonly _changeset = new ComponentChangeset();
100
- private readonly _removeChangeset = new ComponentChangeset();
101
- /** Cached command processor context to avoid per-entity object allocation */
102
- private readonly _commandCtx: CommandProcessorContext = {
103
- sparseStore: this.sparseStore,
104
- ensureArchetype: (ct) => this.ensureArchetype(ct),
105
- };
106
- /** Cached hooks context to avoid per-entity object allocation */
107
- private readonly _hooksCtx: HooksContext = {
108
- multiHooks: this.hooks,
109
- has: (eid, ct) => this.has(eid, ct),
110
- get: (eid, ct) => this.get(eid, ct),
111
- getOptional: (eid, ct) => this.getOptional(eid, ct),
112
- };
92
+ // Debug observability (extracted to DebugStatsManager to reduce World line count)
93
+ private readonly debugStats = new DebugStatsManager();
94
+
95
+ // Command execution (orchestration extracted to CommandExecutor)
96
+ private commandBuffer!: CommandBuffer;
97
+ private commandExecutor!: CommandExecutor;
113
98
 
114
99
  constructor(snapshot?: SerializedWorld) {
100
+ // Must create the manager before any code that may invoke ensureArchetype
101
+ // (including the snapshot deserialization path below, and the closures
102
+ // captured in the field-initialized _commandCtx).
103
+ this.archetypeManager = new ArchetypeManager(
104
+ {
105
+ queryRegistry: this.queryRegistry,
106
+ hooks: this.hooks,
107
+ recordArchetypeCreated: () => this.debugStats.recordArchetypeCreated(),
108
+ recordArchetypeRemoved: () => this.debugStats.recordArchetypeRemoved(),
109
+ },
110
+ this.sparseStore,
111
+ );
112
+
115
113
  if (snapshot && typeof snapshot === "object") {
116
114
  deserializeWorld(
117
115
  {
@@ -119,15 +117,34 @@ export class World {
119
117
  componentEntities: this.componentEntities,
120
118
  entityReferences: this.entityReferences,
121
119
  ensureArchetype: (ct) => this.ensureArchetype(ct),
122
- setEntityToArchetype: (eid, arch) => this.entityToArchetype.set(eid, arch),
120
+ setEntityToArchetype: (eid, arch) => this.archetypeManager.entityToArchetype.set(eid, arch),
123
121
  },
124
122
  snapshot,
125
123
  );
126
124
  }
127
- }
128
125
 
129
- private createArchetypeSignature(componentTypes: EntityId<any>[]): string {
130
- return componentTypes.join(",");
126
+ // CommandExecutor must be created after archetypeManager (and debugStats) because its context
127
+ // closes over several pieces and the destroy fast path.
128
+ const execCtx: CommandExecutorContext = {
129
+ componentEntities: this.componentEntities,
130
+ entityReferences: this.entityReferences,
131
+ hooks: this.hooks,
132
+ entityToArchetype: this.entityToArchetype,
133
+ ensureArchetype: (ct) => this.ensureArchetype(ct),
134
+ sparseStore: this.sparseStore,
135
+ has: (eid, ct) => this.has(eid, ct),
136
+ get: (eid, ct) => this.get(eid, ct),
137
+ getOptional: (eid, ct) => this.getOptional(eid, ct),
138
+ destroyEntityImmediate: (eid) => this.destroyEntityImmediate(eid),
139
+ incrementMigrations: () => this.debugStats.incrementMigrations(),
140
+ triggerLifecycleHooks,
141
+ triggerRemoveHooksForEntityDeletion,
142
+ };
143
+ this.commandExecutor = new CommandExecutor(execCtx);
144
+
145
+ this.commandBuffer = new CommandBuffer((entityId, commands) =>
146
+ this.commandExecutor.executeEntityCommands(entityId, commands),
147
+ );
131
148
  }
132
149
 
133
150
  /**
@@ -255,71 +272,6 @@ export class World {
255
272
  return this.entityToArchetype.has(entityId);
256
273
  }
257
274
 
258
- private assertEntityExists(entityId: EntityId, label: "Entity" | "Component entity"): void {
259
- if (!this.exists(entityId)) {
260
- throw new Error(`${label} ${entityId} does not exist`);
261
- }
262
- }
263
-
264
- private assertComponentTypeValid(componentType: EntityId): void {
265
- const detailedType = getDetailedIdType(componentType);
266
- if (detailedType.type === "invalid") {
267
- throw new Error(`Invalid component type: ${componentType}`);
268
- }
269
- }
270
-
271
- private assertSetComponentTypeValid(componentType: EntityId): void {
272
- const detailedType = getDetailedIdType(componentType);
273
- if (detailedType.type === "invalid") {
274
- throw new Error(`Invalid component type: ${componentType}`);
275
- }
276
- if (detailedType.type === "wildcard-relation") {
277
- throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
278
- }
279
- }
280
-
281
- private resolveSetOperation(
282
- entityId: EntityId | ComponentId,
283
- componentTypeOrComponent?: EntityId | any,
284
- maybeComponent?: any,
285
- ): { entityId: EntityId; componentType: EntityId; component: any } {
286
- // Handle singleton component overload: set(componentId, data)
287
- if (maybeComponent === undefined && componentTypeOrComponent !== undefined) {
288
- const detailedType = getDetailedIdType(entityId);
289
- if (detailedType.type === "component" || detailedType.type === "component-relation") {
290
- const componentId = entityId as ComponentId;
291
- this.assertEntityExists(componentId, "Component entity");
292
- this.assertSetComponentTypeValid(componentId);
293
- return { entityId: componentId, componentType: componentId, component: componentTypeOrComponent };
294
- }
295
- }
296
-
297
- const targetEntityId = entityId as EntityId;
298
- const componentType = componentTypeOrComponent as EntityId;
299
- this.assertEntityExists(targetEntityId, "Entity");
300
- this.assertSetComponentTypeValid(componentType);
301
-
302
- return { entityId: targetEntityId, componentType, component: maybeComponent };
303
- }
304
-
305
- private resolveRemoveOperation<T>(
306
- entityId: EntityId | ComponentId,
307
- componentType?: EntityId<T>,
308
- ): { entityId: EntityId; componentType: EntityId } {
309
- // Handle singleton component overload: remove(componentId)
310
- if (componentType === undefined) {
311
- const componentId = entityId as ComponentId<T>;
312
- this.assertEntityExists(componentId, "Component entity");
313
- return { entityId: componentId, componentType: componentId };
314
- }
315
-
316
- const targetEntityId = entityId as EntityId;
317
- this.assertEntityExists(targetEntityId, "Entity");
318
- this.assertComponentTypeValid(componentType);
319
-
320
- return { entityId: targetEntityId, componentType };
321
- }
322
-
323
275
  /**
324
276
  * Adds or updates a component on an entity (or marks void component as present).
325
277
  * The change is buffered and takes effect after calling `world.sync()`.
@@ -328,30 +280,39 @@ export class World {
328
280
  * @overload set(entityId: EntityId, componentType: EntityId<void>): void
329
281
  * Marks a void component as present on the entity
330
282
  *
283
+ * @overload set<T>(componentId: ComponentId<T>, component: Exclude<NoInfer<T>, number>): void
284
+ * @deprecated Use `world.singleton(componentId).set(value)` or `world.set(componentId, componentId, value)` instead.
285
+ * Compatibility shorthand for singleton component data when the second argument is not a number
286
+ *
331
287
  * @overload set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void
332
288
  * Adds or updates a component with data on the entity
333
289
  *
334
- * @overload set<T>(componentId: ComponentId<T>, component: NoInfer<T>): void
335
- * Adds or updates a singleton component (shorthand for set(componentId, componentId, component))
336
- *
337
290
  * @throws {Error} If the entity does not exist
338
291
  * @throws {Error} If the component type is invalid or is a wildcard relation
339
292
  *
340
293
  * @example
341
294
  * world.set(entity, Position, { x: 10, y: 20 });
342
295
  * world.set(entity, Marker); // void component
343
- * world.set(GlobalConfig, { debug: true }); // singleton component
296
+ * world.singleton(GlobalConfig).set({ debug: true }); // singleton component
297
+ * world.set(GlobalConfig, { debug: true }); // deprecated singleton compatibility shorthand
344
298
  * world.sync(); // Apply changes
345
299
  */
346
300
  set(entityId: EntityId, componentType: EntityId<void>): void;
301
+ /** @deprecated Use `world.singleton(componentId).set(value)` or `world.set(componentId, componentId, value)` instead. */
302
+ set<T>(componentId: ComponentId<T>, component: Exclude<NoInfer<T>, number>): void;
347
303
  set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void;
348
- set<T>(componentId: ComponentId<T>, component: NoInfer<T>): void;
349
304
  set(entityId: EntityId | ComponentId, componentTypeOrComponent?: EntityId | any, maybeComponent?: any): void {
350
305
  const {
351
306
  entityId: targetEntityId,
352
307
  componentType,
353
308
  component,
354
- } = this.resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent);
309
+ deprecatedSingletonShorthand,
310
+ } = resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent, arguments.length, (id) =>
311
+ this.exists(id),
312
+ );
313
+ if (deprecatedSingletonShorthand) {
314
+ console.warn(World.DEPRECATED_SINGLETON_SET_SHORTHAND_WARNING);
315
+ }
355
316
  this.commandBuffer.set(targetEntityId, componentType, component);
356
317
  }
357
318
 
@@ -381,9 +342,10 @@ export class World {
381
342
  remove<T>(componentId: ComponentId<T>): void;
382
343
  remove<T>(entityId: EntityId, componentType: EntityId<T>): void;
383
344
  remove<T>(entityId: EntityId | ComponentId, componentType?: EntityId<T>): void {
384
- const { entityId: targetEntityId, componentType: targetComponentType } = this.resolveRemoveOperation(
345
+ const { entityId: targetEntityId, componentType: targetComponentType } = resolveRemoveOperation(
385
346
  entityId,
386
347
  componentType,
348
+ (id) => this.exists(id),
387
349
  );
388
350
  this.commandBuffer.remove(targetEntityId, targetComponentType);
389
351
  }
@@ -403,6 +365,32 @@ export class World {
403
365
  this.commandBuffer.delete(entityId);
404
366
  }
405
367
 
368
+ /**
369
+ * Returns an explicit handle for a singleton component (component-as-entity).
370
+ *
371
+ * This is the preferred API for singleton components.
372
+ *
373
+ * @example
374
+ * const config = world.singleton(GlobalConfig);
375
+ * config.set({ debug: true });
376
+ * world.sync();
377
+ * console.log(config.get());
378
+ */
379
+ singleton<T>(componentId: ComponentId<T>): SingletonHandle<T> {
380
+ assertEntityExists(componentId, "Component entity", (id) => this.exists(id));
381
+ assertSetComponentTypeValid(componentId);
382
+
383
+ return new SingletonHandle(componentId, {
384
+ has: () => this.componentEntities.hasSingleton(componentId),
385
+ get: () => this.get(componentId),
386
+ getOptional: () => this.getOptional(componentId),
387
+ remove: () => this.commandBuffer.remove(componentId, componentId),
388
+ set: (value) => {
389
+ this.commandBuffer.set(componentId, componentId as EntityId<any>, value as any);
390
+ },
391
+ });
392
+ }
393
+
406
394
  /**
407
395
  * Checks if a specific **component** is present on an entity.
408
396
  *
@@ -612,7 +600,7 @@ export class World {
612
600
  entityId: EntityId,
613
601
  relationComp: ComponentId<T>,
614
602
  ): [target: EntityId<unknown>, data: T | undefined][] {
615
- this.assertEntityExists(entityId, "Entity");
603
+ assertEntityExists(entityId, "Entity", (id) => this.exists(id));
616
604
 
617
605
  const wildcard = relation(relationComp, "*") as WildcardRelationId<T>;
618
606
 
@@ -659,7 +647,7 @@ export class World {
659
647
  * given base component.
660
648
  */
661
649
  hasRelation(entityId: EntityId, relationComp: ComponentId<any>, targetId?: EntityId): boolean {
662
- this.assertEntityExists(entityId, "Entity");
650
+ assertEntityExists(entityId, "Entity", (id) => this.exists(id));
663
651
 
664
652
  if (targetId !== undefined) {
665
653
  const specific = relation(relationComp, targetId);
@@ -675,7 +663,7 @@ export class World {
675
663
  * Returns the number of relations of the given base component held by the entity.
676
664
  */
677
665
  countRelations(entityId: EntityId, relationComp: ComponentId<any>): number {
678
- this.assertEntityExists(entityId, "Entity");
666
+ assertEntityExists(entityId, "Entity", (id) => this.exists(id));
679
667
  const targets = this.getRelationTargets(entityId, relationComp);
680
668
  return targets.length;
681
669
  }
@@ -922,88 +910,7 @@ export class World {
922
910
  * This is intended for development/debugging and leak detection.
923
911
  */
924
912
  createDebugStatsCollector(callback: (stats: SyncDebugStats) => void): DebugStatsCollector {
925
- this._debugCollectors.add(callback);
926
-
927
- return {
928
- [Symbol.dispose]: () => {
929
- this._debugCollectors.delete(callback);
930
- },
931
- };
932
- }
933
-
934
- private _resetDebugActivityCounters(): void {
935
- this._debugMigrations = 0;
936
- this._debugArchetypesCreated = 0;
937
- this._debugArchetypesRemoved = 0;
938
- debugHookExecutionCounter.value = 0;
939
- }
940
-
941
- private _deliverDebugStats(timings: {
942
- syncStart: number;
943
- syncEnd: number;
944
- commandBufferStart: number;
945
- commandBufferEnd: number;
946
- commandIterations: number;
947
- }): void {
948
- // Build structural counts (post-sync)
949
- // Note: singletons (component-as-entity) are not included in the main archetype map.
950
- // For debug purposes the dominant number is regular entities; we keep it simple here.
951
- const entityCount = this.entityToArchetype.size;
952
- let emptyArchetypes = 0;
953
- for (const arch of this.archetypes) {
954
- if (arch.size === 0) emptyArchetypes++;
955
- }
956
-
957
- let archetypesByComponentSize = 0;
958
- for (const set of this.archetypesByComponent.values()) {
959
- archetypesByComponentSize += set.size;
960
- }
961
-
962
- const stats: SyncDebugStats = {
963
- timestamps: {
964
- syncStart: timings.syncStart,
965
- syncEnd: timings.syncEnd,
966
- commandBufferStart: timings.commandBufferStart,
967
- commandBufferEnd: timings.commandBufferEnd,
968
- },
969
- commandIterations: timings.commandIterations,
970
-
971
- entities: {
972
- total: entityCount,
973
- freelistSize: this.entityIdManager.getFreelistSize(),
974
- nextId: this.entityIdManager.getNextId(),
975
- },
976
- archetypes: {
977
- total: this.archetypes.length,
978
- empty: emptyArchetypes,
979
- },
980
- queries: {
981
- cached: (this.queryRegistry as any).cache?.size ?? 0,
982
- registered: (this.queryRegistry as any).queries?.size ?? 0,
983
- },
984
- hooks: {
985
- total: this.hooks.size,
986
- },
987
- indices: {
988
- entityReferences: this.entityReferences.size,
989
- entityToReferencingArchetypes: this.entityToReferencingArchetypes.size,
990
- archetypesByComponent: archetypesByComponentSize,
991
- },
992
- activity: {
993
- migrations: this._debugMigrations,
994
- hooksExecuted: debugHookExecutionCounter.value,
995
- archetypesCreated: this._debugArchetypesCreated,
996
- archetypesRemoved: this._debugArchetypesRemoved,
997
- },
998
- };
999
-
1000
- for (const cb of this._debugCollectors) {
1001
- try {
1002
- cb(stats);
1003
- } catch {
1004
- // Intentionally ignore user callback errors
1005
- }
1006
- }
913
+ return this.debugStats.createCollector(callback);
1007
914
  }
1008
915
 
1009
916
  /**
@@ -1017,29 +924,56 @@ export class World {
1017
924
  * world.sync(); // Apply all buffered changes
1018
925
  */
1019
926
  sync(): void {
1020
- const hasCollectors = this._debugCollectors.size > 0;
1021
-
1022
- const syncStart = hasCollectors ? performance.now() : 0;
1023
-
1024
- if (hasCollectors) {
1025
- this._resetDebugActivityCounters();
927
+ if (!this.debugStats.hasActiveCollectors()) {
928
+ // Fast path: no debug collectors, skip all timing and stats work
929
+ this.commandBuffer.execute();
930
+ return;
1026
931
  }
1027
932
 
1028
- const commandBufferStart = hasCollectors ? performance.now() : 0;
933
+ // Slow path: full instrumentation for active debug stats collectors
934
+ const syncStart = performance.now();
935
+ this.debugStats.resetActivity();
936
+
937
+ const commandBufferStart = performance.now();
1029
938
  const commandIterations = this.commandBuffer.execute();
1030
- const commandBufferEnd = hasCollectors ? performance.now() : 0;
939
+ const commandBufferEnd = performance.now();
1031
940
 
1032
- const syncEnd = hasCollectors ? performance.now() : 0;
941
+ const syncEnd = performance.now();
1033
942
 
1034
- if (hasCollectors) {
1035
- this._deliverDebugStats({
943
+ // Build the data bag for the extracted manager (keeps it decoupled from internal maps)
944
+ const entityCount = this.entityToArchetype.size;
945
+ let emptyArchetypes = 0;
946
+ for (const arch of this.archetypes) {
947
+ if (arch.size === 0) emptyArchetypes++;
948
+ }
949
+
950
+ let archetypesByComponentSize = 0;
951
+ for (const set of this.archetypesByComponent.values()) {
952
+ archetypesByComponentSize += set.size;
953
+ }
954
+
955
+ this.debugStats.deliver(
956
+ {
1036
957
  syncStart,
1037
958
  syncEnd,
1038
959
  commandBufferStart,
1039
960
  commandBufferEnd,
1040
961
  commandIterations,
1041
- });
1042
- }
962
+ },
963
+ {
964
+ entityCount,
965
+ freelistSize: this.entityIdManager.getFreelistSize(),
966
+ nextId: this.entityIdManager.getNextId(),
967
+ archetypeCount: this.archetypes.length,
968
+ emptyArchetypes,
969
+ archetypesByComponentSize,
970
+ cachedQueryCount: (this.queryRegistry as any).cache?.size ?? 0,
971
+ registeredQueryCount: (this.queryRegistry as any).queries?.size ?? 0,
972
+ hookCount: this.hooks.size,
973
+ entityReferencesSize: this.entityReferences.size,
974
+ entityToReferencingArchetypesSize: this.entityToReferencingArchetypes.size,
975
+ },
976
+ );
1043
977
  }
1044
978
 
1045
979
  /**
@@ -1076,7 +1010,7 @@ export class World {
1076
1010
  createQuery(componentTypes: EntityId<any>[], filter: QueryFilter = {}): Query {
1077
1011
  const sortedTypes = normalizeComponentTypes(componentTypes);
1078
1012
  const filterKey = serializeQueryFilter(filter);
1079
- const key = `${this.createArchetypeSignature(sortedTypes)}${filterKey ? `|${filterKey}` : ""}`;
1013
+ const key = `${sortedTypes.join(",")}${filterKey ? `|${filterKey}` : ""}`;
1080
1014
  return this.queryRegistry.getOrCreate(this, sortedTypes, key, filter);
1081
1015
  }
1082
1016
 
@@ -1146,71 +1080,7 @@ export class World {
1146
1080
  * @internal
1147
1081
  */
1148
1082
  getMatchingArchetypes(componentTypes: EntityId<any>[]): Archetype[] {
1149
- if (componentTypes.length === 0) {
1150
- return [...this.archetypes];
1151
- }
1152
-
1153
- const regularComponents: EntityId<any>[] = [];
1154
- const wildcardRelations: { componentId: ComponentId<any>; relationId: EntityId<any> }[] = [];
1155
-
1156
- for (const componentType of componentTypes) {
1157
- if (isWildcardRelationId(componentType)) {
1158
- const componentId = getComponentIdFromRelationId(componentType);
1159
- if (componentId !== undefined) {
1160
- wildcardRelations.push({ componentId, relationId: componentType });
1161
- }
1162
- } else {
1163
- regularComponents.push(componentType);
1164
- }
1165
- }
1166
-
1167
- let matchingArchetypes = this.getArchetypesWithComponents(regularComponents);
1168
-
1169
- for (const { componentId, relationId } of wildcardRelations) {
1170
- const markerSet = this.archetypesByComponent.get(relationId);
1171
- const archetypesWithMarker = markerSet ? Array.from(markerSet) : [];
1172
- matchingArchetypes =
1173
- matchingArchetypes.length === 0
1174
- ? archetypesWithMarker
1175
- : matchingArchetypes.filter((a) => markerSet?.has(a) || a.hasRelationWithComponentId(componentId));
1176
- }
1177
-
1178
- return matchingArchetypes;
1179
- }
1180
-
1181
- private getArchetypesWithComponents(componentTypes: EntityId<any>[]): Archetype[] {
1182
- if (componentTypes.length === 0) return [...this.archetypes];
1183
- if (componentTypes.length === 1) {
1184
- const set = this.archetypesByComponent.get(componentTypes[0]!);
1185
- return set ? Array.from(set) : [];
1186
- }
1187
-
1188
- // Sort by Set size, intersect starting from the smallest
1189
- const sets = componentTypes
1190
- .map((type) => this.archetypesByComponent.get(type))
1191
- .filter((s): s is Set<Archetype> => s !== undefined && s.size > 0)
1192
- .sort((a, b) => a.size - b.size);
1193
-
1194
- if (sets.length === 0) return [];
1195
- if (sets.length < componentTypes.length) return []; // One component has no matching archetypes
1196
-
1197
- const smallest = sets[0]!;
1198
-
1199
- // 2-component fast path
1200
- if (sets.length === 2) {
1201
- const other = sets[1]!;
1202
- return Array.from(smallest).filter((a) => other.has(a));
1203
- }
1204
-
1205
- // Multi-component intersection
1206
- let result = new Set(smallest);
1207
- for (let i = 1; i < sets.length; i++) {
1208
- for (const item of result) {
1209
- if (!sets[i]!.has(item)) result.delete(item);
1210
- }
1211
- if (result.size === 0) return [];
1212
- }
1213
- return Array.from(result);
1083
+ return this.archetypeManager.getMatchingArchetypes(componentTypes);
1214
1084
  }
1215
1085
 
1216
1086
  /**
@@ -1268,287 +1138,17 @@ export class World {
1268
1138
  }
1269
1139
  }
1270
1140
 
1271
- private executeEntityCommands(entityId: EntityId, commands: Command[]): void {
1272
- this._changeset.clear();
1273
-
1274
- // 1. Route: component entities use flat-map storage
1275
- if (this.componentEntities.exists(entityId)) {
1276
- this.componentEntities.executeCommands(entityId, commands);
1277
- return;
1278
- }
1279
-
1280
- // 2. Route: destroy uses fast path
1281
- if (commands.some((cmd) => cmd.type === "destroy")) {
1282
- this.destroyEntityImmediate(entityId);
1283
- return;
1284
- }
1285
-
1286
- // 3. Apply structural changes
1287
- this.applyEntityCommands(entityId, commands);
1288
- }
1289
-
1290
- private applyEntityCommands(entityId: EntityId, commands: Command[]): void {
1291
- const currentArchetype = this.entityToArchetype.get(entityId);
1292
- if (!currentArchetype) return;
1293
-
1294
- const changeset = this._changeset;
1295
- processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
1296
- if (isExclusiveComponent(compId)) {
1297
- removeMatchingRelations(eid, arch, compId, changeset);
1298
- }
1299
- });
1300
-
1301
- const hasStructuralChange = changeset.removes.size > 0 || changeset.adds.size > 0;
1302
-
1303
- if (this.hooks.size === 0) {
1304
- // Fast path: no hooks, skip removedComponents map allocation and hook triggering
1305
- const newArchetype = applyChangeset(
1306
- this._commandCtx,
1307
- entityId,
1308
- currentArchetype,
1309
- changeset,
1310
- this.entityToArchetype,
1311
- null,
1312
- );
1313
- if (hasStructuralChange) {
1314
- this.updateEntityReferences(entityId, changeset);
1315
- }
1316
- if (this._debugCollectors.size > 0 && newArchetype !== currentArchetype) {
1317
- this._debugMigrations++;
1318
- }
1319
- return;
1320
- }
1321
-
1322
- const removedComponents = new Map<EntityId<any>, any>();
1323
- const newArchetype = applyChangeset(
1324
- this._commandCtx,
1325
- entityId,
1326
- currentArchetype,
1327
- changeset,
1328
- this.entityToArchetype,
1329
- removedComponents,
1330
- );
1331
-
1332
- if (hasStructuralChange) {
1333
- this.updateEntityReferences(entityId, changeset);
1334
- }
1335
-
1336
- if (this._debugCollectors.size > 0 && newArchetype !== currentArchetype) {
1337
- this._debugMigrations++;
1338
- }
1339
-
1340
- triggerLifecycleHooks(
1341
- this.createHooksContext(),
1342
- entityId,
1343
- changeset.adds,
1344
- removedComponents,
1345
- currentArchetype,
1346
- newArchetype,
1347
- );
1348
- }
1349
-
1350
- private createHooksContext(): HooksContext {
1351
- return this._hooksCtx;
1352
- }
1353
-
1354
- private removeComponentImmediate(entityId: EntityId, componentType: EntityId<any>, targetEntityId: EntityId): void {
1355
- const sourceArchetype = this.entityToArchetype.get(entityId);
1356
- if (!sourceArchetype) return;
1357
-
1358
- const changeset = this._removeChangeset;
1359
- changeset.clear();
1360
- changeset.delete(componentType);
1361
- maybeRemoveWildcardMarker(
1362
- entityId,
1363
- sourceArchetype,
1364
- componentType,
1365
- getComponentIdFromRelationId(componentType),
1366
- changeset,
1367
- );
1368
-
1369
- const removedComponent = sourceArchetype.get(entityId, componentType);
1370
- const newArchetype = applyChangeset(
1371
- this._commandCtx,
1372
- entityId,
1373
- sourceArchetype,
1374
- changeset,
1375
- this.entityToArchetype,
1376
- null,
1377
- );
1378
- untrackEntityReference(this.entityReferences, entityId, componentType, targetEntityId);
1379
- triggerLifecycleHooks(
1380
- this.createHooksContext(),
1381
- entityId,
1382
- new Map(),
1383
- new Map([[componentType, removedComponent]]),
1384
- sourceArchetype,
1385
- newArchetype,
1386
- );
1387
- }
1388
-
1389
- private updateEntityReferences(entityId: EntityId, changeset: ComponentChangeset): void {
1390
- for (const componentType of changeset.removes) {
1391
- if (isEntityRelation(componentType)) {
1392
- const targetId = getTargetIdFromRelationId(componentType)!;
1393
- untrackEntityReference(this.entityReferences, entityId, componentType, targetId);
1394
- } else if (componentType >= ENTITY_ID_START) {
1395
- untrackEntityReference(this.entityReferences, entityId, componentType, componentType);
1396
- }
1397
- }
1398
-
1399
- for (const [componentType] of changeset.adds) {
1400
- if (isEntityRelation(componentType)) {
1401
- const targetId = getTargetIdFromRelationId(componentType)!;
1402
- trackEntityReference(this.entityReferences, entityId, componentType, targetId);
1403
- } else if (componentType >= ENTITY_ID_START) {
1404
- trackEntityReference(this.entityReferences, entityId, componentType, componentType);
1405
- }
1406
- }
1407
- }
1408
-
1141
+ // Delegators to the extracted ArchetypeManager (keeps public + internal call sites unchanged).
1409
1142
  private ensureArchetype(componentTypes: Iterable<EntityId<any>>): Archetype {
1410
- const regularTypes = filterRegularComponentTypes(componentTypes);
1411
- const sortedTypes = normalizeComponentTypes(regularTypes);
1412
- const hashKey = this.createArchetypeSignature(sortedTypes);
1413
-
1414
- return getOrCompute(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
1415
- }
1416
-
1417
- /** Add componentType to the reverse index if it contains an entity ID */
1418
- private addToReferencingIndex(componentType: EntityId<any>, archetype: Archetype): void {
1419
- const detailedType = getDetailedIdType(componentType);
1420
- let entityId: EntityId | undefined;
1421
-
1422
- if (detailedType.type === "entity") {
1423
- entityId = componentType as EntityId;
1424
- } else if (detailedType.type === "entity-relation") {
1425
- entityId = detailedType.targetId;
1426
- }
1427
-
1428
- if (entityId !== undefined) {
1429
- let refs = this.entityToReferencingArchetypes.get(entityId);
1430
- if (!refs) {
1431
- refs = new Set();
1432
- this.entityToReferencingArchetypes.set(entityId, refs);
1433
- }
1434
- refs.add(archetype);
1435
- }
1436
- }
1437
-
1438
- /** Remove componentType from the reverse index */
1439
- private removeFromReferencingIndex(componentType: EntityId<any>, archetype: Archetype): void {
1440
- const detailedType = getDetailedIdType(componentType);
1441
- let entityId: EntityId | undefined;
1442
-
1443
- if (detailedType.type === "entity") {
1444
- entityId = componentType as EntityId;
1445
- } else if (detailedType.type === "entity-relation") {
1446
- entityId = detailedType.targetId;
1447
- }
1448
-
1449
- if (entityId !== undefined) {
1450
- const refs = this.entityToReferencingArchetypes.get(entityId);
1451
- if (refs) {
1452
- refs.delete(archetype);
1453
- if (refs.size === 0) {
1454
- this.entityToReferencingArchetypes.delete(entityId);
1455
- }
1456
- }
1457
- }
1458
- }
1459
-
1460
- private createNewArchetype(componentTypes: EntityId<any>[]): Archetype {
1461
- const newArchetype = new Archetype(componentTypes, this.sparseStore);
1462
- this.archetypes.push(newArchetype);
1463
-
1464
- if (this._debugCollectors.size > 0) {
1465
- this._debugArchetypesCreated++;
1466
- }
1467
-
1468
- for (const componentType of componentTypes) {
1469
- let archetypes = this.archetypesByComponent.get(componentType);
1470
- if (!archetypes) {
1471
- archetypes = new Set();
1472
- this.archetypesByComponent.set(componentType, archetypes);
1473
- }
1474
- archetypes.add(newArchetype);
1475
-
1476
- // Update reverse index
1477
- this.addToReferencingIndex(componentType, newArchetype);
1478
- }
1479
-
1480
- this.queryRegistry.onNewArchetype(newArchetype);
1481
- this.updateArchetypeHookMatches(newArchetype);
1482
-
1483
- return newArchetype;
1484
- }
1485
-
1486
- private updateArchetypeHookMatches(archetype: Archetype): void {
1487
- for (const entry of this.hooks) {
1488
- if (this.archetypeMatchesHook(archetype, entry)) {
1489
- archetype.matchingMultiHooks.add(entry);
1490
- if (entry.matchedArchetypes) {
1491
- entry.matchedArchetypes.add(archetype);
1492
- }
1493
- }
1494
- }
1495
- }
1496
-
1497
- private archetypeMatchesHook(archetype: Archetype, entry: LifecycleHookEntry): boolean {
1498
- return (
1499
- entry.requiredComponents.every((c: EntityId<any>) => {
1500
- if (isWildcardRelationId(c)) {
1501
- if (isSparseWildcard(c)) return true;
1502
- const componentId = getComponentIdFromRelationId(c);
1503
- return componentId !== undefined && archetype.hasRelationWithComponentId(componentId);
1504
- }
1505
- return archetype.componentTypeSet.has(c) || isSparseRelation(c);
1506
- }) && matchesFilter(archetype, entry.filter)
1507
- );
1143
+ return this.archetypeManager.ensureArchetype(componentTypes);
1508
1144
  }
1509
1145
 
1510
1146
  private cleanupArchetypesReferencingEntity(entityId: EntityId): void {
1511
- const refs = this.entityToReferencingArchetypes.get(entityId);
1512
- if (!refs) return;
1513
-
1514
- for (const archetype of refs) {
1515
- if (archetype.getEntities().length === 0) {
1516
- this.removeArchetype(archetype);
1517
- }
1518
- }
1519
- // removeArchetype already cleans up the reverse index entries
1520
- this.entityToReferencingArchetypes.delete(entityId);
1147
+ this.archetypeManager.cleanupArchetypesReferencingEntity(entityId);
1521
1148
  }
1522
1149
 
1523
- private removeArchetype(archetype: Archetype): void {
1524
- const index = this.archetypes.indexOf(archetype);
1525
- if (index !== -1) {
1526
- // swap-and-pop: O(1) removal
1527
- const last = this.archetypes[this.archetypes.length - 1]!;
1528
- this.archetypes[index] = last;
1529
- this.archetypes.pop();
1530
- }
1531
-
1532
- if (this._debugCollectors.size > 0) {
1533
- this._debugArchetypesRemoved++;
1534
- }
1535
-
1536
- this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
1537
-
1538
- for (const componentType of archetype.componentTypes) {
1539
- const archetypes = this.archetypesByComponent.get(componentType);
1540
- if (archetypes) {
1541
- archetypes.delete(archetype);
1542
- if (archetypes.size === 0) {
1543
- this.archetypesByComponent.delete(componentType);
1544
- }
1545
- }
1546
-
1547
- // Clean up reverse index
1548
- this.removeFromReferencingIndex(componentType, archetype);
1549
- }
1550
-
1551
- this.queryRegistry.onArchetypeRemoved(archetype);
1150
+ private archetypeMatchesHook(archetype: Archetype, entry: LifecycleHookEntry): boolean {
1151
+ return this.archetypeManager.archetypeMatchesHook(archetype, entry);
1552
1152
  }
1553
1153
 
1554
1154
  /**
@@ -1572,7 +1172,28 @@ export class World {
1572
1172
  * const savedData = JSON.parse(localStorage.getItem('save'));
1573
1173
  * const newWorld = new World(savedData);
1574
1174
  */
1175
+ // Thin delegator to the extracted CommandExecutor for cascade paths (destroy* methods).
1176
+ private removeComponentImmediate(entityId: EntityId, componentType: EntityId<any>, targetEntityId: EntityId): void {
1177
+ this.commandExecutor.removeComponentImmediate(entityId, componentType, targetEntityId);
1178
+ }
1179
+
1180
+ private createHooksContext(): HooksContext {
1181
+ // The executor owns the current HooksContext; expose for any remaining internal use.
1182
+ return (
1183
+ (this.commandExecutor as any).getHooksContext?.() ?? {
1184
+ multiHooks: this.hooks,
1185
+ has: (eid, ct) => this.has(eid, ct),
1186
+ get: (eid, ct) => this.get(eid, ct),
1187
+ getOptional: (eid, ct) => this.getOptional(eid, ct),
1188
+ }
1189
+ );
1190
+ }
1191
+
1575
1192
  serialize(): SerializedWorld {
1576
- return serializeWorld(this.archetypes, this.componentEntities, this.entityIdManager);
1193
+ return serializeWorld(
1194
+ this.archetypeManager.archetypes as Archetype[],
1195
+ this.componentEntities,
1196
+ this.entityIdManager,
1197
+ );
1577
1198
  }
1578
1199
  }