@codehz/ecs 0.9.0 → 0.10.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.
@@ -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,30 +27,25 @@ 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
@@ -66,52 +54,59 @@ import { deserializeWorld, serializeWorld } from "./serialization";
66
54
  export class World {
67
55
  // Core data structures for entity and archetype management
68
56
  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
57
  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
58
  /** Sparse relation storage (for components created with `sparse: true`), shared with all Archetype instances */
77
59
  private readonly sparseStore = new SparseStoreImpl();
78
60
  /** Component entity (singleton) storage */
79
61
  private readonly componentEntities = new ComponentEntityStore();
80
62
 
63
+ // Archetype storage, indexes, creation/removal, and referencing are now encapsulated
64
+ // in ArchetypeManager (extracted to reduce World line count and improve cohesion).
65
+ private archetypeManager!: ArchetypeManager;
66
+
67
+ // Temporary forwarding accessors so the rest of World (and its internal collaborators)
68
+ // can continue using the familiar names with almost zero call-site changes.
69
+ // These can be removed in a follow-up cleanup pass if desired.
70
+ private get archetypes() {
71
+ return this.archetypeManager.archetypes;
72
+ }
73
+ private get entityToArchetype() {
74
+ return this.archetypeManager.entityToArchetype;
75
+ }
76
+ private get archetypesByComponent() {
77
+ return this.archetypeManager.archetypesByComponent;
78
+ }
79
+ private get entityToReferencingArchetypes() {
80
+ return this.archetypeManager.entityToReferencingArchetypes;
81
+ }
82
+
81
83
  // Query registry – manages caching, ref counts, and archetype notifications
82
84
  private readonly queryRegistry = new QueryRegistry();
83
85
 
84
86
  // Lifecycle hooks (declared before cached contexts that reference them)
85
87
  private hooks: Set<LifecycleHookEntry> = new Set();
86
88
 
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
- };
89
+ // Debug observability (extracted to DebugStatsManager to reduce World line count)
90
+ private readonly debugStats = new DebugStatsManager();
91
+
92
+ // Command execution (orchestration extracted to CommandExecutor)
93
+ private commandBuffer!: CommandBuffer;
94
+ private commandExecutor!: CommandExecutor;
113
95
 
