@codehz/ecs 0.6.8 → 0.6.10

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.
package/world.mjs CHANGED
@@ -98,16 +98,15 @@ function decodeRelationId(relationId) {
98
98
  function getIdType(id) {
99
99
  if (isComponentId(id)) return "component";
100
100
  if (isEntityId(id)) return "entity";
101
- if (id < 0) try {
102
- const decoded = decodeRelationId(id);
103
- if (decoded.type !== "wildcard" && !isEntityId(decoded.targetId) && !isComponentId(decoded.targetId)) return "invalid";
104
- switch (decoded.type) {
105
- case "entity": return "entity-relation";
106
- case "component": return "component-relation";
107
- case "wildcard": return "wildcard-relation";
108
- }
109
- } catch (_error) {
110
- return "invalid";
101
+ if (id < 0) {
102
+ const decoded = decodeRelationRaw(id);
103
+ if (decoded === null) return "invalid";
104
+ const { componentId: rawComponentId, targetId: rawTargetId } = decoded;
105
+ if (!isValidComponentId(rawComponentId)) return "invalid";
106
+ if (rawTargetId === WILDCARD_TARGET_ID) return "wildcard-relation";
107
+ else if (isEntityId(rawTargetId)) return "entity-relation";
108
+ else if (isComponentId(rawTargetId)) return "component-relation";
109
+ else return "invalid";
111
110
  }
112
111
  return "invalid";
113
112
  }
@@ -119,28 +118,29 @@ function getIdType(id) {
119
118
  function getDetailedIdType(id) {
120
119
  if (isComponentId(id)) return { type: "component" };
121
120
  if (isEntityId(id)) return { type: "entity" };
122
- if (id < 0) try {
123
- const decoded = decodeRelationId(id);
124
- if (decoded.type !== "wildcard" && !isEntityId(decoded.targetId) && !isComponentId(decoded.targetId)) return { type: "invalid" };
125
- let type;
126
- switch (decoded.type) {
127
- case "entity":
128
- type = "entity-relation";
129
- break;
130
- case "component":
131
- type = "component-relation";
132
- break;
133
- case "wildcard":
134
- type = "wildcard-relation";
135
- break;
136
- }
137
- return {
138
- type,
139
- componentId: decoded.componentId,
140
- targetId: decoded.targetId
121
+ if (id < 0) {
122
+ const decoded = decodeRelationRaw(id);
123
+ if (decoded === null) return { type: "invalid" };
124
+ const { componentId: rawComponentId, targetId: rawTargetId } = decoded;
125
+ if (!isValidComponentId(rawComponentId)) return { type: "invalid" };
126
+ const componentId = rawComponentId;
127
+ const targetId = rawTargetId;
128
+ if (targetId === WILDCARD_TARGET_ID) return {
129
+ type: "wildcard-relation",
130
+ componentId,
131
+ targetId
141
132
  };
142
- } catch (_error) {
143
- return { type: "invalid" };
133
+ else if (isEntityId(targetId)) return {
134
+ type: "entity-relation",
135
+ componentId,
136
+ targetId
137
+ };
138
+ else if (isComponentId(targetId)) return {
139
+ type: "component-relation",
140
+ componentId,
141
+ targetId
142
+ };
143
+ else return { type: "invalid" };
144
144
  }
145
145
  return { type: "invalid" };
146
146
  }
@@ -175,17 +175,18 @@ function isEntityRelation(id) {
175
175
  */
176
176
  var EntityIdManager = class {
177
177
  nextId = ENTITY_ID_START;
178
- freelist = /* @__PURE__ */ new Set();
178
+ /**
179
+ * Free list uses a stack (LIFO) for better memory locality when reusing IDs.
180
+ * We use an array instead of a Set for significantly better performance.
181
+ */
182
+ freelist = [];
179
183
  /**
180
184
  * Allocate a new entity ID
181
185
  * Uses freelist if available, otherwise increments counter
182
186
  */
183
187
  allocate() {
184
- if (this.freelist.size > 0) {
185
- const id = this.freelist.values().next().value;
186
- this.freelist.delete(id);
187
- return id;
188
- } else {
188
+ if (this.freelist.length > 0) return this.freelist.pop();
189
+ else {
189
190
  const id = this.nextId;
190
191
  this.nextId++;
191
192
  if (this.nextId >= Number.MAX_SAFE_INTEGER) throw new Error("Entity ID overflow: reached maximum safe integer");
@@ -199,13 +200,13 @@ var EntityIdManager = class {
199
200
  deallocate(id) {
200
201
  if (!isEntityId(id)) throw new Error("Can only deallocate valid entity IDs");
201
202
  if (id >= this.nextId) throw new Error("Cannot deallocate an ID that was never allocated");
202
- this.freelist.add(id);
203
+ this.freelist.push(id);
203
204
  }
204
205
  /**
205
206
  * Get the current freelist size (for debugging/monitoring)
206
207
  */
207
208
  getFreelistSize() {
208
- return this.freelist.size;
209
+ return this.freelist.length;
209
210
  }
210
211
  /**
211
212
  * Get the next ID that would be allocated (for debugging)
@@ -230,7 +231,7 @@ var EntityIdManager = class {
230
231
  deserializeState(state) {
231
232
  if (typeof state.nextId !== "number") throw new Error("Invalid state for EntityIdManager.deserializeState");
232
233
  this.nextId = state.nextId;
233
- this.freelist = new Set(state.freelist || []);
234
+ this.freelist = state.freelist || [];
234
235
  }
235
236
  };
236
237
  /**
@@ -368,6 +369,7 @@ const componentNames = new Array(COMPONENT_ID_MAX + 1);
368
369
  const exclusiveFlags = new BitSet(COMPONENT_ID_MAX + 1);
369
370
  const cascadeDeleteFlags = new BitSet(COMPONENT_ID_MAX + 1);
370
371
  const dontFragmentFlags = new BitSet(COMPONENT_ID_MAX + 1);
372
+ const componentMerges = new Array(COMPONENT_ID_MAX + 1);
371
373
  /**
372
374
  * Allocate a new component ID from the global allocator.
373
375
  * @param nameOrOptions Optional name for the component (for serialization/debugging) or options object
@@ -400,6 +402,7 @@ function component(nameOrOptions) {
400
402
  if (options.exclusive) exclusiveFlags.set(id);
401
403
  if (options.cascadeDelete) cascadeDeleteFlags.set(id);
402
404
  if (options.dontFragment) dontFragmentFlags.set(id);
405
+ if (options.merge) componentMerges[id] = options.merge;
403
406
  }
404
407
  return id;
405
408
  }
@@ -418,6 +421,21 @@ function getComponentIdByName(name) {
418
421
  function getComponentNameById(id) {
419
422
  return componentNames[id];
420
423
  }
424
+ function getBaseComponentId(componentType) {
425
+ if (isComponentId(componentType)) return componentType;
426
+ const decoded = decodeRelationRaw(componentType);
427
+ if (decoded === null) return void 0;
428
+ return isValidComponentId(decoded.componentId) ? decoded.componentId : void 0;
429
+ }
430
+ /**
431
+ * Get merge callback for a componentType (including relation component types).
432
+ * Returns undefined if the base component has no merge callback.
433
+ */
434
+ function getComponentMerge(componentType) {
435
+ const baseComponentId = getBaseComponentId(componentType);
436
+ if (baseComponentId === void 0) return void 0;
437
+ return componentMerges[baseComponentId];
438
+ }
421
439
  /**
422
440
  * Check if a component is marked as exclusive
423
441
  * @param id The component ID
@@ -627,10 +645,17 @@ var ComponentChangeset = class {
627
645
  //#endregion
628
646
  //#region src/commands/command-buffer.ts
629
647
  /**
648
+ * Maximum number of command buffer execution iterations to prevent infinite loops
649
+ */
650
+ const MAX_COMMAND_ITERATIONS = 100;
651
+ /**
630
652
  * Command buffer for deferred structural changes
631
653
  */
632
654
  var CommandBuffer = class {
633
655
  commands = [];
656
+ swapBuffer = [];
657
+ /** Reusable map to group commands by entity, avoids per-sync allocations */
658
+ entityCommands = /* @__PURE__ */ new Map();
634
659
  executeEntityCommands;
635
660
  /**
636
661
  * Create a command buffer with an executor function
@@ -669,19 +694,22 @@ var CommandBuffer = class {
669
694
  * Execute all commands and clear the buffer
670
695
  */
671
696
  execute() {
672
- const MAX_ITERATIONS = 100;
673
697
  let iterations = 0;
674
698
  while (this.commands.length > 0) {
675
- if (iterations >= MAX_ITERATIONS) throw new Error("Command execution exceeded maximum iterations, possible infinite loop");
699
+ if (iterations >= MAX_COMMAND_ITERATIONS) throw new Error("Command execution exceeded maximum iterations, possible infinite loop");
676
700
  iterations++;
677
- const currentCommands = [...this.commands];
678
- this.commands = [];
679
- const entityCommands = /* @__PURE__ */ new Map();
701
+ const currentCommands = this.commands;
702
+ this.commands = this.swapBuffer;
703
+ const entityCommands = this.entityCommands;
680
704
  for (const cmd of currentCommands) {
681
- if (!entityCommands.has(cmd.entityId)) entityCommands.set(cmd.entityId, []);
682
- entityCommands.get(cmd.entityId).push(cmd);
705
+ const existing = entityCommands.get(cmd.entityId);
706
+ if (existing !== void 0) existing.push(cmd);
707
+ else entityCommands.set(cmd.entityId, [cmd]);
683
708
  }
709
+ currentCommands.length = 0;
710
+ this.swapBuffer = currentCommands;
684
711
  for (const [entityId, commands] of entityCommands) this.executeEntityCommands(entityId, commands);
712
+ entityCommands.clear();
685
713
  }
686
714
  }
687
715
  /**
@@ -721,8 +749,8 @@ function matchesComponentTypes(archetype, componentTypes) {
721
749
  });
722
750
  else if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId !== void 0 && isDontFragmentComponent(detailedType.componentId)) {
723
751
  const wildcardMarker = relation(detailedType.componentId, "*");
724
- return archetype.componentTypes.includes(wildcardMarker);
725
- } else return archetype.componentTypes.includes(type);
752
+ return archetype.componentTypeSet.has(wildcardMarker);
753
+ } else return archetype.componentTypeSet.has(type);
726
754
  });
727
755
  }
728
756
  /**
@@ -735,10 +763,20 @@ function matchesFilter(archetype, filter) {
735
763
  if (!isRelationId(archetypeType)) return false;
736
764
  return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
737
765
  });
738
- else return !archetype.componentTypes.includes(type);
766
+ else return !archetype.componentTypeSet.has(type);
739
767
  });
740
768
  }
741
769
 
770
+ //#endregion
771
+ //#region src/core/component-type-utils.ts
772
+ /**
773
+ * Normalize component type collections into a stable ascending order.
774
+ * This keeps cache keys and archetype signatures deterministic.
775
+ */
776
+ function normalizeComponentTypes(componentTypes) {
777
+ return [...componentTypes].sort((a, b) => a - b);
778
+ }
779
+
742
780
  //#endregion
743
781
  //#region src/query/query.ts
744
782
  /**
@@ -750,13 +788,15 @@ var Query = class {
750
788
  filter;
751
789
  cachedArchetypes = [];
752
790
  isDisposed = false;
791
+ /** Cache key assigned by World for O(1) releaseQuery lookup */
792
+ _cacheKey;
753
793
  /** Cached wildcard component types for faster entity filtering */
754
794
  wildcardTypes;
755
795
  /** Cached specific dontFragment relation types that need entity-level filtering */
756
796
  specificDontFragmentTypes;
757
797
  constructor(world, componentTypes, filter = {}) {
758
798
  this.world = world;
759
- this.componentTypes = [...componentTypes].sort((a, b) => a - b);
799
+ this.componentTypes = normalizeComponentTypes(componentTypes);
760
800
  this.filter = filter;
761
801
  this.wildcardTypes = this.componentTypes.filter((ct) => getDetailedIdType(ct).type === "wildcard-relation");
762
802
  this.specificDontFragmentTypes = this.componentTypes.filter((ct) => {
@@ -903,10 +943,10 @@ var Query = class {
903
943
  * Get a value from cache or compute and cache it if not present
904
944
  * @param cache The cache map
905
945
  * @param key The cache key
906
- * @param compute Function to compute the value if not cached
946
+ * @param compute Function to compute the value if not cached (may have side effects)
907
947
  * @returns The cached or computed value
908
948
  */
909
- function getOrComputeCache(cache, key, compute) {
949
+ function getOrCompute(cache, key, compute) {
910
950
  let value = cache.get(key);
911
951
  if (value === void 0) {
912
952
  value = compute();
@@ -914,21 +954,6 @@ function getOrComputeCache(cache, key, compute) {
914
954
  }
915
955
  return value;
916
956
  }
917
- /**
918
- * Get a value from cache or create and cache it if not present, allowing side effects during creation
919
- * @param cache The cache map
920
- * @param key The cache key
921
- * @param create Function to create the value if not cached (can have side effects)
922
- * @returns The cached or created value
923
- */
924
- function getOrCreateWithSideEffect(cache, key, create) {
925
- let value = cache.get(key);
926
- if (value === void 0) {
927
- value = create();
928
- cache.set(key, value);
929
- }
930
- return value;
931
- }
932
957
 
933
958
  //#endregion
934
959
  //#region src/core/types.ts
@@ -939,6 +964,18 @@ function isOptionalEntityId(type) {
939
964
  //#endregion
940
965
  //#region src/core/archetype-helpers.ts
941
966
  /**
967
+ * Check if a components map has any wildcard relations matching a component ID
968
+ * @param components - Component entity's components map
969
+ * @param wildcardComponentId - The component ID to match
970
+ * @returns True if at least one matching relation exists
971
+ */
972
+ function hasWildcardRelation(components, wildcardComponentId) {
973
+ for (const relId of components.keys()) if (isRelationId(relId)) {
974
+ if (getComponentIdFromRelationId(relId) === wildcardComponentId) return true;
975
+ }
976
+ return false;
977
+ }
978
+ /**
942
979
  * Check if a detailed type represents a relation (entity or component)
943
980
  */
944
981
  function isRelationType(detailedType) {
@@ -1036,6 +1073,10 @@ var Archetype = class {
1036
1073
  */
1037
1074
  componentTypes;
1038
1075
  /**
1076
+ * Set version of componentTypes for O(1) lookups in hot paths
1077
+ */
1078
+ componentTypeSet;
1079
+ /**
1039
1080
  * List of entities in this archetype
1040
1081
  */
1041
1082
  entities = [];
@@ -1055,9 +1096,6 @@ var Archetype = class {
1055
1096
  */
1056
1097
  dontFragmentRelations;
1057
1098
  /**
1058
- * Cache for pre-computed component data sources to avoid repeated calculations
1059
- */
1060
- /**
1061
1099
  * Multi-hooks that match this archetype
1062
1100
  */
1063
1101
  matchingMultiHooks = /* @__PURE__ */ new Set();
@@ -1066,16 +1104,23 @@ var Archetype = class {
1066
1104
  */
1067
1105
  componentDataSourcesCache = /* @__PURE__ */ new Map();
1068
1106
  constructor(componentTypes, dontFragmentRelations) {
1069
- this.componentTypes = [...componentTypes].sort((a, b) => a - b);
1107
+ this.componentTypes = normalizeComponentTypes(componentTypes);
1108
+ this.componentTypeSet = new Set(this.componentTypes);
1070
1109
  this.dontFragmentRelations = dontFragmentRelations;
1071
1110
  for (const componentType of this.componentTypes) this.componentData.set(componentType, []);
1072
1111
  }
1073
1112
  get size() {
1074
1113
  return this.entities.length;
1075
1114
  }
1115
+ /**
1116
+ * Check if the given component types match this archetype
1117
+ * @param componentTypes - Component types to check (can be in any order)
1118
+ * @returns true if the types match this archetype's component set
1119
+ * @note This method handles unsorted input by internally sorting for comparison
1120
+ */
1076
1121
  matches(componentTypes) {
1077
1122
  if (this.componentTypes.length !== componentTypes.length) return false;
1078
- const sortedTypes = [...componentTypes].sort((a, b) => a - b);
1123
+ const sortedTypes = normalizeComponentTypes(componentTypes);
1079
1124
  return this.componentTypes.every((type, index) => type === sortedTypes[index]);
1080
1125
  }
1081
1126
  addEntity(entityId, componentData) {
@@ -1092,7 +1137,7 @@ var Archetype = class {
1092
1137
  addDontFragmentRelations(entityId, componentData) {
1093
1138
  const dontFragmentData = /* @__PURE__ */ new Map();
1094
1139
  for (const [componentType, data] of componentData) {
1095
- if (this.componentTypes.includes(componentType)) continue;
1140
+ if (this.componentTypeSet.has(componentType)) continue;
1096
1141
  const detailedType = getDetailedIdType(componentType);
1097
1142
  if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId)) dontFragmentData.set(componentType, data);
1098
1143
  }
@@ -1110,6 +1155,9 @@ var Archetype = class {
1110
1155
  if (dontFragmentData) for (const [componentType, data] of dontFragmentData) entityData.set(componentType, data);
1111
1156
  return entityData;
1112
1157
  }
1158
+ getEntityDontFragmentRelations(entityId) {
1159
+ return this.dontFragmentRelations.get(entityId);
1160
+ }
1113
1161
  dump() {
1114
1162
  return this.entities.map((entity, i) => {
1115
1163
  const components = /* @__PURE__ */ new Map();
@@ -1176,7 +1224,7 @@ var Archetype = class {
1176
1224
  return relations;
1177
1225
  }
1178
1226
  getRegularComponent(entityId, index, componentType) {
1179
- if (this.componentTypes.includes(componentType)) {
1227
+ if (this.componentTypeSet.has(componentType)) {
1180
1228
  const data = this.getComponentData(componentType)[index];
1181
1229
  if (data === MISSING_COMPONENT) throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
1182
1230
  return data;
@@ -1188,7 +1236,7 @@ var Archetype = class {
1188
1236
  getOptional(entityId, componentType) {
1189
1237
  const index = this.entityToIndex.get(entityId);
1190
1238
  if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
1191
- if (this.componentTypes.includes(componentType)) {
1239
+ if (this.componentTypeSet.has(componentType)) {
1192
1240
  const data = this.getComponentData(componentType)[index];
1193
1241
  if (data === MISSING_COMPONENT) return void 0;
1194
1242
  return { value: data };
@@ -1231,7 +1279,7 @@ var Archetype = class {
1231
1279
  }
1232
1280
  getCachedComponentDataSources(componentTypes) {
1233
1281
  const cacheKey = buildCacheKey(componentTypes);
1234
- return getOrComputeCache(this.componentDataSourcesCache, cacheKey, () => componentTypes.map((compType) => this.getComponentDataSource(compType)));
1282
+ return getOrCompute(this.componentDataSourcesCache, cacheKey, () => componentTypes.map((compType) => this.getComponentDataSource(compType)));
1235
1283
  }
1236
1284
  getComponentDataSource(compType) {
1237
1285
  const optional = isOptionalEntityId(compType);
@@ -1376,8 +1424,8 @@ function decodeSerializedId(sid) {
1376
1424
  //#endregion
1377
1425
  //#region src/core/world-commands.ts
1378
1426
  function processCommands(entityId, currentArchetype, commands, changeset, handleExclusiveRelation) {
1379
- for (const command of commands) if (command.type === "set" && command.componentType) processSetCommand(entityId, currentArchetype, command.componentType, command.component, changeset, handleExclusiveRelation);
1380
- else if (command.type === "delete" && command.componentType) processDeleteCommand(entityId, currentArchetype, command.componentType, changeset);
1427
+ for (const command of commands) if (command.type === "set") processSetCommand(entityId, currentArchetype, command.componentType, command.component, changeset, handleExclusiveRelation);
1428
+ else if (command.type === "delete") processDeleteCommand(entityId, currentArchetype, command.componentType, changeset);
1381
1429
  }
1382
1430
  function processSetCommand(entityId, currentArchetype, componentType, component$1, changeset, handleExclusiveRelation) {
1383
1431
  const componentId = getComponentIdFromRelationId(componentType);
@@ -1385,9 +1433,15 @@ function processSetCommand(entityId, currentArchetype, componentType, component$
1385
1433
  handleExclusiveRelation(entityId, currentArchetype, componentId);
1386
1434
  if (isDontFragmentComponent(componentId)) {
1387
1435
  const wildcardMarker = relation(componentId, "*");
1388
- if (!currentArchetype.componentTypes.includes(wildcardMarker)) changeset.set(wildcardMarker, void 0);
1436
+ if (!currentArchetype.componentTypeSet.has(wildcardMarker)) changeset.set(wildcardMarker, void 0);
1389
1437
  }
1390
1438
  }
1439
+ const merge = getComponentMerge(componentType);
1440
+ if (merge !== void 0 && changeset.adds.has(componentType)) {
1441
+ const prev = changeset.adds.get(componentType);
1442
+ changeset.set(componentType, merge(prev, component$1));
1443
+ return;
1444
+ }
1391
1445
  changeset.set(componentType, component$1);
1392
1446
  }
1393
1447
  function processDeleteCommand(entityId, currentArchetype, componentType, changeset) {
@@ -1403,10 +1457,9 @@ function removeMatchingRelations(entityId, archetype, baseComponentId, changeset
1403
1457
  if (isWildcardRelationId(componentType)) continue;
1404
1458
  if (getComponentIdFromRelationId(componentType) === baseComponentId) changeset.delete(componentType);
1405
1459
  }
1406
- const entityData = archetype.getEntity(entityId);
1407
- if (entityData) for (const [componentType] of entityData) {
1408
- if (archetype.componentTypes.includes(componentType)) continue;
1409
- if (getComponentIdFromRelationId(componentType) === baseComponentId) changeset.delete(componentType);
1460
+ const dontFragmentData = archetype.getEntityDontFragmentRelations(entityId);
1461
+ if (dontFragmentData) {
1462
+ for (const componentType of dontFragmentData.keys()) if (getComponentIdFromRelationId(componentType) === baseComponentId) changeset.delete(componentType);
1410
1463
  }
1411
1464
  }
1412
1465
  function removeWildcardRelations(entityId, currentArchetype, baseComponentId, changeset) {
@@ -1416,35 +1469,66 @@ function removeWildcardRelations(entityId, currentArchetype, baseComponentId, ch
1416
1469
  function maybeRemoveWildcardMarker(entityId, archetype, removedComponentType, componentId, changeset) {
1417
1470
  if (componentId === void 0 || !isDontFragmentComponent(componentId)) return;
1418
1471
  const wildcardMarker = relation(componentId, "*");
1419
- const entityData = archetype.getEntity(entityId);
1420
- if (!entityData) {
1421
- changeset.delete(wildcardMarker);
1422
- return;
1423
- }
1424
- for (const [otherComponentType] of entityData) {
1472
+ for (const otherComponentType of archetype.componentTypes) {
1425
1473
  if (otherComponentType === removedComponentType) continue;
1426
1474
  if (otherComponentType === wildcardMarker) continue;
1427
1475
  if (changeset.removes.has(otherComponentType)) continue;
1428
1476
  if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
1429
1477
  }
1478
+ const dontFragmentData = archetype.getEntityDontFragmentRelations(entityId);
1479
+ if (dontFragmentData) for (const otherComponentType of dontFragmentData.keys()) {
1480
+ if (otherComponentType === removedComponentType) continue;
1481
+ if (changeset.removes.has(otherComponentType)) continue;
1482
+ if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
1483
+ }
1430
1484
  changeset.delete(wildcardMarker);
1431
1485
  }
1486
+ function hasEntityComponent(archetype, entityId, componentType) {
1487
+ if (archetype.componentTypeSet.has(componentType)) return true;
1488
+ return archetype.getEntityDontFragmentRelations(entityId)?.has(componentType) ?? false;
1489
+ }
1490
+ function pruneMissingRemovals(changeset, archetype, entityId) {
1491
+ let toPrune;
1492
+ for (const componentType of changeset.removes) if (!hasEntityComponent(archetype, entityId, componentType)) {
1493
+ if (toPrune === void 0) toPrune = [];
1494
+ toPrune.push(componentType);
1495
+ }
1496
+ if (toPrune !== void 0) for (const componentType of toPrune) changeset.removes.delete(componentType);
1497
+ }
1498
+ function hasArchetypeStructuralChange(changeset, currentArchetype) {
1499
+ for (const componentType of changeset.removes) if (!isDontFragmentRelation(componentType) && currentArchetype.componentTypeSet.has(componentType)) return true;
1500
+ for (const componentType of changeset.adds.keys()) if (!isDontFragmentRelation(componentType) && !currentArchetype.componentTypeSet.has(componentType)) return true;
1501
+ return false;
1502
+ }
1503
+ function buildFinalRegularComponentTypes(currentArchetype, changeset) {
1504
+ const finalRegularTypes = new Set(currentArchetype.componentTypes);
1505
+ for (const componentType of changeset.removes) if (!isDontFragmentRelation(componentType)) finalRegularTypes.delete(componentType);
1506
+ for (const componentType of changeset.adds.keys()) if (!isDontFragmentRelation(componentType)) finalRegularTypes.add(componentType);
1507
+ return Array.from(finalRegularTypes);
1508
+ }
1432
1509
  function applyChangeset(ctx, entityId, currentArchetype, changeset, entityToArchetype) {
1433
- const currentEntityData = currentArchetype.getEntity(entityId);
1434
- const allCurrentComponentTypes = currentEntityData ? Array.from(currentEntityData.keys()) : currentArchetype.componentTypes;
1435
- const finalComponentTypes = changeset.getFinalComponentTypes(allCurrentComponentTypes);
1436
1510
  const removedComponents = /* @__PURE__ */ new Map();
1437
- if (finalComponentTypes) if (!areComponentTypesEqual(filterRegularComponentTypes(allCurrentComponentTypes), filterRegularComponentTypes(finalComponentTypes))) return {
1511
+ pruneMissingRemovals(changeset, currentArchetype, entityId);
1512
+ if (hasArchetypeStructuralChange(changeset, currentArchetype)) return {
1438
1513
  removedComponents,
1439
- newArchetype: moveEntityToNewArchetype(ctx, entityId, currentArchetype, finalComponentTypes, changeset, removedComponents, entityToArchetype)
1514
+ newArchetype: moveEntityToNewArchetype(ctx, entityId, currentArchetype, buildFinalRegularComponentTypes(currentArchetype, changeset), changeset, removedComponents, entityToArchetype)
1440
1515
  };
1441
- else updateEntityInSameArchetype(ctx, entityId, currentArchetype, changeset, removedComponents);
1442
- else updateEntityInSameArchetype(ctx, entityId, currentArchetype, changeset, removedComponents);
1516
+ updateEntityInSameArchetype(ctx, entityId, currentArchetype, changeset, removedComponents);
1443
1517
  return {
1444
1518
  removedComponents,
1445
1519
  newArchetype: currentArchetype
1446
1520
  };
1447
1521
  }
1522
+ /**
1523
+ * Optimized variant of applyChangeset for when no lifecycle hooks are registered.
1524
+ * Skips creating the removedComponents map, reducing allocations in the hot path.
1525
+ */
1526
+ function applyChangesetNoHooks(ctx, entityId, currentArchetype, changeset, entityToArchetype) {
1527
+ pruneMissingRemovals(changeset, currentArchetype, entityId);
1528
+ if (hasArchetypeStructuralChange(changeset, currentArchetype)) return moveEntityToNewArchetypeNoHooks(ctx, entityId, currentArchetype, buildFinalRegularComponentTypes(currentArchetype, changeset), changeset, entityToArchetype);
1529
+ updateEntityInSameArchetypeNoHooks(ctx, entityId, currentArchetype, changeset);
1530
+ return currentArchetype;
1531
+ }
1448
1532
  function moveEntityToNewArchetype(ctx, entityId, currentArchetype, finalComponentTypes, changeset, removedComponents, entityToArchetype) {
1449
1533
  const newArchetype = ctx.ensureArchetype(finalComponentTypes);
1450
1534
  const currentComponents = currentArchetype.removeEntity(entityId);
@@ -1460,6 +1544,28 @@ function updateEntityInSameArchetype(ctx, entityId, currentArchetype, changeset,
1460
1544
  currentArchetype.set(entityId, componentType, component$1);
1461
1545
  }
1462
1546
  }
1547
+ /**
1548
+ * No-hooks variant: moves entity to new archetype without collecting removed component data.
1549
+ * Only called from applyChangesetNoHooks when no lifecycle hooks are registered.
1550
+ */
1551
+ function moveEntityToNewArchetypeNoHooks(ctx, entityId, currentArchetype, finalComponentTypes, changeset, entityToArchetype) {
1552
+ const newArchetype = ctx.ensureArchetype(finalComponentTypes);
1553
+ const currentComponents = currentArchetype.removeEntity(entityId);
1554
+ newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
1555
+ entityToArchetype.set(entityId, newArchetype);
1556
+ return newArchetype;
1557
+ }
1558
+ /**
1559
+ * No-hooks variant: updates entity in same archetype without tracking removed component data.
1560
+ * Only called from applyChangesetNoHooks when no lifecycle hooks are registered.
1561
+ */
1562
+ function updateEntityInSameArchetypeNoHooks(ctx, entityId, currentArchetype, changeset) {
1563
+ applyDontFragmentChangesNoHooks(ctx.dontFragmentRelations, entityId, changeset);
1564
+ for (const [componentType, component$1] of changeset.adds) {
1565
+ if (isDontFragmentRelation(componentType)) continue;
1566
+ currentArchetype.set(entityId, componentType, component$1);
1567
+ }
1568
+ }
1463
1569
  function applyDontFragmentChanges(dontFragmentRelations, entityId, changeset, removedComponents) {
1464
1570
  let entityRelations = dontFragmentRelations.get(entityId);
1465
1571
  for (const componentType of changeset.removes) if (isDontFragmentRelation(componentType)) {
@@ -1480,6 +1586,23 @@ function applyDontFragmentChanges(dontFragmentRelations, entityId, changeset, re
1480
1586
  }
1481
1587
  if (entityRelations && entityRelations.size === 0) dontFragmentRelations.delete(entityId);
1482
1588
  }
1589
+ /**
1590
+ * No-hooks variant of applyDontFragmentChanges that skips tracking removed component data.
1591
+ */
1592
+ function applyDontFragmentChangesNoHooks(dontFragmentRelations, entityId, changeset) {
1593
+ let entityRelations = dontFragmentRelations.get(entityId);
1594
+ for (const componentType of changeset.removes) if (isDontFragmentRelation(componentType)) {
1595
+ if (entityRelations) entityRelations.delete(componentType);
1596
+ }
1597
+ for (const [componentType, component$1] of changeset.adds) if (isDontFragmentRelation(componentType)) {
1598
+ if (!entityRelations) {
1599
+ entityRelations = /* @__PURE__ */ new Map();
1600
+ dontFragmentRelations.set(entityId, entityRelations);
1601
+ }
1602
+ entityRelations.set(componentType, component$1);
1603
+ }
1604
+ if (entityRelations && entityRelations.size === 0) dontFragmentRelations.delete(entityId);
1605
+ }
1483
1606
  function filterRegularComponentTypes(componentTypes) {
1484
1607
  const regularTypes = [];
1485
1608
  for (const componentType of componentTypes) {
@@ -1492,12 +1615,6 @@ function filterRegularComponentTypes(componentTypes) {
1492
1615
  }
1493
1616
  return regularTypes;
1494
1617
  }
1495
- function areComponentTypesEqual(types1, types2) {
1496
- if (types1.length !== types2.length) return false;
1497
- const sorted1 = [...types1].sort((a, b) => a - b);
1498
- const sorted2 = [...types2].sort((a, b) => a - b);
1499
- return sorted1.every((v, i) => v === sorted2[i]);
1500
- }
1501
1618
 
1502
1619
  //#endregion
1503
1620
  //#region src/core/world-hooks.ts
@@ -1579,11 +1696,11 @@ function triggerMultiComponentHooks(ctx, entityId, addedComponents, removedCompo
1579
1696
  }
1580
1697
  function entityHasAllComponents(ctx, entityId, requiredComponents) {
1581
1698
  return requiredComponents.every((c) => {
1582
- if (isWildcardRelationId(c)) try {
1583
- const wildcardData = ctx.get(entityId, c);
1699
+ if (isWildcardRelationId(c)) {
1700
+ const wildcardResult = ctx.getOptional(entityId, c);
1701
+ if (!wildcardResult) return false;
1702
+ const wildcardData = wildcardResult.value;
1584
1703
  return Array.isArray(wildcardData) && wildcardData.length > 0;
1585
- } catch {
1586
- return false;
1587
1704
  }
1588
1705
  return ctx.has(entityId, c);
1589
1706
  });
@@ -1591,11 +1708,11 @@ function entityHasAllComponents(ctx, entityId, requiredComponents) {
1591
1708
  function entityHadAllComponentsBefore(ctx, entityId, requiredComponents, removedComponents) {
1592
1709
  return requiredComponents.every((c) => {
1593
1710
  if (anyComponentMatches(removedComponents, c)) return true;
1594
- if (isWildcardRelationId(c)) try {
1595
- const wildcardData = ctx.get(entityId, c);
1711
+ if (isWildcardRelationId(c)) {
1712
+ const wildcardResult = ctx.getOptional(entityId, c);
1713
+ if (!wildcardResult) return false;
1714
+ const wildcardData = wildcardResult.value;
1596
1715
  return Array.isArray(wildcardData) && wildcardData.length > 0;
1597
- } catch {
1598
- return false;
1599
1716
  }
1600
1717
  return ctx.has(entityId, c);
1601
1718
  });
@@ -1770,9 +1887,23 @@ var World = class {
1770
1887
  relationEntityIdsByTarget = /* @__PURE__ */ new Map();
1771
1888
  queries = [];
1772
1889
  queryCache = /* @__PURE__ */ new Map();
1773
- commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
1774
1890
  legacyHooks = /* @__PURE__ */ new Map();
1775
1891
  hooks = /* @__PURE__ */ new Set();
1892
+ commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
1893
+ _changeset = new ComponentChangeset();
1894
+ /** Cached command processor context to avoid per-entity object allocation */
1895
+ _commandCtx = {
1896
+ dontFragmentRelations: this.dontFragmentRelations,
1897
+ ensureArchetype: (ct) => this.ensureArchetype(ct)
1898
+ };
1899
+ /** Cached hooks context to avoid per-entity object allocation */
1900
+ _hooksCtx = {
1901
+ hooks: this.legacyHooks,
1902
+ multiHooks: this.hooks,
1903
+ has: (eid, ct) => this.has(eid, ct),
1904
+ get: (eid, ct) => this.get(eid, ct),
1905
+ getOptional: (eid, ct) => this.getOptional(eid, ct)
1906
+ };
1776
1907
  constructor(snapshot) {
1777
1908
  if (snapshot && typeof snapshot === "object") this.deserializeSnapshot(snapshot);
1778
1909
  }
@@ -1879,8 +2010,9 @@ var World = class {
1879
2010
  destroyEntityImmediate(entityId) {
1880
2011
  const queue = [entityId];
1881
2012
  const visited = /* @__PURE__ */ new Set();
1882
- while (queue.length > 0) {
1883
- const cur = queue.shift();
2013
+ let queueIndex = 0;
2014
+ while (queueIndex < queue.length) {
2015
+ const cur = queue[queueIndex++];
1884
2016
  if (visited.has(cur)) continue;
1885
2017
  visited.add(cur);
1886
2018
  const archetype = this.entityToArchetype.get(cur);
@@ -1915,40 +2047,77 @@ var World = class {
1915
2047
  if (this.isComponentEntityId(entityId)) return true;
1916
2048
  return this.entityToArchetype.has(entityId);
1917
2049
  }
1918
- set(entityId, componentTypeOrComponent, maybeComponent) {
2050
+ assertEntityExists(entityId, label) {
2051
+ if (!this.exists(entityId)) throw new Error(`${label} ${entityId} does not exist`);
2052
+ }
2053
+ assertComponentTypeValid(componentType) {
2054
+ if (getDetailedIdType(componentType).type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
2055
+ }
2056
+ assertSetComponentTypeValid(componentType) {
2057
+ const detailedType = getDetailedIdType(componentType);
2058
+ if (detailedType.type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
2059
+ if (detailedType.type === "wildcard-relation") throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
2060
+ }
2061
+ resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent) {
1919
2062
  if (maybeComponent === void 0 && componentTypeOrComponent !== void 0) {
1920
- const detailedType$1 = getDetailedIdType(entityId);
1921
- if (detailedType$1.type === "component" || detailedType$1.type === "component-relation") {
2063
+ const detailedType = getDetailedIdType(entityId);
2064
+ if (detailedType.type === "component" || detailedType.type === "component-relation") {
1922
2065
  const componentId = entityId;
1923
- const component$2 = componentTypeOrComponent;
1924
- if (!this.exists(componentId)) throw new Error(`Component entity ${componentId} does not exist`);
1925
- const detailedComponentType = getDetailedIdType(componentId);
1926
- if (detailedComponentType.type === "invalid") throw new Error(`Invalid component type: ${componentId}`);
1927
- if (detailedComponentType.type === "wildcard-relation") throw new Error(`Cannot directly add wildcard relation components: ${componentId}`);
1928
- this.commandBuffer.set(componentId, componentId, component$2);
1929
- return;
2066
+ this.assertEntityExists(componentId, "Component entity");
2067
+ this.assertSetComponentTypeValid(componentId);
2068
+ return {
2069
+ entityId: componentId,
2070
+ componentType: componentId,
2071
+ component: componentTypeOrComponent
2072
+ };
1930
2073
  }
1931
2074
  }
1932
- const entityIdArg = entityId;
2075
+ const targetEntityId = entityId;
1933
2076
  const componentType = componentTypeOrComponent;
1934
- const component$1 = maybeComponent;
1935
- if (!this.exists(entityIdArg)) throw new Error(`Entity ${entityIdArg} does not exist`);
1936
- const detailedType = getDetailedIdType(componentType);
1937
- if (detailedType.type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
1938
- if (detailedType.type === "wildcard-relation") throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
1939
- this.commandBuffer.set(entityIdArg, componentType, component$1);
2077
+ this.assertEntityExists(targetEntityId, "Entity");
2078
+ this.assertSetComponentTypeValid(componentType);
2079
+ return {
2080
+ entityId: targetEntityId,
2081
+ componentType,
2082
+ component: maybeComponent
2083
+ };
1940
2084
  }
1941
- remove(entityId, componentType) {
2085
+ resolveRemoveOperation(entityId, componentType) {
1942
2086
  if (componentType === void 0) {
1943
2087
  const componentId = entityId;
1944
- if (!this.exists(componentId)) throw new Error(`Component entity ${componentId} does not exist`);
1945
- this.commandBuffer.remove(componentId, componentId);
1946
- return;
2088
+ this.assertEntityExists(componentId, "Component entity");
2089
+ return {
2090
+ entityId: componentId,
2091
+ componentType: componentId
2092
+ };
1947
2093
  }
1948
- const entityIdArg = entityId;
1949
- if (!this.exists(entityIdArg)) throw new Error(`Entity ${entityIdArg} does not exist`);
1950
- if (getDetailedIdType(componentType).type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
1951
- this.commandBuffer.remove(entityIdArg, componentType);
2094
+ const targetEntityId = entityId;
2095
+ this.assertEntityExists(targetEntityId, "Entity");
2096
+ this.assertComponentTypeValid(componentType);
2097
+ return {
2098
+ entityId: targetEntityId,
2099
+ componentType
2100
+ };
2101
+ }
2102
+ getComponentEntityWildcardRelations(entityId, wildcardComponentType) {
2103
+ const componentId = getComponentIdFromRelationId(wildcardComponentType);
2104
+ const data = this.componentEntityComponents.get(entityId);
2105
+ if (componentId === void 0 || !data) return [];
2106
+ const relations = [];
2107
+ for (const [key, value] of data.entries()) {
2108
+ if (getComponentIdFromRelationId(key) !== componentId) continue;
2109
+ const detailed = getDetailedIdType(key);
2110
+ if (detailed.type === "entity-relation" || detailed.type === "component-relation") relations.push([detailed.targetId, value]);
2111
+ }
2112
+ return relations;
2113
+ }
2114
+ set(entityId, componentTypeOrComponent, maybeComponent) {
2115
+ const { entityId: targetEntityId, componentType, component: component$1 } = this.resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent);
2116
+ this.commandBuffer.set(targetEntityId, componentType, component$1);
2117
+ }
2118
+ remove(entityId, componentType) {
2119
+ const { entityId: targetEntityId, componentType: targetComponentType } = this.resolveRemoveOperation(entityId, componentType);
2120
+ this.commandBuffer.remove(targetEntityId, targetComponentType);
1952
2121
  }
1953
2122
  /**
1954
2123
  * Deletes an entity and all its components from the world.
@@ -1975,39 +2144,27 @@ var World = class {
1975
2144
  if (componentId === void 0) return false;
1976
2145
  const data = this.componentEntityComponents.get(entityId);
1977
2146
  if (!data) return false;
1978
- for (const key of data.keys()) if (getComponentIdFromRelationId(key) === componentId) return true;
1979
- return false;
2147
+ return hasWildcardRelation(data, componentId);
1980
2148
  }
1981
2149
  return this.componentEntityComponents.get(entityId)?.has(componentType) ?? false;
1982
2150
  }
1983
2151
  const archetype = this.entityToArchetype.get(entityId);
1984
2152
  if (!archetype) return false;
1985
- if (archetype.componentTypes.includes(componentType)) return true;
2153
+ if (archetype.componentTypeSet.has(componentType)) return true;
1986
2154
  if (isDontFragmentRelation(componentType)) return this.dontFragmentRelations.get(entityId)?.has(componentType) ?? false;
1987
2155
  return false;
1988
2156
  }
1989
2157
  get(entityId, componentType = entityId) {
1990
2158
  if (this.isComponentEntityId(entityId)) {
1991
- if (isWildcardRelationId(componentType)) {
1992
- const componentId = getComponentIdFromRelationId(componentType);
1993
- const data$1 = this.componentEntityComponents.get(entityId);
1994
- const relations = [];
1995
- if (componentId !== void 0 && data$1) {
1996
- for (const [key, value] of data$1.entries()) if (getComponentIdFromRelationId(key) === componentId) {
1997
- const detailed = getDetailedIdType(key);
1998
- if (detailed.type === "entity-relation" || detailed.type === "component-relation") relations.push([detailed.targetId, value]);
1999
- }
2000
- }
2001
- return relations;
2002
- }
2159
+ if (isWildcardRelationId(componentType)) return this.getComponentEntityWildcardRelations(entityId, componentType);
2003
2160
  const data = this.componentEntityComponents.get(entityId);
2004
2161
  if (!data || !data.has(componentType)) throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
2005
2162
  return data.get(componentType);
2006
2163
  }
2007
2164
  const archetype = this.entityToArchetype.get(entityId);
2008
2165
  if (!archetype) throw new Error(`Entity ${entityId} does not exist`);
2009
- if (componentType >= 0 || componentType % 2 ** 42 !== 0) {
2010
- const inArchetype = archetype.componentTypes.includes(componentType);
2166
+ if (componentType >= 0 || componentType % RELATION_SHIFT !== 0) {
2167
+ const inArchetype = archetype.componentTypeSet.has(componentType);
2011
2168
  const hasDontFragment = isDontFragmentRelation(componentType);
2012
2169
  if (!(inArchetype || hasDontFragment && this.dontFragmentRelations.get(entityId)?.has(componentType))) throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
2013
2170
  }
@@ -2016,15 +2173,7 @@ var World = class {
2016
2173
  getOptional(entityId, componentType = entityId) {
2017
2174
  if (this.isComponentEntityId(entityId)) {
2018
2175
  if (isWildcardRelationId(componentType)) {
2019
- const componentId = getComponentIdFromRelationId(componentType);
2020
- if (componentId === void 0) return void 0;
2021
- const data$1 = this.componentEntityComponents.get(entityId);
2022
- if (!data$1) return void 0;
2023
- const relations = [];
2024
- for (const [key, value] of data$1.entries()) if (getComponentIdFromRelationId(key) === componentId) {
2025
- const detailed = getDetailedIdType(key);
2026
- if (detailed.type === "entity-relation" || detailed.type === "component-relation") relations.push([detailed.targetId, value]);
2027
- }
2176
+ const relations = this.getComponentEntityWildcardRelations(entityId, componentType);
2028
2177
  if (relations.length === 0) return void 0;
2029
2178
  return { value: relations };
2030
2179
  }
@@ -2166,7 +2315,7 @@ var World = class {
2166
2315
  * });
2167
2316
  */
2168
2317
  createQuery(componentTypes, filter = {}) {
2169
- const sortedTypes = [...componentTypes].sort((a, b) => a - b);
2318
+ const sortedTypes = normalizeComponentTypes(componentTypes);
2170
2319
  const filterKey = serializeQueryFilter(filter);
2171
2320
  const key = `${this.createArchetypeSignature(sortedTypes)}${filterKey ? `|${filterKey}` : ""}`;
2172
2321
  const cached = this.queryCache.get(key);
@@ -2175,6 +2324,7 @@ var World = class {
2175
2324
  return cached.query;
2176
2325
  }
2177
2326
  const query = new Query(this, sortedTypes, filter);
2327
+ query._cacheKey = key;
2178
2328
  this.queryCache.set(key, {
2179
2329
  query,
2180
2330
  refCount: 1
@@ -2240,14 +2390,15 @@ var World = class {
2240
2390
  * world.releaseQuery(query); // Optional cleanup
2241
2391
  */
2242
2392
  releaseQuery(query) {
2243
- for (const [k, v] of this.queryCache.entries()) if (v.query === query) {
2244
- v.refCount--;
2245
- if (v.refCount <= 0) {
2246
- this.queryCache.delete(k);
2247
- this._unregisterQuery(query);
2248
- v.query._disposeInternal();
2249
- }
2250
- return;
2393
+ const key = query._cacheKey;
2394
+ if (!key) return;
2395
+ const cached = this.queryCache.get(key);
2396
+ if (!cached || cached.query !== query) return;
2397
+ cached.refCount--;
2398
+ if (cached.refCount <= 0) {
2399
+ this.queryCache.delete(key);
2400
+ this._unregisterQuery(query);
2401
+ cached.query._disposeInternal();
2251
2402
  }
2252
2403
  }
2253
2404
  /**
@@ -2279,8 +2430,21 @@ var World = class {
2279
2430
  getArchetypesWithComponents(componentTypes) {
2280
2431
  if (componentTypes.length === 0) return [...this.archetypes];
2281
2432
  if (componentTypes.length === 1) return this.archetypesByComponent.get(componentTypes[0]) || [];
2282
- const archetypeLists = componentTypes.map((type) => this.archetypesByComponent.get(type) || []);
2283
- return archetypeLists[0].filter((archetype) => archetypeLists.slice(1).every((list) => list.includes(archetype)));
2433
+ const archetypeLists = componentTypes.map((type) => this.archetypesByComponent.get(type) || []).sort((a, b) => a.length - b.length);
2434
+ const shortest = archetypeLists[0];
2435
+ if (shortest.length === 0) return [];
2436
+ if (archetypeLists.length === 2) {
2437
+ const second = archetypeLists[1];
2438
+ const secondSet = new Set(second);
2439
+ return shortest.filter((a) => secondSet.has(a));
2440
+ }
2441
+ let result = new Set(shortest);
2442
+ for (let i = 1; i < archetypeLists.length; i++) {
2443
+ const listSet = new Set(archetypeLists[i]);
2444
+ for (const item of result) if (!listSet.has(item)) result.delete(item);
2445
+ if (result.size === 0) return [];
2446
+ }
2447
+ return Array.from(result);
2284
2448
  }
2285
2449
  query(componentTypes, includeComponents) {
2286
2450
  const matchingArchetypes = this.getMatchingArchetypes(componentTypes);
@@ -2295,7 +2459,8 @@ var World = class {
2295
2459
  }
2296
2460
  }
2297
2461
  executeEntityCommands(entityId, commands) {
2298
- const changeset = new ComponentChangeset();
2462
+ const changeset = this._changeset;
2463
+ changeset.clear();
2299
2464
  if (this.isComponentEntityId(entityId)) {
2300
2465
  this.executeComponentEntityCommands(entityId, commands);
2301
2466
  return changeset;
@@ -2309,11 +2474,15 @@ var World = class {
2309
2474
  processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
2310
2475
  if (isExclusiveComponent(compId)) removeMatchingRelations(eid, arch, compId, changeset);
2311
2476
  });
2312
- const { removedComponents, newArchetype } = applyChangeset({
2313
- dontFragmentRelations: this.dontFragmentRelations,
2314
- ensureArchetype: (ct) => this.ensureArchetype(ct)
2315
- }, entityId, currentArchetype, changeset, this.entityToArchetype);
2316
- this.updateEntityReferences(entityId, changeset);
2477
+ const hasHooks = this.legacyHooks.size > 0 || this.hooks.size > 0;
2478
+ const hasEntityRefs = changeset.removes.size > 0 || changeset.adds.size > 0;
2479
+ if (!hasHooks) {
2480
+ applyChangesetNoHooks(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype);
2481
+ if (hasEntityRefs) this.updateEntityReferences(entityId, changeset);
2482
+ return changeset;
2483
+ }
2484
+ const { removedComponents, newArchetype } = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype);
2485
+ if (hasEntityRefs) this.updateEntityReferences(entityId, changeset);
2317
2486
  triggerLifecycleHooks(this.createHooksContext(), entityId, changeset.adds, removedComponents, currentArchetype, newArchetype);
2318
2487
  return changeset;
2319
2488
  }
@@ -2322,27 +2491,32 @@ var World = class {
2322
2491
  this.clearComponentEntityComponents(entityId);
2323
2492
  return;
2324
2493
  }
2325
- for (const command of commands) if (command.type === "set" && command.componentType) this.getComponentEntityComponents(entityId, true).set(command.componentType, command.component);
2326
- else if (command.type === "delete" && command.componentType) {
2494
+ const pendingSetValues = /* @__PURE__ */ new Map();
2495
+ for (const command of commands) if (command.type === "set" && command.componentType) {
2496
+ const merge = getComponentMerge(command.componentType);
2497
+ let nextValue = command.component;
2498
+ if (merge !== void 0 && pendingSetValues.has(command.componentType)) nextValue = merge(pendingSetValues.get(command.componentType), command.component);
2499
+ pendingSetValues.set(command.componentType, nextValue);
2500
+ this.getComponentEntityComponents(entityId, true).set(command.componentType, nextValue);
2501
+ } else if (command.type === "delete" && command.componentType) {
2327
2502
  const data = this.componentEntityComponents.get(entityId);
2328
- if (!data) continue;
2329
2503
  if (isWildcardRelationId(command.componentType)) {
2330
2504
  const componentId = getComponentIdFromRelationId(command.componentType);
2331
2505
  if (componentId !== void 0) {
2332
- for (const key of Array.from(data.keys())) if (getComponentIdFromRelationId(key) === componentId) data.delete(key);
2506
+ if (data) {
2507
+ for (const key of Array.from(data.keys())) if (getComponentIdFromRelationId(key) === componentId) data.delete(key);
2508
+ }
2509
+ for (const key of Array.from(pendingSetValues.keys())) if (getComponentIdFromRelationId(key) === componentId) pendingSetValues.delete(key);
2333
2510
  }
2334
- } else data.delete(command.componentType);
2335
- if (data.size === 0) this.clearComponentEntityComponents(entityId);
2511
+ } else {
2512
+ data?.delete(command.componentType);
2513
+ pendingSetValues.delete(command.componentType);
2514
+ }
2515
+ if (data?.size === 0) this.clearComponentEntityComponents(entityId);
2336
2516
  }
2337
2517
  }
2338
2518
  createHooksContext() {
2339
- return {
2340
- hooks: this.legacyHooks,
2341
- multiHooks: this.hooks,
2342
- has: (eid, ct) => this.has(eid, ct),
2343
- get: (eid, ct) => this.get(eid, ct),
2344
- getOptional: (eid, ct) => this.getOptional(eid, ct)
2345
- };
2519
+ return this._hooksCtx;
2346
2520
  }
2347
2521
  removeComponentImmediate(entityId, componentType, targetEntityId) {
2348
2522
  const sourceArchetype = this.entityToArchetype.get(entityId);
@@ -2351,10 +2525,7 @@ var World = class {
2351
2525
  changeset.delete(componentType);
2352
2526
  maybeRemoveWildcardMarker(entityId, sourceArchetype, componentType, getComponentIdFromRelationId(componentType), changeset);
2353
2527
  const removedComponent = sourceArchetype.get(entityId, componentType);
2354
- const { newArchetype } = applyChangeset({
2355
- dontFragmentRelations: this.dontFragmentRelations,
2356
- ensureArchetype: (ct) => this.ensureArchetype(ct)
2357
- }, entityId, sourceArchetype, changeset, this.entityToArchetype);
2528
+ const { newArchetype } = applyChangeset(this._commandCtx, entityId, sourceArchetype, changeset, this.entityToArchetype);
2358
2529
  untrackEntityReference(this.entityReferences, entityId, componentType, targetEntityId);
2359
2530
  triggerLifecycleHooks(this.createHooksContext(), entityId, /* @__PURE__ */ new Map(), new Map([[componentType, removedComponent]]), sourceArchetype, newArchetype);
2360
2531
  }
@@ -2362,16 +2533,16 @@ var World = class {
2362
2533
  for (const componentType of changeset.removes) if (isEntityRelation(componentType)) {
2363
2534
  const targetId = getTargetIdFromRelationId(componentType);
2364
2535
  untrackEntityReference(this.entityReferences, entityId, componentType, targetId);
2365
- } else if (componentType >= 1024) untrackEntityReference(this.entityReferences, entityId, componentType, componentType);
2536
+ } else if (componentType >= ENTITY_ID_START) untrackEntityReference(this.entityReferences, entityId, componentType, componentType);
2366
2537
  for (const [componentType] of changeset.adds) if (isEntityRelation(componentType)) {
2367
2538
  const targetId = getTargetIdFromRelationId(componentType);
2368
2539
  trackEntityReference(this.entityReferences, entityId, componentType, targetId);
2369
- } else if (componentType >= 1024) trackEntityReference(this.entityReferences, entityId, componentType, componentType);
2540
+ } else if (componentType >= ENTITY_ID_START) trackEntityReference(this.entityReferences, entityId, componentType, componentType);
2370
2541
  }
2371
2542
  ensureArchetype(componentTypes) {
2372
- const sortedTypes = filterRegularComponentTypes(componentTypes).sort((a, b) => a - b);
2543
+ const sortedTypes = normalizeComponentTypes(filterRegularComponentTypes(componentTypes));
2373
2544
  const hashKey = this.createArchetypeSignature(sortedTypes);
2374
- return getOrCreateWithSideEffect(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
2545
+ return getOrCompute(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
2375
2546
  }
2376
2547
  createNewArchetype(componentTypes) {
2377
2548
  const newArchetype = new Archetype(componentTypes, this.dontFragmentRelations);
@@ -2395,7 +2566,7 @@ var World = class {
2395
2566
  const componentId = getComponentIdFromRelationId(c);
2396
2567
  return componentId !== void 0 && archetype.hasRelationWithComponentId(componentId);
2397
2568
  }
2398
- return archetype.componentTypes.includes(c) || isDontFragmentRelation(c);
2569
+ return archetype.componentTypeSet.has(c) || isDontFragmentRelation(c);
2399
2570
  });
2400
2571
  }
2401
2572
  archetypeReferencesEntity(archetype, entityId) {