@codehz/ecs 0.8.0 → 0.8.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.
@@ -561,15 +561,28 @@ interface LifecycleHookEntry {
561
561
  //#endregion
562
562
  //#region src/archetype/store.d.ts
563
563
  /**
564
- * Minimal interface for storing dontFragment relation data keyed by entity ID.
564
+ * Interface for storing dontFragment relation data.
565
565
  *
566
- * Using an interface here decouples `Archetype` (and `world-commands.ts`) from
567
- * the concrete `Map` used by `World`, making archetypes independently testable.
566
+ * Storage is now primarily keyed by relation ComponentId (the "kind" of relation)
567
+ * rather than by entity. This provides O(1) or near-O(1) answers for the hot
568
+ * wildcard-related paths (hasRelationWithComponentId, wildcard materialization
569
+ * during iteration, hook matching, etc.).
570
+ *
571
+ * A lightweight reverse index (entity -> Set of base ComponentIds) is maintained
572
+ * to efficiently support the infrequent "get all dontFragment data for this entity"
573
+ * operations (removeEntity, dump, getEntity, serialization).
574
+ *
575
+ * The interface no longer leaks internal Map structures. Callers work with
576
+ * semantic operations only.
568
577
  */
569
578
  interface DontFragmentStore {
570
- get(entityId: EntityId): Map<EntityId<any>, any> | undefined;
571
- set(entityId: EntityId, data: Map<EntityId<any>, any>): void;
572
- delete(entityId: EntityId): void;
579
+ getValue(entityId: EntityId, relationType: EntityId<any>): any | undefined;
580
+ setValue(entityId: EntityId, relationType: EntityId<any>, data: any): void;
581
+ deleteValue(entityId: EntityId, relationType: EntityId<any>): boolean;
582
+ hasAnyForComponent(componentId: EntityId<any>): boolean;
583
+ getRelationsForComponent(entityId: EntityId, componentId: EntityId<any>): [target: EntityId, data: any][];
584
+ getAllForEntity(entityId: EntityId): Array<[relationType: EntityId<any>, data: any]>;
585
+ deleteEntity(entityId: EntityId): void;
573
586
  }
574
587
  //#endregion
575
588
  //#region src/archetype/archetype.d.ts
@@ -601,9 +614,9 @@ declare class Archetype {
601
614
  */
602
615
  private entityToIndex;
603
616
  /**
604
- * DontFragmentStore for relation data keyed by entity ID.
605
- * This allows entities with different relation targets to share the same archetype
606
- * without migration overhead when entities change archetypes.
617
+ * DontFragmentStore (keyed primarily by relation ComponentId).
618
+ * Uses optimized RelationEntry (single/multi) for the common exclusive case.
619
+ * See store.ts for implementation details.
607
620
  */
608
621
  private dontFragmentRelations;
609
622
  /**
@@ -626,6 +639,12 @@ declare class Archetype {
626
639
  addEntity(entityId: EntityId, componentData: Map<EntityId<any>, any>): void;
627
640
  private addDontFragmentRelations;
628
641
  getEntity(entityId: EntityId): Map<EntityId<any>, any> | undefined;
642
+ /**
643
+ * Returns all dontFragment relations for the given entity as an array of tuples.
644
+ * This is a compatibility adapter during the store refactor.
645
+ *
646
+ * Prefer the new DontFragmentStore methods when possible.
647
+ */
629
648
  getEntityDontFragmentRelations(entityId: EntityId): Map<EntityId<any>, any> | undefined;
630
649
  dump(): Array<{
631
650
  entity: EntityId;
package/dist/world.mjs CHANGED
@@ -695,17 +695,6 @@ function matchesRelationComponentId(componentType, componentId) {
695
695
  return isRelationType(detailedType) && detailedType.componentId === componentId;
696
696
  }
697
697
  /**
698
- * Find all relations in dontFragment data that match a component ID
699
- */
700
- function findMatchingDontFragmentRelations(dontFragmentData, componentId, relations = []) {
701
- if (!dontFragmentData) return relations;
702
- for (const [relType, data] of dontFragmentData) {
703
- const relDetailed = getDetailedIdType(relType);
704
- if (isRelationType(relDetailed) && relDetailed.componentId === componentId) relations.push([relDetailed.targetId, data]);
705
- }
706
- return relations;
707
- }
708
- /**
709
698
  * Build cache key for component types
710
699
  */
711
700
  function buildCacheKey(componentTypes) {
@@ -719,9 +708,10 @@ function getWildcardRelationDataSource(componentTypes, componentId, optional) {
719
708
  return optional ? matchingRelations.length > 0 ? matchingRelations : void 0 : matchingRelations;
720
709
  }
721
710
  /**
722
- * Build wildcard relation value from matching relations
711
+ * Build wildcard relation value from matching relations.
712
+ * Now receives the DontFragmentStore directly for efficient per-component lookups.
723
713
  */
724
- function buildWildcardRelationValue(wildcardRelationType, matchingRelations, getDataAtIndex, dontFragmentData, entityId, optional) {
714
+ function buildWildcardRelationValue(wildcardRelationType, matchingRelations, getDataAtIndex, dontFragmentStore, entityId, optional) {
725
715
  const relations = [];
726
716
  const targetComponentId = getComponentIdFromRelationId(wildcardRelationType);
727
717
  for (const relType of matchingRelations || []) {
@@ -729,7 +719,10 @@ function buildWildcardRelationValue(wildcardRelationType, matchingRelations, get
729
719
  const targetId = getTargetIdFromRelationId(relType);
730
720
  relations.push([targetId, data === MISSING_COMPONENT ? void 0 : data]);
731
721
  }
732
- if (targetComponentId !== void 0) findMatchingDontFragmentRelations(dontFragmentData, targetComponentId, relations);
722
+ if (targetComponentId !== void 0) {
723
+ const dfMatches = dontFragmentStore.getRelationsForComponent(entityId, targetComponentId);
724
+ for (const m of dfMatches) relations.push(m);
725
+ }
733
726
  if (relations.length === 0) {
734
727
  if (!optional) {
735
728
  const componentId = getComponentIdFromRelationId(wildcardRelationType);
@@ -757,7 +750,7 @@ function buildRegularComponentValue(dataSource, entityIndex, optional) {
757
750
  function buildSingleComponent(compType, dataSource, entityIndex, entityId, getComponentData, dontFragmentRelations) {
758
751
  const optional = isOptionalEntityId(compType);
759
752
  const actualType = optional ? compType.optional : compType;
760
- if (getIdType(actualType) === "wildcard-relation") return buildWildcardRelationValue(actualType, dataSource, (relType) => getComponentData(relType)[entityIndex], dontFragmentRelations.get(entityId), entityId, optional);
753
+ if (getIdType(actualType) === "wildcard-relation") return buildWildcardRelationValue(actualType, dataSource, (relType) => getComponentData(relType)[entityIndex], dontFragmentRelations, entityId, optional);
761
754
  else return buildRegularComponentValue(dataSource, entityIndex, optional);
762
755
  }
763
756
  //#endregion
@@ -794,9 +787,9 @@ var Archetype = class {
794
787
  */
795
788
  entityToIndex = /* @__PURE__ */ new Map();
796
789
  /**
797
- * DontFragmentStore for relation data keyed by entity ID.
798
- * This allows entities with different relation targets to share the same archetype
799
- * without migration overhead when entities change archetypes.
790
+ * DontFragmentStore (keyed primarily by relation ComponentId).
791
+ * Uses optimized RelationEntry (single/multi) for the common exclusive case.
792
+ * See store.ts for implementation details.
800
793
  */
801
794
  dontFragmentRelations;
802
795
  /**
@@ -839,13 +832,11 @@ var Archetype = class {
839
832
  this.addDontFragmentRelations(entityId, componentData);
840
833
  }
841
834
  addDontFragmentRelations(entityId, componentData) {
842
- const dontFragmentData = /* @__PURE__ */ new Map();
843
835
  for (const [componentType, data] of componentData) {
844
836
  if (this.componentTypeSet.has(componentType)) continue;
845
837
  const detailedType = getDetailedIdType(componentType);
846
- if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId)) dontFragmentData.set(componentType, data);
838
+ if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId)) this.dontFragmentRelations.setValue(entityId, componentType, data);
847
839
  }
848
- if (dontFragmentData.size > 0) this.dontFragmentRelations.set(entityId, dontFragmentData);
849
840
  }
850
841
  getEntity(entityId) {
851
842
  const index = this.entityToIndex.get(entityId);
@@ -855,12 +846,22 @@ var Archetype = class {
855
846
  const data = this.getComponentData(componentType)[index];
856
847
  entityData.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
857
848
  }
858
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
859
- if (dontFragmentData) for (const [componentType, data] of dontFragmentData) entityData.set(componentType, data);
849
+ const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entityId);
850
+ for (const [componentType, data] of dontFragmentTuples) entityData.set(componentType, data);
860
851
  return entityData;
861
852
  }
853
+ /**
854
+ * Returns all dontFragment relations for the given entity as an array of tuples.
855
+ * This is a compatibility adapter during the store refactor.
856
+ *
857
+ * Prefer the new DontFragmentStore methods when possible.
858
+ */
862
859
  getEntityDontFragmentRelations(entityId) {
863
- return this.dontFragmentRelations.get(entityId);
860
+ const tuples = this.dontFragmentRelations.getAllForEntity(entityId);
861
+ if (tuples.length === 0) return void 0;
862
+ const map = /* @__PURE__ */ new Map();
863
+ for (const [relType, data] of tuples) map.set(relType, data);
864
+ return map;
864
865
  }
865
866
  dump() {
866
867
  return this.entities.map((entity, i) => {
@@ -869,8 +870,8 @@ var Archetype = class {
869
870
  const data = this.getComponentData(componentType)[i];
870
871
  components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
871
872
  }
872
- const dontFragmentData = this.dontFragmentRelations.get(entity);
873
- if (dontFragmentData) for (const [componentType, data] of dontFragmentData) components.set(componentType, data);
873
+ const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entity);
874
+ for (const [componentType, data] of dontFragmentTuples) components.set(componentType, data);
874
875
  return {
875
876
  entity,
876
877
  components
@@ -882,11 +883,9 @@ var Archetype = class {
882
883
  if (index === void 0) return void 0;
883
884
  const removedData = /* @__PURE__ */ new Map();
884
885
  for (const componentType of this.componentTypes) removedData.set(componentType, this.getComponentData(componentType)[index]);
885
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
886
- if (dontFragmentData) {
887
- for (const [componentType, data] of dontFragmentData) removedData.set(componentType, data);
888
- this.dontFragmentRelations.delete(entityId);
889
- }
886
+ const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entityId);
887
+ for (const [componentType, data] of dontFragmentTuples) removedData.set(componentType, data);
888
+ this.dontFragmentRelations.deleteEntity(entityId);
890
889
  this.entityToIndex.delete(entityId);
891
890
  const lastIndex = this.entities.length - 1;
892
891
  if (index !== lastIndex) {
@@ -924,7 +923,10 @@ var Archetype = class {
924
923
  }
925
924
  }
926
925
  }
927
- if (componentId !== void 0) findMatchingDontFragmentRelations(this.dontFragmentRelations.get(entityId), componentId, relations);
926
+ if (componentId !== void 0) {
927
+ const matches = this.dontFragmentRelations.getRelationsForComponent(entityId, componentId);
928
+ for (const m of matches) relations.push(m);
929
+ }
928
930
  return relations;
929
931
  }
930
932
  getRegularComponent(entityId, index, componentType) {
@@ -933,8 +935,7 @@ var Archetype = class {
933
935
  if (data === MISSING_COMPONENT) throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
934
936
  return data;
935
937
  }
936
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
937
- if (dontFragmentData?.has(componentType)) return dontFragmentData.get(componentType);
938
+ if (this.dontFragmentRelations.getValue(entityId, componentType) !== void 0 || this.dontFragmentRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) return this.dontFragmentRelations.getValue(entityId, componentType);
938
939
  throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
939
940
  }
940
941
  getOptional(entityId, componentType) {
@@ -945,8 +946,9 @@ var Archetype = class {
945
946
  if (data === MISSING_COMPONENT) return void 0;
946
947
  return { value: data };
947
948
  }
948
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
949
- if (dontFragmentData?.has(componentType)) return { value: dontFragmentData.get(componentType) };
949
+ const value = this.dontFragmentRelations.getValue(entityId, componentType);
950
+ if (value !== void 0) return { value };
951
+ if (this.dontFragmentRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) return { value: this.dontFragmentRelations.getValue(entityId, componentType) };
950
952
  }
951
953
  set(entityId, componentType, data) {
952
954
  const index = this.entityToIndex.get(entityId);
@@ -957,12 +959,7 @@ var Archetype = class {
957
959
  }
958
960
  const detailedType = getDetailedIdType(componentType);
959
961
  if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId)) {
960
- let dontFragmentData = this.dontFragmentRelations.get(entityId);
961
- if (!dontFragmentData) {
962
- dontFragmentData = /* @__PURE__ */ new Map();
963
- this.dontFragmentRelations.set(entityId, dontFragmentData);
964
- }
965
- dontFragmentData.set(componentType, data);
962
+ this.dontFragmentRelations.setValue(entityId, componentType, data);
966
963
  return;
967
964
  }
968
965
  throw new Error(`Component type ${componentType} is not in this archetype`);
@@ -1026,12 +1023,15 @@ var Archetype = class {
1026
1023
  }
1027
1024
  forEach(callback) {
1028
1025
  for (let i = 0; i < this.entities.length; i++) {
1026
+ const entity = this.entities[i];
1029
1027
  const components = /* @__PURE__ */ new Map();
1030
1028
  for (const componentType of this.componentTypes) {
1031
1029
  const data = this.getComponentData(componentType)[i];
1032
1030
  components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
1033
1031
  }
1034
- callback(this.entities[i], components);
1032
+ const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entity);
1033
+ for (const [componentType, data] of dontFragmentTuples) components.set(componentType, data);
1034
+ callback(entity, components);
1035
1035
  }
1036
1036
  }
1037
1037
  hasRelationWithComponentId(componentId) {
@@ -1039,32 +1039,165 @@ var Archetype = class {
1039
1039
  const detailedType = getDetailedIdType(componentType);
1040
1040
  if (isRelationType(detailedType) && detailedType.componentId === componentId) return true;
1041
1041
  }
1042
- for (const entityId of this.entities) {
1043
- const entityDontFragmentRelations = this.dontFragmentRelations.get(entityId);
1044
- if (entityDontFragmentRelations) for (const relationType of entityDontFragmentRelations.keys()) {
1045
- const detailedType = getDetailedIdType(relationType);
1046
- if (isRelationType(detailedType) && detailedType.componentId === componentId) return true;
1047
- }
1048
- }
1042
+ for (const entityId of this.entities) if (this.dontFragmentRelations.getRelationsForComponent(entityId, componentId).length > 0) return true;
1049
1043
  return false;
1050
1044
  }
1051
1045
  };
1052
1046
  //#endregion
1053
1047
  //#region src/archetype/store.ts
1054
1048
  /**
1055
- * Default implementation backed by a plain `Map`.
1056
- * Created once by `World` and shared with every `Archetype`.
1049
+ * Production implementation of DontFragmentStore.
1050
+ *
1051
+ * Internal layout (optimized):
1052
+ * - byComponent: baseComponentId → (entityId → RelationEntry)
1053
+ * RelationEntry uses a single-value form for the common exclusive case (1 target),
1054
+ * avoiding Map allocation entirely for the vast majority of dontFragment usage.
1055
+ * - entityIndex: entityId → Set<baseComponentId>
1056
+ * Lightweight reverse index.
1057
1057
  */
1058
1058
  var DontFragmentStoreImpl = class {
1059
- data = /* @__PURE__ */ new Map();
1060
- get(entityId) {
1061
- return this.data.get(entityId);
1059
+ /**
1060
+ * Primary storage, keyed by the base relation component ID.
1061
+ */
1062
+ byComponent = /* @__PURE__ */ new Map();
1063
+ /**
1064
+ * Reverse index: which base component kinds an entity participates in.
1065
+ * Used only by the infrequent getAllForEntity / deleteEntity paths.
1066
+ */
1067
+ entityIndex = /* @__PURE__ */ new Map();
1068
+ getValue(entityId, relationType) {
1069
+ const componentId = getComponentIdFromRelationId(relationType);
1070
+ if (componentId === void 0) return void 0;
1071
+ const entities = this.byComponent.get(componentId);
1072
+ if (!entities) return void 0;
1073
+ const entry = entities.get(entityId);
1074
+ if (!entry) return void 0;
1075
+ const targetId = getTargetIdFromRelationId(relationType);
1076
+ if (entry.type === "single") return entry.target === targetId ? entry.data : void 0;
1077
+ else {
1078
+ const item = entry.targets.get(targetId);
1079
+ return item ? item.data : void 0;
1080
+ }
1081
+ }
1082
+ setValue(entityId, relationType, data) {
1083
+ const componentId = getComponentIdFromRelationId(relationType);
1084
+ if (componentId === void 0) throw new Error("setValue called with a non-relation type on DontFragmentStore");
1085
+ let entities = this.byComponent.get(componentId);
1086
+ if (!entities) {
1087
+ entities = /* @__PURE__ */ new Map();
1088
+ this.byComponent.set(componentId, entities);
1089
+ }
1090
+ const targetId = getTargetIdFromRelationId(relationType);
1091
+ let entry = entities.get(entityId);
1092
+ if (!entry) {
1093
+ entry = {
1094
+ type: "single",
1095
+ relationType,
1096
+ target: targetId,
1097
+ data
1098
+ };
1099
+ entities.set(entityId, entry);
1100
+ } else if (entry.type === "single") if (entry.target === targetId) {
1101
+ entry.data = data;
1102
+ entry.relationType = relationType;
1103
+ } else {
1104
+ const targets = /* @__PURE__ */ new Map();
1105
+ targets.set(entry.target, {
1106
+ relationType: entry.relationType,
1107
+ data: entry.data
1108
+ });
1109
+ targets.set(targetId, {
1110
+ relationType,
1111
+ data
1112
+ });
1113
+ entities.set(entityId, {
1114
+ type: "multi",
1115
+ targets
1116
+ });
1117
+ }
1118
+ else entry.targets.set(targetId, {
1119
+ relationType,
1120
+ data
1121
+ });
1122
+ let components = this.entityIndex.get(entityId);
1123
+ if (!components) {
1124
+ components = /* @__PURE__ */ new Set();
1125
+ this.entityIndex.set(entityId, components);
1126
+ }
1127
+ components.add(componentId);
1128
+ }
1129
+ deleteValue(entityId, relationType) {
1130
+ const componentId = getComponentIdFromRelationId(relationType);
1131
+ if (componentId === void 0) return false;
1132
+ const entities = this.byComponent.get(componentId);
1133
+ if (!entities) return false;
1134
+ const entry = entities.get(entityId);
1135
+ if (!entry) return false;
1136
+ const targetId = getTargetIdFromRelationId(relationType);
1137
+ let existed = false;
1138
+ if (entry.type === "single") {
1139
+ if (entry.target === targetId) {
1140
+ existed = true;
1141
+ entities.delete(entityId);
1142
+ }
1143
+ } else {
1144
+ existed = entry.targets.delete(targetId);
1145
+ if (entry.targets.size === 0) entities.delete(entityId);
1146
+ else if (entry.targets.size === 1) {
1147
+ const [first] = entry.targets.entries();
1148
+ const [t, item] = first;
1149
+ entities.set(entityId, {
1150
+ type: "single",
1151
+ relationType: item.relationType,
1152
+ target: t,
1153
+ data: item.data
1154
+ });
1155
+ }
1156
+ }
1157
+ if (!entities.has(entityId) && entities.size === 0) this.byComponent.delete(componentId);
1158
+ const components = this.entityIndex.get(entityId);
1159
+ if (components && !entities.has(entityId)) {
1160
+ components.delete(componentId);
1161
+ if (components.size === 0) this.entityIndex.delete(entityId);
1162
+ }
1163
+ return existed;
1062
1164
  }
1063
- set(entityId, data) {
1064
- this.data.set(entityId, data);
1165
+ hasAnyForComponent(componentId) {
1166
+ const entities = this.byComponent.get(componentId);
1167
+ return entities !== void 0 && entities.size > 0;
1065
1168
  }
1066
- delete(entityId) {
1067
- this.data.delete(entityId);
1169
+ getRelationsForComponent(entityId, componentId) {
1170
+ const result = [];
1171
+ const entities = this.byComponent.get(componentId);
1172
+ if (!entities) return result;
1173
+ const entry = entities.get(entityId);
1174
+ if (!entry) return result;
1175
+ if (entry.type === "single") result.push([entry.target, entry.data]);
1176
+ else for (const [target, item] of entry.targets) result.push([target, item.data]);
1177
+ return result;
1178
+ }
1179
+ getAllForEntity(entityId) {
1180
+ const components = this.entityIndex.get(entityId);
1181
+ if (!components || components.size === 0) return [];
1182
+ const result = [];
1183
+ for (const componentId of components) {
1184
+ const entry = this.byComponent.get(componentId)?.get(entityId);
1185
+ if (entry) if (entry.type === "single") result.push([entry.relationType, entry.data]);
1186
+ else for (const item of entry.targets.values()) result.push([item.relationType, item.data]);
1187
+ }
1188
+ return result;
1189
+ }
1190
+ deleteEntity(entityId) {
1191
+ const components = this.entityIndex.get(entityId);
1192
+ if (!components) return;
1193
+ for (const componentId of components) {
1194
+ const entities = this.byComponent.get(componentId);
1195
+ if (entities) {
1196
+ entities.delete(entityId);
1197
+ if (entities.size === 0) this.byComponent.delete(componentId);
1198
+ }
1199
+ }
1200
+ this.entityIndex.delete(entityId);
1068
1201
  }
1069
1202
  };
1070
1203
  //#endregion
@@ -1789,6 +1922,10 @@ function maybeRemoveWildcardMarker(entityId, archetype, removedComponentType, co
1789
1922
  if (changeset.removes.has(otherComponentType)) continue;
1790
1923
  if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
1791
1924
  }
1925
+ for (const addedType of changeset.adds.keys()) {
1926
+ if (addedType === removedComponentType) continue;
1927
+ if (getComponentIdFromRelationId(addedType) === componentId) return;
1928
+ }
1792
1929
  changeset.delete(wildcardMarker);
1793
1930
  }
1794
1931
  function hasEntityComponent(archetype, entityId, componentType) {
@@ -1835,40 +1972,20 @@ function applyChangeset(ctx, entityId, currentArchetype, changeset, entityToArch
1835
1972
  }
1836
1973
  /**
1837
1974
  * No-hooks variant of applyDontFragmentChanges that skips tracking removed component data.
1975
+ *
1976
+ * Rewritten for the new DontFragmentStore interface (ComponentId-primary storage).
1838
1977
  */
1839
1978
  function applyDontFragmentChanges(dontFragmentRelations, entityId, changeset, removedComponents) {
1840
- let entityRelations = dontFragmentRelations.get(entityId);
1841
1979
  for (const componentType of changeset.removes) if (isDontFragmentRelation(componentType)) {
1842
- if (entityRelations) {
1843
- const removedValue = entityRelations.get(componentType);
1844
- if (removedValue !== void 0 || entityRelations.has(componentType)) {
1845
- removedComponents.set(componentType, removedValue);
1846
- entityRelations.delete(componentType);
1847
- }
1848
- }
1849
- }
1850
- for (const [componentType, component] of changeset.adds) if (isDontFragmentRelation(componentType)) {
1851
- if (!entityRelations) {
1852
- entityRelations = /* @__PURE__ */ new Map();
1853
- dontFragmentRelations.set(entityId, entityRelations);
1854
- }
1855
- entityRelations.set(componentType, component);
1980
+ const removedValue = dontFragmentRelations.getValue(entityId, componentType);
1981
+ if (removedValue !== void 0 || dontFragmentRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) removedComponents.set(componentType, removedValue);
1982
+ dontFragmentRelations.deleteValue(entityId, componentType);
1856
1983
  }
1857
- if (entityRelations && entityRelations.size === 0) dontFragmentRelations.delete(entityId);
1984
+ for (const [componentType, component] of changeset.adds) if (isDontFragmentRelation(componentType)) dontFragmentRelations.setValue(entityId, componentType, component);
1858
1985
  }
1859
1986
  function applyDontFragmentChangesNoHooks(dontFragmentRelations, entityId, changeset) {
1860
- let entityRelations = dontFragmentRelations.get(entityId);
1861
- for (const componentType of changeset.removes) if (isDontFragmentRelation(componentType)) {
1862
- if (entityRelations) entityRelations.delete(componentType);
1863
- }
1864
- for (const [componentType, component] of changeset.adds) if (isDontFragmentRelation(componentType)) {
1865
- if (!entityRelations) {
1866
- entityRelations = /* @__PURE__ */ new Map();
1867
- dontFragmentRelations.set(entityId, entityRelations);
1868
- }
1869
- entityRelations.set(componentType, component);
1870
- }
1871
- if (entityRelations && entityRelations.size === 0) dontFragmentRelations.delete(entityId);
1987
+ for (const componentType of changeset.removes) if (isDontFragmentRelation(componentType)) dontFragmentRelations.deleteValue(entityId, componentType);
1988
+ for (const [componentType, component] of changeset.adds) if (isDontFragmentRelation(componentType)) dontFragmentRelations.setValue(entityId, componentType, component);
1872
1989
  }
1873
1990
  function filterRegularComponentTypes(componentTypes) {
1874
1991
  const regularTypes = [];
@@ -2131,10 +2248,35 @@ var MultiMap = class {
2131
2248
  };
2132
2249
  //#endregion
2133
2250
  //#region src/world/references.ts
2251
+ /**
2252
+ * Record that `sourceEntityId` holds a reference to `targetEntityId` via the given component/relation.
2253
+ *
2254
+ * Called when an entity-valued component or an entity-relation is added to an entity.
2255
+ *
2256
+ * @param entityReferences - The shared reverse index map
2257
+ * @param sourceEntityId - The entity that contains the reference
2258
+ * @param componentType - The component type or encoded relation ID used for the reference
2259
+ * @param targetEntityId - The entity being referenced
2260
+ *
2261
+ * @internal
2262
+ */
2134
2263
  function trackEntityReference(entityReferences, sourceEntityId, componentType, targetEntityId) {
2135
2264
  if (!entityReferences.has(targetEntityId)) entityReferences.set(targetEntityId, new MultiMap());
2136
2265
  entityReferences.get(targetEntityId).add(sourceEntityId, componentType);
2137
2266
  }
2267
+ /**
2268
+ * Remove the record that `sourceEntityId` references `targetEntityId` via the given component/relation.
2269
+ *
2270
+ * Called when an entity-valued component or entity-relation is removed (or during deletion).
2271
+ * Automatically prunes empty target entries from the map.
2272
+ *
2273
+ * @param entityReferences - The shared reverse index map
2274
+ * @param sourceEntityId - The entity that no longer holds the reference
2275
+ * @param componentType - The component type or encoded relation ID that was used
2276
+ * @param targetEntityId - The previously referenced entity
2277
+ *
2278
+ * @internal
2279
+ */
2138
2280
  function untrackEntityReference(entityReferences, sourceEntityId, componentType, targetEntityId) {
2139
2281
  const references = entityReferences.get(targetEntityId);
2140
2282
  if (references) {
@@ -2142,6 +2284,19 @@ function untrackEntityReference(entityReferences, sourceEntityId, componentType,
2142
2284
  if (references.keyCount === 0) entityReferences.delete(targetEntityId);
2143
2285
  }
2144
2286
  }
2287
+ /**
2288
+ * Iterate over all (sourceEntityId, componentOrRelationId) pairs that currently reference the given target.
2289
+ *
2290
+ * Returns an empty iterable when the target has no incoming references.
2291
+ * The returned iterable yields `[source, componentType]` pairs suitable for cleanup decisions
2292
+ * (e.g. whether to cascade-delete the source or just remove the specific component/relation).
2293
+ *
2294
+ * @param entityReferences - The shared reverse index map
2295
+ * @param targetEntityId - The entity whose referrers we want to inspect
2296
+ * @returns Iterable of [sourceEntityId, componentOrRelationId]
2297
+ *
2298
+ * @internal
2299
+ */
2145
2300
  function getEntityReferences(entityReferences, targetEntityId) {
2146
2301
  return entityReferences.get(targetEntityId) ?? new MultiMap();
2147
2302
  }
@@ -2523,7 +2678,10 @@ var World = class {
2523
2678
  const archetype = this.entityToArchetype.get(entityId);
2524
2679
  if (!archetype) return false;
2525
2680
  if (archetype.componentTypeSet.has(componentType)) return true;
2526
- if (isDontFragmentRelation(componentType)) return this.dontFragmentStore.get(entityId)?.has(componentType) ?? false;
2681
+ if (isDontFragmentRelation(componentType)) {
2682
+ if (this.dontFragmentStore.getValue(entityId, componentType) !== void 0) return true;
2683
+ return this.dontFragmentStore.getAllForEntity(entityId).some(([t]) => t === componentType);
2684
+ }
2527
2685
  return false;
2528
2686
  }
2529
2687
  get(entityId, componentType = entityId) {
@@ -2536,7 +2694,7 @@ var World = class {
2536
2694
  if (componentType >= 0 || componentType % RELATION_SHIFT !== 0) {
2537
2695
  const inArchetype = archetype.componentTypeSet.has(componentType);
2538
2696
  const hasDontFragment = isDontFragmentRelation(componentType);
2539
- if (!(inArchetype || hasDontFragment && this.dontFragmentStore.get(entityId)?.has(componentType))) throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
2697
+ if (!(inArchetype || hasDontFragment && (this.dontFragmentStore.getValue(entityId, componentType) !== void 0 || this.dontFragmentStore.getAllForEntity(entityId).some(([t]) => t === componentType)))) throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
2540
2698
  }
2541
2699
  return archetype.get(entityId, componentType);
2542
2700
  }