114
96
  constructor(snapshot?: SerializedWorld) {
97
+ // Must create the manager before any code that may invoke ensureArchetype
98
+ // (including the snapshot deserialization path below, and the closures
99
+ // captured in the field-initialized _commandCtx).
100
+ this.archetypeManager = new ArchetypeManager(
101
+ {
102
+ queryRegistry: this.queryRegistry,
103
+ hooks: this.hooks,
104
+ recordArchetypeCreated: () => this.debugStats.recordArchetypeCreated(),
105
+ recordArchetypeRemoved: () => this.debugStats.recordArchetypeRemoved(),
106
+ },
107
+ this.sparseStore,
108
+ );
109
+
115
110
  if (snapshot && typeof snapshot === "object") {
116
111
  deserializeWorld(
117
112
  {
@@ -119,15 +114,34 @@ export class World {
119
114
  componentEntities: this.componentEntities,
120
115
  entityReferences: this.entityReferences,
121
116
  ensureArchetype: (ct) => this.ensureArchetype(ct),
122
- setEntityToArchetype: (eid, arch) => this.entityToArchetype.set(eid, arch),
117
+ setEntityToArchetype: (eid, arch) => this.archetypeManager.entityToArchetype.set(eid, arch),
123
118
  },
124
119
  snapshot,
125
120
  );
126
121
  }
127
- }
128
122
 
129
- private createArchetypeSignature(componentTypes: EntityId<any>[]): string {
130
- return componentTypes.join(",");
123
+ // CommandExecutor must be created after archetypeManager (and debugStats) because its context
124
+ // closes over several pieces and the destroy fast path.
125
+ const execCtx: CommandExecutorContext = {
126
+ componentEntities: this.componentEntities,
127
+ entityReferences: this.entityReferences,
128
+ hooks: this.hooks,
129
+ entityToArchetype: this.entityToArchetype,
130
+ ensureArchetype: (ct) => this.ensureArchetype(ct),
131
+ sparseStore: this.sparseStore,
132
+ has: (eid, ct) => this.has(eid, ct),
133
+ get: (eid, ct) => this.get(eid, ct),
134
+ getOptional: (eid, ct) => this.getOptional(eid, ct),
135
+ destroyEntityImmediate: (eid) => this.destroyEntityImmediate(eid),
136
+ incrementMigrations: () => this.debugStats.incrementMigrations(),
137
+ triggerLifecycleHooks,
138
+ triggerRemoveHooksForEntityDeletion,
139
+ };
140
+ this.commandExecutor = new CommandExecutor(execCtx);
141
+
142
+ this.commandBuffer = new CommandBuffer((entityId, commands) =>
143
+ this.commandExecutor.executeEntityCommands(entityId, commands),
144
+ );
131
145
  }
132
146
 
133
147
  /**
@@ -255,71 +269,6 @@ export class World {
255
269
  return this.entityToArchetype.has(entityId);
256
270
  }
257
271
 
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
272
  /**
324
273
  * Adds or updates a component on an entity (or marks void component as present).
325
274
  * The change is buffered and takes effect after calling `world.sync()`.
@@ -331,27 +280,23 @@ export class World {
331
280
  * @overload set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void
332
281
  * Adds or updates a component with data on the entity
333
282
  *
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
283
  * @throws {Error} If the entity does not exist
338
284
  * @throws {Error} If the component type is invalid or is a wildcard relation
339
285
  *
340
286
  * @example
341
287
  * world.set(entity, Position, { x: 10, y: 20 });
342
288
  * world.set(entity, Marker); // void component
343
- * world.set(GlobalConfig, { debug: true }); // singleton component
289
+ * world.singleton(GlobalConfig).set({ debug: true }); // singleton component
344
290
  * world.sync(); // Apply changes
345
291
  */
346
292
  set(entityId: EntityId, componentType: EntityId<void>): void;
347
293
  set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void;
348
- set<T>(componentId: ComponentId<T>, component: NoInfer<T>): void;
349
294
  set(entityId: EntityId | ComponentId, componentTypeOrComponent?: EntityId | any, maybeComponent?: any): void {
350
295
  const {
351
296
  entityId: targetEntityId,
352
297
  componentType,
353
298
  component,
354
- } = this.resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent);
299
+ } = resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent, (id) => this.exists(id));
355
300
  this.commandBuffer.set(targetEntityId, componentType, component);
356
301
  }
357
302
 
@@ -381,9 +326,10 @@ export class World {
381
326
  remove<T>(componentId: ComponentId<T>): void;
382
327
  remove<T>(entityId: EntityId, componentType: EntityId<T>): void;
383
328
  remove<T>(entityId: EntityId | ComponentId, componentType?: EntityId<T>): void {
384
- const { entityId: targetEntityId, componentType: targetComponentType } = this.resolveRemoveOperation(
329
+ const { entityId: targetEntityId, componentType: targetComponentType } = resolveRemoveOperation(
385
330
  entityId,
386
331
  componentType,
332
+ (id) => this.exists(id),
387
333
  );
388
334
  this.commandBuffer.remove(targetEntityId, targetComponentType);
389
335
  }
@@ -403,6 +349,32 @@ export class World {
403
349
  this.commandBuffer.delete(entityId);
404
350
  }
405
351
 
352
+ /**
353
+ * Returns an explicit handle for a singleton component (component-as-entity).
354
+ *
355
+ * This is the preferred API for singleton components.
356
+ *
357
+ * @example
358
+ * const config = world.singleton(GlobalConfig);
359
+ * config.set({ debug: true });
360
+ * world.sync();
361
+ * console.log(config.get());
362
+ */
363
+ singleton<T>(componentId: ComponentId<T>): SingletonHandle<T> {
364
+ assertEntityExists(componentId, "Component entity", (id) => this.exists(id));
365
+ assertSetComponentTypeValid(componentId);
366
+
367
+ return new SingletonHandle(componentId, {
368
+ has: () => this.componentEntities.hasSingleton(componentId),
369
+ get: () => this.get(componentId),
370
+ getOptional: () => this.getOptional(componentId),
371
+ remove: () => this.commandBuffer.remove(componentId, componentId),
372
+ set: (value) => {
373
+ this.commandBuffer.set(componentId, componentId as EntityId<any>, value as any);
374
+ },
375
+ });
376
+ }
377
+
406
378
  /**
407
379
  * Checks if a specific **component** is present on an entity.
408
380
  *
@@ -612,7 +584,7 @@ export class World {
612
584
  entityId: EntityId,
613
585
  relationComp: ComponentId<T>,
614
586
  ): [target: EntityId<unknown>, data: T | undefined][] {
615
- this.assertEntityExists(entityId, "Entity");
587
+ assertEntityExists(entityId, "Entity", (id) => this.exists(id));
616
588
 
617
589
  const wildcard = relation(relationComp, "*") as WildcardRelationId<T>;
618
590
 
@@ -659,7 +631,7 @@ export class World {
659
631
  * given base component.
660
632
  */
661
633
  hasRelation(entityId: EntityId, relationComp: ComponentId<any>, targetId?: EntityId): boolean {
662
- this.assertEntityExists(entityId, "Entity");
634
+ assertEntityExists(entityId, "Entity", (id) => this.exists(id));
663
635
 
664
636
  if (targetId !== undefined) {
665
637
  const specific = relation(relationComp, targetId);
@@ -675,7 +647,7 @@ export class World {
675
647
  * Returns the number of relations of the given base component held by the entity.
676
648
  */
677
649
  countRelations(entityId: EntityId, relationComp: ComponentId<any>): number {
678
- this.assertEntityExists(entityId, "Entity");
650
+ assertEntityExists(entityId, "Entity", (id) => this.exists(id));
679
651
  const targets = this.getRelationTargets(entityId, relationComp);
680
652
  return targets.length;
681
653
  }
@@ -922,88 +894,7 @@ export class World {
922
894
  * This is intended for development/debugging and leak detection.
923
895
  */
924
896
  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
- }
897
+ return this.debugStats.createCollector(callback);
1007
898
  }
1008
899
 
1009
900
  /**
@@ -1017,29 +908,56 @@ export class World {
1017
908
  * world.sync(); // Apply all buffered changes
1018
909
  */
1019
910
  sync(): void {
1020
- const hasCollectors = this._debugCollectors.size > 0;
1021
-
1022
- const syncStart = hasCollectors ? performance.now() : 0;
1023
-
1024
- if (hasCollectors) {
1025
- this._resetDebugActivityCounters();
911
+ if (!this.debugStats.hasActiveCollectors()) {
912
+ // Fast path: no debug collectors, skip all timing and stats work
913
+ this.commandBuffer.execute();
914
+ return;
1026
915
  }
1027
916
 
1028
- const commandBufferStart = hasCollectors ? performance.now() : 0;
917
+ // Slow path: full instrumentation for active debug stats collectors
918
+ const syncStart = performance.now();
919
+ this.debugStats.resetActivity();
920
+
921
+ const commandBufferStart = performance.now();
1029
922
  const commandIterations = this.commandBuffer.execute();
1030
- const commandBufferEnd = hasCollectors ? performance.now() : 0;
923
+ const commandBufferEnd = performance.now();
924
+
925
+ const syncEnd = performance.now();
926
+
927
+ // Build the data bag for the extracted manager (keeps it decoupled from internal maps)
928
+ const entityCount = this.entityToArchetype.size;
929
+ let emptyArchetypes = 0;
930
+ for (const arch of this.archetypes) {
931
+ if (arch.size === 0) emptyArchetypes++;
932
+ }
1031
933
 
1032
- const syncEnd = hasCollectors ? performance.now() : 0;
934
+ let archetypesByComponentSize = 0;
935
+ for (const set of this.archetypesByComponent.values()) {
936
+ archetypesByComponentSize += set.size;
937
+ }
1033
938
 
1034
- if (hasCollectors) {
1035
- this._deliverDebugStats({
939
+ this.debugStats.deliver(
940
+ {
1036
941
  syncStart,
1037
942
  syncEnd,
1038
943
  commandBufferStart,
1039
944
  commandBufferEnd,
1040
945
  commandIterations,
1041
- });
1042
- }
946
+ },
947
+ {
948
+ entityCount,
949
+ freelistSize: this.entityIdManager.getFreelistSize(),
950
+ nextId: this.entityIdManager.getNextId(),
951
+ archetypeCount: this.archetypes.length,
952
+ emptyArchetypes,
953
+ archetypesByComponentSize,
954
+ cachedQueryCount: (this.queryRegistry as any).cache?.size ?? 0,
955
+ registeredQueryCount: (this.queryRegistry as any).queries?.size ?? 0,
956
+ hookCount: this.hooks.size,
957
+ entityReferencesSize: this.entityReferences.size,
958
+ entityToReferencingArchetypesSize: this.entityToReferencingArchetypes.size,
959
+ },
960
+ );
1043
961
  }
1044
962
 
1045
963
  /**
@@ -1076,7 +994,7 @@ export class World {
1076
994
  createQuery(componentTypes: EntityId<any>[], filter: QueryFilter = {}): Query {
1077
995
  const sortedTypes = normalizeComponentTypes(componentTypes);
1078
996
  const filterKey = serializeQueryFilter(filter);
1079
- const key = `${this.createArchetypeSignature(sortedTypes)}${filterKey ? `|${filterKey}` : ""}`;
997
+ const key = `${sortedTypes.join(",")}${filterKey ? `|${filterKey}` : ""}`;
1080
998
  return this.queryRegistry.getOrCreate(this, sortedTypes, key, filter);
1081
999
  }
1082
1000
 
@@ -1146,71 +1064,7 @@ export class World {
1146
1064
  * @internal
1147
1065
  */
1148
1066
  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);
1067
+ return this.archetypeManager.getMatchingArchetypes(componentTypes);
1214
1068
  }
1215
1069
 
1216
1070
  /**
@@ -1268,287 +1122,17 @@ export class World {
1268
1122
  }
1269
1123
  }
1270
1124
 
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
-
1125
+ // Delegators to the extracted ArchetypeManager (keeps public + internal call sites unchanged).
1409
1126
  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
- );
1127
+ return this.archetypeManager.ensureArchetype(componentTypes);
1508
1128
  }
1509
1129
 
1510
1130
  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);
1131
+ this.archetypeManager.cleanupArchetypesReferencingEntity(entityId);
1521
1132
  }
1522
1133
 
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);
1134
+ private archetypeMatchesHook(archetype: Archetype, entry: LifecycleHookEntry): boolean {
1135
+ return this.archetypeManager.archetypeMatchesHook(archetype, entry);
1552
1136
  }
1553
1137
 
1554
1138
  /**
@@ -1572,7 +1156,28 @@ export class World {
1572
1156
  * const savedData = JSON.parse(localStorage.getItem('save'));
1573
1157
  * const newWorld = new World(savedData);
1574
1158
  */
1159
+ // Thin delegator to the extracted CommandExecutor for cascade paths (destroy* methods).
1160
+ private removeComponentImmediate(entityId: EntityId, componentType: EntityId<any>, targetEntityId: EntityId): void {
1161
+ this.commandExecutor.removeComponentImmediate(entityId, componentType, targetEntityId);
1162
+ }
1163
+
1164
+ private createHooksContext(): HooksContext {
1165
+ // The executor owns the current HooksContext; expose for any remaining internal use.
1166
+ return (
1167
+ (this.commandExecutor as any).getHooksContext?.() ?? {
1168
+ multiHooks: this.hooks,
1169
+ has: (eid, ct) => this.has(eid, ct),
1170
+ get: (eid, ct) => this.get(eid, ct),
1171
+ getOptional: (eid, ct) => this.getOptional(eid, ct),
1172
+ }
1173
+ );
1174
+ }
1175
+
1575
1176
  serialize(): SerializedWorld {
1576
- return serializeWorld(this.archetypes, this.componentEntities, this.entityIdManager);
1177
+ return serializeWorld(
1178
+ this.archetypeManager.archetypes as Archetype[],
1179
+ this.componentEntities,
1180
+ this.entityIdManager,
1181
+ );
1577
1182
  }
1578
1183
  }