@codehz/ecs 0.8.1 → 0.9.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.
Files changed (50) hide show
  1. package/README.en.md +26 -3
  2. package/README.md +28 -3
  3. package/dist/builder.d.mts +296 -46
  4. package/dist/index.d.mts +2 -2
  5. package/dist/index.mjs +2 -2
  6. package/dist/testing.d.mts +1 -1
  7. package/dist/testing.mjs +1 -1
  8. package/dist/world.mjs +452 -179
  9. package/dist/world.mjs.map +1 -1
  10. package/examples/debug-observability.ts +92 -0
  11. package/examples/inventory-system-relations.ts +1 -1
  12. package/examples/parent-child-hierarchy.ts +18 -38
  13. package/package.json +1 -1
  14. package/skills/ecs/SKILL.md +9 -4
  15. package/src/__tests__/component/singleton.test.ts +40 -1
  16. package/src/__tests__/core/archetype.test.ts +155 -13
  17. package/src/__tests__/core/bitset.test.ts +12 -0
  18. package/src/__tests__/entity/entity.test.ts +33 -0
  19. package/src/__tests__/entity/id-system.test.ts +40 -0
  20. package/src/__tests__/perf/comprehensive.perf.test.ts +6 -9
  21. package/src/__tests__/perf/serialization.perf.test.ts +242 -0
  22. package/src/__tests__/perf/{dontfragment-wildcard.perf.test.ts → sparse-wildcard.perf.test.ts} +13 -16
  23. package/src/__tests__/query/caching.test.ts +62 -0
  24. package/src/__tests__/query/filter.test.ts +16 -22
  25. package/src/__tests__/query/perf.test.ts +3 -5
  26. package/src/__tests__/relations/hierarchy.test.ts +208 -0
  27. package/src/__tests__/relations/{dont-fragment → sparse}/basic.test.ts +64 -69
  28. package/src/__tests__/relations/{dont-fragment → sparse}/query-notification.test.ts +17 -9
  29. package/src/__tests__/serialization/bounds.test.ts +134 -1
  30. package/src/__tests__/world/commands.test.ts +337 -0
  31. package/src/__tests__/world/debug-stats.test.ts +206 -0
  32. package/src/__tests__/world/multi-component-hooks.test.ts +44 -0
  33. package/src/__tests__/world/serialize.test.ts +17 -0
  34. package/src/__tests__/world/wildcard-relation-hooks.test.ts +127 -0
  35. package/src/archetype/archetype.ts +96 -46
  36. package/src/archetype/helpers.ts +7 -29
  37. package/src/archetype/store.ts +35 -20
  38. package/src/commands/buffer.ts +5 -2
  39. package/src/commands/changeset.ts +0 -31
  40. package/src/component/registry.ts +64 -63
  41. package/src/entity/index.ts +6 -3
  42. package/src/index.ts +13 -0
  43. package/src/query/filter.ts +4 -10
  44. package/src/query/query.ts +12 -12
  45. package/src/storage/serialization.ts +29 -2
  46. package/src/types/index.ts +71 -0
  47. package/src/world/commands.ts +44 -56
  48. package/src/world/hooks.ts +8 -0
  49. package/src/world/serialization.ts +32 -18
  50. package/src/world/world.ts +387 -20
package/dist/world.mjs CHANGED
@@ -366,7 +366,7 @@ const ComponentIdForNames = /* @__PURE__ */ new Map();
366
366
  const componentNames = new Array(COMPONENT_ID_MAX + 1);
367
367
  const exclusiveFlags = new BitSet(COMPONENT_ID_MAX + 1);
368
368
  const cascadeDeleteFlags = new BitSet(COMPONENT_ID_MAX + 1);
369
- const dontFragmentFlags = new BitSet(COMPONENT_ID_MAX + 1);
369
+ const sparseFlags = new BitSet(COMPONENT_ID_MAX + 1);
370
370
  const componentMerges = new Array(COMPONENT_ID_MAX + 1);
371
371
  /**
372
372
  * Allocate a new component ID from the global allocator.
@@ -399,7 +399,7 @@ function component(nameOrOptions) {
399
399
  if (options) {
400
400
  if (options.exclusive) exclusiveFlags.set(id);
401
401
  if (options.cascadeDelete) cascadeDeleteFlags.set(id);
402
- if (options.dontFragment) dontFragmentFlags.set(id);
402
+ if (options.sparse || options.dontFragment) sparseFlags.set(id);
403
403
  if (options.merge) componentMerges[id] = options.merge;
404
404
  }
405
405
  return id;
@@ -468,20 +468,21 @@ function isExclusiveComponent(id) {
468
468
  return exclusiveFlags.has(id);
469
469
  }
470
470
  /**
471
- * Check if a component is marked as `dontFragment`.
471
+ * Check if a component is marked as `sparse` (sparse storage for relations).
472
472
  *
473
- * When a component has `dontFragment: true`, relations using it do not cause
474
- * archetype fragmentation — entities with different relation targets can share
475
- * the same archetype. This is a fast O(1) bitset lookup.
473
+ * When a component has `sparse: true`, relations using it do not cause archetype
474
+ * fragmentation — entities with different relation targets can share the same
475
+ * archetype. This is a fast O(1) bitset lookup. The legacy `dontFragment` key
476
+ * is still accepted and sets the same internal flag.
476
477
  *
477
478
  * @param id - The component ID to check.
478
- * @returns `true` if the component was created with `dontFragment: true`.
479
+ * @returns `true` if the component was created with `sparse: true` (or the
480
+ * legacy `dontFragment: true`).
479
481
  *
480
- * @see {@link ComponentOptions.dontFragment} for the full explanation of how
481
- * `dontFragment` prevents archetype fragmentation.
482
+ * @see {@link ComponentOptions.sparse} for the full explanation of sparse storage.
482
483
  */
483
- function isDontFragmentComponent(id) {
484
- return dontFragmentFlags.has(id);
484
+ function isSparseComponent(id) {
485
+ return sparseFlags.has(id);
485
486
  }
486
487
  /**
487
488
  * Generic optimized function to check whether a relation ID's base component
@@ -491,9 +492,8 @@ function isDontFragmentComponent(id) {
491
492
  * ID and checking: (1) the ID is a valid relation, (2) the component ID is in the
492
493
  * valid range, (3) the target satisfies the condition, and (4) the flag bit is set.
493
494
  *
494
- * Used as the fast-path implementation for `isDontFragmentRelation`,
495
- * `isDontFragmentWildcard`, `isExclusiveRelation`, `isExclusiveWildcard`,
496
- * and `isCascadeDeleteRelation`.
495
+ * Used as the fast-path implementation for `isSparseRelation`, `isSparseWildcard`,
496
+ * `isExclusiveRelation`, `isExclusiveWildcard`, and `isCascadeDeleteRelation`.
497
497
  *
498
498
  * @param id - The entity/relation ID to check.
499
499
  * @param flagBitSet - The bitset tracking which component IDs have the flag.
@@ -509,52 +509,40 @@ function checkRelationFlag(id, flagBitSet, targetCondition) {
509
509
  return isValidComponentId(componentId) && targetCondition(targetId) && flagBitSet.has(componentId);
510
510
  }
511
511
  /**
512
- * Check if an ID is a specific (non-wildcard) relation backed by a `dontFragment`
513
- * component.
512
+ * Check if an ID is a specific (non-wildcard) relation backed by a `sparse`
513
+ * component (i.e. stored in the side sparse store rather than the archetype).
514
514
  *
515
515
  * This is used in hot paths (archetype resolution, command processing) to determine
516
- * whether a relation should be excluded from the archetype signature. Relations with
517
- * `dontFragment` components are stored in the shared {@link DontFragmentStore} instead
518
- * of being part of the archetype's component type list.
519
- *
520
- * This is an optimized function that avoids the overhead of `getDetailedIdType`
521
- * by directly decoding and checking the relation's component ID against the
522
- * `dontFragment` bitset.
516
+ * whether a relation should be excluded from the archetype signature.
523
517
  *
524
518
  * @param id - The entity/relation ID to check (must be a relation ID, not a plain
525
519
  * component ID).
526
520
  * @returns `true` if this is a specific-target relation (not wildcard) whose base
527
- * component was created with `dontFragment: true`.
521
+ * component was created with `sparse: true` (or legacy `dontFragment: true`).
528
522
  *
529
- * @see {@link isDontFragmentWildcard} for the wildcard variant.
530
- * @see {@link ComponentOptions.dontFragment} for the full explanation.
523
+ * @see {@link isSparseWildcard} for the wildcard variant.
524
+ * @see {@link ComponentOptions.sparse} for the full explanation.
531
525
  */
532
- function isDontFragmentRelation(id) {
533
- return checkRelationFlag(id, dontFragmentFlags, (targetId) => targetId !== 0);
526
+ function isSparseRelation(id) {
527
+ return checkRelationFlag(id, sparseFlags, (targetId) => targetId !== 0);
534
528
  }
535
529
  /**
536
530
  * Check if an ID is a wildcard relation (`relation(Comp, "*")`) backed by a
537
- * `dontFragment` component.
538
- *
539
- * Wildcard markers for `dontFragment` components are placed in the archetype
540
- * component list so that queries can discover archetypes containing entities
541
- * with that relation type. This function is used in `filterRegularComponentTypes`
542
- * to **keep** these wildcard markers in the archetype signature while stripping
543
- * out specific-target `dontFragment` relations.
531
+ * `sparse` component.
544
532
  *
545
- * This is an optimized function that avoids the overhead of `getDetailedIdType`
546
- * by directly decoding and checking the relation's component ID against the
547
- * `dontFragment` bitset.
533
+ * Wildcard markers for sparse components are placed in the archetype component
534
+ * list so that queries can discover archetypes containing entities with that
535
+ * relation type.
548
536
  *
549
537
  * @param id - The entity/relation ID to check.
550
538
  * @returns `true` if this is a wildcard relation (`"*"` target) whose base
551
- * component was created with `dontFragment: true`.
539
+ * component was created with `sparse: true` (or legacy `dontFragment: true`).
552
540
  *
553
- * @see {@link isDontFragmentRelation} for the specific-target variant.
554
- * @see {@link ComponentOptions.dontFragment} for the full explanation.
541
+ * @see {@link isSparseRelation} for the specific-target variant.
542
+ * @see {@link ComponentOptions.sparse} for the full explanation.
555
543
  */
556
- function isDontFragmentWildcard(id) {
557
- return checkRelationFlag(id, dontFragmentFlags, (targetId) => targetId === 0);
544
+ function isSparseWildcard(id) {
545
+ return checkRelationFlag(id, sparseFlags, (targetId) => targetId === 0);
558
546
  }
559
547
  /**
560
548
  * Check if a relation ID is a cascade delete entity-relation.
@@ -709,9 +697,9 @@ function getWildcardRelationDataSource(componentTypes, componentId, optional) {
709
697
  }
710
698
  /**
711
699
  * Build wildcard relation value from matching relations.
712
- * Now receives the DontFragmentStore directly for efficient per-component lookups.
700
+ * Receives the SparseStore directly for efficient per-component lookups.
713
701
  */
714
- function buildWildcardRelationValue(wildcardRelationType, matchingRelations, getDataAtIndex, dontFragmentStore, entityId, optional) {
702
+ function buildWildcardRelationValue(wildcardRelationType, matchingRelations, getDataAtIndex, sparseStore, entityId, optional) {
715
703
  const relations = [];
716
704
  const targetComponentId = getComponentIdFromRelationId(wildcardRelationType);
717
705
  for (const relType of matchingRelations || []) {
@@ -720,7 +708,7 @@ function buildWildcardRelationValue(wildcardRelationType, matchingRelations, get
720
708
  relations.push([targetId, data === MISSING_COMPONENT ? void 0 : data]);
721
709
  }
722
710
  if (targetComponentId !== void 0) {
723
- const dfMatches = dontFragmentStore.getRelationsForComponent(entityId, targetComponentId);
711
+ const dfMatches = sparseStore.getRelationsForComponent(entityId, targetComponentId);
724
712
  for (const m of dfMatches) relations.push(m);
725
713
  }
726
714
  if (relations.length === 0) {
@@ -747,10 +735,10 @@ function buildRegularComponentValue(dataSource, entityIndex, optional) {
747
735
  /**
748
736
  * Build a single component value based on its type
749
737
  */
750
- function buildSingleComponent(compType, dataSource, entityIndex, entityId, getComponentData, dontFragmentRelations) {
738
+ function buildSingleComponent(compType, dataSource, entityIndex, entityId, getComponentData, sparseRelations) {
751
739
  const optional = isOptionalEntityId(compType);
752
740
  const actualType = optional ? compType.optional : compType;
753
- if (getIdType(actualType) === "wildcard-relation") return buildWildcardRelationValue(actualType, dataSource, (relType) => getComponentData(relType)[entityIndex], dontFragmentRelations, entityId, optional);
741
+ if (getIdType(actualType) === "wildcard-relation") return buildWildcardRelationValue(actualType, dataSource, (relType) => getComponentData(relType)[entityIndex], sparseRelations, entityId, optional);
754
742
  else return buildRegularComponentValue(dataSource, entityIndex, optional);
755
743
  }
756
744
  //#endregion
@@ -787,11 +775,10 @@ var Archetype = class {
787
775
  */
788
776
  entityToIndex = /* @__PURE__ */ new Map();
789
777
  /**
790
- * DontFragmentStore (keyed primarily by relation ComponentId).
791
- * Uses optimized RelationEntry (single/multi) for the common exclusive case.
778
+ * SparseStore used for relations declared with `sparse: true`.
792
779
  * See store.ts for implementation details.
793
780
  */
794
- dontFragmentRelations;
781
+ sparseRelations;
795
782
  /**
796
783
  * Multi-hooks that match this archetype
797
784
  */
@@ -800,10 +787,10 @@ var Archetype = class {
800
787
  * Cache for pre-computed component data sources to avoid repeated calculations
801
788
  */
802
789
  componentDataSourcesCache = /* @__PURE__ */ new Map();
803
- constructor(componentTypes, dontFragmentRelations) {
790
+ constructor(componentTypes, sparseStore) {
804
791
  this.componentTypes = normalizeComponentTypes(componentTypes);
805
792
  this.componentTypeSet = new Set(this.componentTypes);
806
- this.dontFragmentRelations = dontFragmentRelations;
793
+ this.sparseRelations = sparseStore;
807
794
  for (const componentType of this.componentTypes) this.componentData.set(componentType, []);
808
795
  }
809
796
  get size() {
@@ -829,13 +816,13 @@ var Archetype = class {
829
816
  const data = componentData.get(componentType);
830
817
  this.getComponentData(componentType).push(!componentData.has(componentType) ? MISSING_COMPONENT : data);
831
818
  }
832
- this.addDontFragmentRelations(entityId, componentData);
819
+ this.addSparseRelations(entityId, componentData);
833
820
  }
834
- addDontFragmentRelations(entityId, componentData) {
821
+ addSparseRelations(entityId, componentData) {
835
822
  for (const [componentType, data] of componentData) {
836
823
  if (this.componentTypeSet.has(componentType)) continue;
837
824
  const detailedType = getDetailedIdType(componentType);
838
- if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId)) this.dontFragmentRelations.setValue(entityId, componentType, data);
825
+ if (isRelationType(detailedType) && isSparseComponent(detailedType.componentId)) this.sparseRelations.setValue(entityId, componentType, data);
839
826
  }
840
827
  }
841
828
  getEntity(entityId) {
@@ -846,18 +833,16 @@ var Archetype = class {
846
833
  const data = this.getComponentData(componentType)[index];
847
834
  entityData.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
848
835
  }
849
- const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entityId);
850
- for (const [componentType, data] of dontFragmentTuples) entityData.set(componentType, data);
836
+ const sparseTuples = this.sparseRelations.getAllForEntity(entityId);
837
+ for (const [componentType, data] of sparseTuples) entityData.set(componentType, data);
851
838
  return entityData;
852
839
  }
853
840
  /**
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.
841
+ * Returns all sparse-stored relations for the given entity.
842
+ * Internal helper used by command processing and tests.
858
843
  */
859
- getEntityDontFragmentRelations(entityId) {
860
- const tuples = this.dontFragmentRelations.getAllForEntity(entityId);
844
+ getEntitySparseRelations(entityId) {
845
+ const tuples = this.sparseRelations.getAllForEntity(entityId);
861
846
  if (tuples.length === 0) return void 0;
862
847
  const map = /* @__PURE__ */ new Map();
863
848
  for (const [relType, data] of tuples) map.set(relType, data);
@@ -870,22 +855,61 @@ var Archetype = class {
870
855
  const data = this.getComponentData(componentType)[i];
871
856
  components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
872
857
  }
873
- const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entity);
874
- for (const [componentType, data] of dontFragmentTuples) components.set(componentType, data);
858
+ const sparseTuples = this.sparseRelations.getAllForEntity(entity);
859
+ for (const [componentType, data] of sparseTuples) components.set(componentType, data);
875
860
  return {
876
861
  entity,
877
862
  components
878
863
  };
879
864
  });
880
865
  }
866
+ /**
867
+ * @internal Serialization fast-path.
868
+ *
869
+ * Appends SerializedEntity records directly from the archetype's column storage
870
+ * (componentData arrays) plus sparse relations, avoiding per-entity Map
871
+ * allocation and repeated Array.from(entries()).
872
+ *
873
+ * Component type IDs should be pre-encoded by the caller (once per archetype)
874
+ * and passed in `encodedComponentTypes` (same order and length as this.componentTypes).
875
+ *
876
+ * The provided `encode` function should be the cached variant for best performance
877
+ * on entity IDs and any sparse relation type IDs.
878
+ *
879
+ * `sparseByEntity` is an optional pre-fetched map from a bulk
880
+ * `SparseStore.getAllForEntities` call (further reduces per-entity calls).
881
+ */
882
+ appendSerializedEntities(out, encode, encodedComponentTypes, sparseByEntity) {
883
+ if (encodedComponentTypes.length !== this.componentTypes.length) throw new Error("encodedComponentTypes length must match archetype componentTypes");
884
+ for (let i = 0; i < this.entities.length; i++) {
885
+ const entity = this.entities[i];
886
+ const components = [];
887
+ for (let c = 0; c < this.componentTypes.length; c++) {
888
+ const data = this.getComponentData(this.componentTypes[c])[i];
889
+ components.push({
890
+ type: encodedComponentTypes[c],
891
+ value: data === MISSING_COMPONENT ? void 0 : data
892
+ });
893
+ }
894
+ const sparseTuples = sparseByEntity?.get(entity) ?? this.sparseRelations.getAllForEntity(entity);
895
+ for (const [componentType, data] of sparseTuples) components.push({
896
+ type: encode(componentType),
897
+ value: data
898
+ });
899
+ out.push({
900
+ id: encode(entity),
901
+ components
902
+ });
903
+ }
904
+ }
881
905
  removeEntity(entityId) {
882
906
  const index = this.entityToIndex.get(entityId);
883
907
  if (index === void 0) return void 0;
884
908
  const removedData = /* @__PURE__ */ new Map();
885
909
  for (const componentType of this.componentTypes) removedData.set(componentType, this.getComponentData(componentType)[index]);
886
- const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entityId);
887
- for (const [componentType, data] of dontFragmentTuples) removedData.set(componentType, data);
888
- this.dontFragmentRelations.deleteEntity(entityId);
910
+ const sparseTuples = this.sparseRelations.getAllForEntity(entityId);
911
+ for (const [componentType, data] of sparseTuples) removedData.set(componentType, data);
912
+ this.sparseRelations.deleteEntity(entityId);
889
913
  this.entityToIndex.delete(entityId);
890
914
  const lastIndex = this.entities.length - 1;
891
915
  if (index !== lastIndex) {
@@ -924,7 +948,7 @@ var Archetype = class {
924
948
  }
925
949
  }
926
950
  if (componentId !== void 0) {
927
- const matches = this.dontFragmentRelations.getRelationsForComponent(entityId, componentId);
951
+ const matches = this.sparseRelations.getRelationsForComponent(entityId, componentId);
928
952
  for (const m of matches) relations.push(m);
929
953
  }
930
954
  return relations;
@@ -935,7 +959,7 @@ var Archetype = class {
935
959
  if (data === MISSING_COMPONENT) throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
936
960
  return data;
937
961
  }
938
- if (this.dontFragmentRelations.getValue(entityId, componentType) !== void 0 || this.dontFragmentRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) return this.dontFragmentRelations.getValue(entityId, componentType);
962
+ if (this.sparseRelations.getValue(entityId, componentType) !== void 0 || this.sparseRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) return this.sparseRelations.getValue(entityId, componentType);
939
963
  throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
940
964
  }
941
965
  getOptional(entityId, componentType) {
@@ -946,9 +970,9 @@ var Archetype = class {
946
970
  if (data === MISSING_COMPONENT) return void 0;
947
971
  return { value: data };
948
972
  }
949
- const value = this.dontFragmentRelations.getValue(entityId, componentType);
973
+ const value = this.sparseRelations.getValue(entityId, componentType);
950
974
  if (value !== void 0) return { value };
951
- if (this.dontFragmentRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) return { value: this.dontFragmentRelations.getValue(entityId, componentType) };
975
+ if (this.sparseRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) return { value: this.sparseRelations.getValue(entityId, componentType) };
952
976
  }
953
977
  set(entityId, componentType, data) {
954
978
  const index = this.entityToIndex.get(entityId);
@@ -958,8 +982,8 @@ var Archetype = class {
958
982
  return;
959
983
  }
960
984
  const detailedType = getDetailedIdType(componentType);
961
- if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId)) {
962
- this.dontFragmentRelations.setValue(entityId, componentType, data);
985
+ if (isRelationType(detailedType) && isSparseComponent(detailedType.componentId)) {
986
+ this.sparseRelations.setValue(entityId, componentType, data);
963
987
  return;
964
988
  }
965
989
  throw new Error(`Component type ${componentType} is not in this archetype`);
@@ -992,7 +1016,7 @@ var Archetype = class {
992
1016
  return optional ? this.getOptionalComponentData(actualType) : this.getComponentData(actualType);
993
1017
  }
994
1018
  buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entityId) {
995
- return componentDataSources.map((dataSource, i) => buildSingleComponent(componentTypes[i], dataSource, entityIndex, entityId, (type) => this.getComponentData(type), this.dontFragmentRelations));
1019
+ return componentDataSources.map((dataSource, i) => buildSingleComponent(componentTypes[i], dataSource, entityIndex, entityId, (type) => this.getComponentData(type), this.sparseRelations));
996
1020
  }
997
1021
  getEntitiesWithComponents(componentTypes) {
998
1022
  const result = [];
@@ -1029,8 +1053,8 @@ var Archetype = class {
1029
1053
  const data = this.getComponentData(componentType)[i];
1030
1054
  components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
1031
1055
  }
1032
- const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entity);
1033
- for (const [componentType, data] of dontFragmentTuples) components.set(componentType, data);
1056
+ const sparseTuples = this.sparseRelations.getAllForEntity(entity);
1057
+ for (const [componentType, data] of sparseTuples) components.set(componentType, data);
1034
1058
  callback(entity, components);
1035
1059
  }
1036
1060
  }
@@ -1039,30 +1063,31 @@ var Archetype = class {
1039
1063
  const detailedType = getDetailedIdType(componentType);
1040
1064
  if (isRelationType(detailedType) && detailedType.componentId === componentId) return true;
1041
1065
  }
1042
- for (const entityId of this.entities) if (this.dontFragmentRelations.getRelationsForComponent(entityId, componentId).length > 0) return true;
1066
+ for (const entityId of this.entities) if (this.sparseRelations.getRelationsForComponent(entityId, componentId).length > 0) return true;
1043
1067
  return false;
1044
1068
  }
1045
1069
  };
1046
1070
  //#endregion
1047
1071
  //#region src/archetype/store.ts
1048
1072
  /**
1049
- * Production implementation of DontFragmentStore.
1073
+ * Production implementation of SparseStore.
1050
1074
  *
1051
1075
  * Internal layout (optimized):
1052
1076
  * - byComponent: baseComponentId → (entityId → RelationEntry)
1053
1077
  * 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.
1078
+ * avoiding Map allocation for the vast majority of usage.
1055
1079
  * - entityIndex: entityId → Set<baseComponentId>
1056
1080
  * Lightweight reverse index.
1057
1081
  */
1058
- var DontFragmentStoreImpl = class {
1082
+ var SparseStoreImpl = class {
1059
1083
  /**
1060
1084
  * Primary storage, keyed by the base relation component ID.
1061
1085
  */
1062
1086
  byComponent = /* @__PURE__ */ new Map();
1063
1087
  /**
1064
1088
  * Reverse index: which base component kinds an entity participates in.
1065
- * Used only by the infrequent getAllForEntity / deleteEntity paths.
1089
+ * Only required to support getAllForEntity and deleteEntity efficiently.
1090
+ * The primary storage (byComponent) is deliberately not optimized for these operations.
1066
1091
  */
1067
1092
  entityIndex = /* @__PURE__ */ new Map();
1068
1093
  getValue(entityId, relationType) {
@@ -1081,7 +1106,7 @@ var DontFragmentStoreImpl = class {
1081
1106
  }
1082
1107
  setValue(entityId, relationType, data) {
1083
1108
  const componentId = getComponentIdFromRelationId(relationType);
1084
- if (componentId === void 0) throw new Error("setValue called with a non-relation type on DontFragmentStore");
1109
+ if (componentId === void 0) throw new Error("setValue called with a non-relation type on SparseStore");
1085
1110
  let entities = this.byComponent.get(componentId);
1086
1111
  if (!entities) {
1087
1112
  entities = /* @__PURE__ */ new Map();
@@ -1199,6 +1224,14 @@ var DontFragmentStoreImpl = class {
1199
1224
  }
1200
1225
  this.entityIndex.delete(entityId);
1201
1226
  }
1227
+ getAllForEntities(entityIds) {
1228
+ const result = /* @__PURE__ */ new Map();
1229
+ for (const eid of entityIds) {
1230
+ const data = this.getAllForEntity(eid);
1231
+ if (data.length > 0) result.set(eid, data);
1232
+ }
1233
+ return result;
1234
+ }
1202
1235
  };
1203
1236
  //#endregion
1204
1237
  //#region src/commands/buffer.ts
@@ -1249,7 +1282,8 @@ var CommandBuffer = class {
1249
1282
  });
1250
1283
  }
1251
1284
  /**
1252
- * Execute all commands and clear the buffer
1285
+ * Execute all commands and clear the buffer.
1286
+ * Returns the number of iterations performed (for debug stats).
1253
1287
  */
1254
1288
  execute() {
1255
1289
  let iterations = 0;
@@ -1269,6 +1303,7 @@ var CommandBuffer = class {
1269
1303
  for (const [entityId, commands] of entityCommands) this.executeEntityCommands(entityId, commands);
1270
1304
  entityCommands.clear();
1271
1305
  }
1306
+ return iterations;
1272
1307
  }
1273
1308
  /**
1274
1309
  * Get current commands (for testing)
@@ -1339,29 +1374,6 @@ var ComponentChangeset = class {
1339
1374
  for (const [componentType, component] of this.adds) existingComponents.set(componentType, component);
1340
1375
  return existingComponents;
1341
1376
  }
1342
- /**
1343
- * Get the final component types after applying the changeset
1344
- * @param existingComponentTypes - The current component types on the entity
1345
- * @returns The final component types or undefined if no changes
1346
- */
1347
- getFinalComponentTypes(existingComponentTypes) {
1348
- const finalComponentTypes = new Set(existingComponentTypes);
1349
- let changed = false;
1350
- for (const componentType of this.removes) {
1351
- if (!finalComponentTypes.has(componentType)) {
1352
- this.removes.delete(componentType);
1353
- continue;
1354
- }
1355
- changed = true;
1356
- finalComponentTypes.delete(componentType);
1357
- }
1358
- for (const componentType of this.adds.keys()) {
1359
- if (finalComponentTypes.has(componentType)) continue;
1360
- changed = true;
1361
- finalComponentTypes.add(componentType);
1362
- }
1363
- return changed ? Array.from(finalComponentTypes) : void 0;
1364
- }
1365
1377
  };
1366
1378
  //#endregion
1367
1379
  //#region src/component/entity-store.ts
@@ -1550,7 +1562,7 @@ function matchesComponentTypes(archetype, componentTypes) {
1550
1562
  if (!isRelationId(archetypeType)) return false;
1551
1563
  return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
1552
1564
  });
1553
- else if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId !== void 0 && isDontFragmentComponent(detailedType.componentId)) {
1565
+ else if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId !== void 0 && isSparseComponent(detailedType.componentId)) {
1554
1566
  const wildcardMarker = relation(detailedType.componentId, "*");
1555
1567
  return archetype.componentTypeSet.has(wildcardMarker);
1556
1568
  } else return archetype.componentTypeSet.has(type);
@@ -1597,8 +1609,8 @@ var Query = class {
1597
1609
  _cacheKey;
1598
1610
  /** Cached wildcard component types for faster entity filtering */
1599
1611
  wildcardTypes;
1600
- /** Cached specific dontFragment relation types that need entity-level filtering */
1601
- specificDontFragmentTypes;
1612
+ /** Cached specific sparse relation types that need entity-level filtering */
1613
+ specificSparseRelationTypes;
1602
1614
  /**
1603
1615
  * @internal Queries should be created via {@link World.createQuery}, not instantiated directly.
1604
1616
  */
@@ -1607,9 +1619,9 @@ var Query = class {
1607
1619
  this.componentTypes = normalizeComponentTypes(componentTypes);
1608
1620
  this.filter = filter;
1609
1621
  this.wildcardTypes = this.componentTypes.filter((ct) => getDetailedIdType(ct).type === "wildcard-relation");
1610
- this.specificDontFragmentTypes = this.componentTypes.filter((ct) => {
1622
+ this.specificSparseRelationTypes = this.componentTypes.filter((ct) => {
1611
1623
  const detailedType = getDetailedIdType(ct);
1612
- return (detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId !== void 0 && isDontFragmentComponent(detailedType.componentId);
1624
+ return (detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId !== void 0 && isSparseComponent(detailedType.componentId);
1613
1625
  });
1614
1626
  this.updateCache();
1615
1627
  if (registry) registry.register(this);
@@ -1633,7 +1645,7 @@ var Query = class {
1633
1645
  */
1634
1646
  getEntities() {
1635
1647
  this.ensureNotDisposed();
1636
- if (this.wildcardTypes.length === 0 && this.specificDontFragmentTypes.length === 0) {
1648
+ if (this.wildcardTypes.length === 0 && this.specificSparseRelationTypes.length === 0) {
1637
1649
  const result = [];
1638
1650
  for (const archetype of this.cachedArchetypes) for (const entity of archetype.getEntities()) result.push(entity);
1639
1651
  return result;
@@ -1643,14 +1655,14 @@ var Query = class {
1643
1655
  return result;
1644
1656
  }
1645
1657
  /**
1646
- * Check if entity matches all query requirements (wildcards and specific dontFragment relations)
1658
+ * Check if entity matches all query requirements (wildcards and specific sparse relations)
1647
1659
  */
1648
1660
  entityMatchesQuery(archetype, entity) {
1649
1661
  for (const wildcardType of this.wildcardTypes) {
1650
1662
  const relations = archetype.get(entity, wildcardType);
1651
1663
  if (!relations || relations.length === 0) return false;
1652
1664
  }
1653
- for (const specificType of this.specificDontFragmentTypes) if (archetype.getOptional(entity, specificType) === void 0) return false;
1665
+ for (const specificType of this.specificSparseRelationTypes) if (archetype.getOptional(entity, specificType) === void 0) return false;
1654
1666
  return true;
1655
1667
  }
1656
1668
  /**
@@ -1872,7 +1884,7 @@ function processSetCommand(entityId, currentArchetype, componentType, component,
1872
1884
  const componentId = getComponentIdFromRelationId(componentType);
1873
1885
  if (componentId !== void 0) {
1874
1886
  handleExclusiveRelation(entityId, currentArchetype, componentId);
1875
- if (isDontFragmentComponent(componentId)) {
1887
+ if (isSparseComponent(componentId)) {
1876
1888
  const wildcardMarker = relation(componentId, "*");
1877
1889
  if (!currentArchetype.componentTypeSet.has(wildcardMarker)) changeset.set(wildcardMarker, void 0);
1878
1890
  }
@@ -1898,17 +1910,17 @@ function removeMatchingRelations(entityId, archetype, baseComponentId, changeset
1898
1910
  if (isWildcardRelationId(componentType)) continue;
1899
1911
  if (getComponentIdFromRelationId(componentType) === baseComponentId) changeset.delete(componentType);
1900
1912
  }
1901
- const dontFragmentData = archetype.getEntityDontFragmentRelations(entityId);
1902
- if (dontFragmentData) {
1903
- for (const componentType of dontFragmentData.keys()) if (getComponentIdFromRelationId(componentType) === baseComponentId) changeset.delete(componentType);
1913
+ const sparseData = archetype.getEntitySparseRelations(entityId);
1914
+ if (sparseData) {
1915
+ for (const componentType of sparseData.keys()) if (getComponentIdFromRelationId(componentType) === baseComponentId) changeset.delete(componentType);
1904
1916
  }
1905
1917
  }
1906
1918
  function removeWildcardRelations(entityId, currentArchetype, baseComponentId, changeset) {
1907
1919
  removeMatchingRelations(entityId, currentArchetype, baseComponentId, changeset);
1908
- if (isDontFragmentComponent(baseComponentId)) changeset.delete(relation(baseComponentId, "*"));
1920
+ if (isSparseComponent(baseComponentId)) changeset.delete(relation(baseComponentId, "*"));
1909
1921
  }
1910
1922
  function maybeRemoveWildcardMarker(entityId, archetype, removedComponentType, componentId, changeset) {
1911
- if (componentId === void 0 || !isDontFragmentComponent(componentId)) return;
1923
+ if (componentId === void 0 || !isSparseComponent(componentId)) return;
1912
1924
  const wildcardMarker = relation(componentId, "*");
1913
1925
  for (const otherComponentType of archetype.componentTypes) {
1914
1926
  if (otherComponentType === removedComponentType) continue;
@@ -1916,8 +1928,8 @@ function maybeRemoveWildcardMarker(entityId, archetype, removedComponentType, co
1916
1928
  if (changeset.removes.has(otherComponentType)) continue;
1917
1929
  if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
1918
1930
  }
1919
- const dontFragmentData = archetype.getEntityDontFragmentRelations(entityId);
1920
- if (dontFragmentData) for (const otherComponentType of dontFragmentData.keys()) {
1931
+ const sparseData = archetype.getEntitySparseRelations(entityId);
1932
+ if (sparseData) for (const otherComponentType of sparseData.keys()) {
1921
1933
  if (otherComponentType === removedComponentType) continue;
1922
1934
  if (changeset.removes.has(otherComponentType)) continue;
1923
1935
  if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
@@ -1930,7 +1942,7 @@ function maybeRemoveWildcardMarker(entityId, archetype, removedComponentType, co
1930
1942
  }
1931
1943
  function hasEntityComponent(archetype, entityId, componentType) {
1932
1944
  if (archetype.componentTypeSet.has(componentType)) return true;
1933
- return archetype.getEntityDontFragmentRelations(entityId)?.has(componentType) ?? false;
1945
+ return archetype.getEntitySparseRelations(entityId)?.has(componentType) ?? false;
1934
1946
  }
1935
1947
  function pruneMissingRemovals(changeset, archetype, entityId) {
1936
1948
  let toPrune;
@@ -1941,14 +1953,14 @@ function pruneMissingRemovals(changeset, archetype, entityId) {
1941
1953
  if (toPrune !== void 0) for (const componentType of toPrune) changeset.removes.delete(componentType);
1942
1954
  }
1943
1955
  function hasArchetypeStructuralChange(changeset, currentArchetype) {
1944
- for (const componentType of changeset.removes) if (!isDontFragmentRelation(componentType) && currentArchetype.componentTypeSet.has(componentType)) return true;
1945
- for (const componentType of changeset.adds.keys()) if (!isDontFragmentRelation(componentType) && !currentArchetype.componentTypeSet.has(componentType)) return true;
1956
+ for (const componentType of changeset.removes) if (!isSparseRelation(componentType) && currentArchetype.componentTypeSet.has(componentType)) return true;
1957
+ for (const componentType of changeset.adds.keys()) if (!isSparseRelation(componentType) && !currentArchetype.componentTypeSet.has(componentType)) return true;
1946
1958
  return false;
1947
1959
  }
1948
1960
  function buildFinalRegularComponentTypes(currentArchetype, changeset) {
1949
1961
  const finalRegularTypes = new Set(currentArchetype.componentTypes);
1950
- for (const componentType of changeset.removes) if (!isDontFragmentRelation(componentType)) finalRegularTypes.delete(componentType);
1951
- for (const [componentType] of changeset.adds) if (!isDontFragmentRelation(componentType)) finalRegularTypes.add(componentType);
1962
+ for (const componentType of changeset.removes) if (!isSparseRelation(componentType)) finalRegularTypes.delete(componentType);
1963
+ for (const [componentType] of changeset.adds) if (!isSparseRelation(componentType)) finalRegularTypes.add(componentType);
1952
1964
  return Array.from(finalRegularTypes);
1953
1965
  }
1954
1966
  function applyChangeset(ctx, entityId, currentArchetype, changeset, entityToArchetype, removedComponents) {
@@ -1962,39 +1974,34 @@ function applyChangeset(ctx, entityId, currentArchetype, changeset, entityToArch
1962
1974
  entityToArchetype.set(entityId, newArchetype);
1963
1975
  return newArchetype;
1964
1976
  }
1965
- if (removedComponents !== null) applyDontFragmentChanges(ctx.dontFragmentStore, entityId, changeset, removedComponents);
1966
- else applyDontFragmentChangesNoHooks(ctx.dontFragmentStore, entityId, changeset);
1977
+ if (removedComponents !== null) applySparseChanges(ctx.sparseStore, entityId, changeset, removedComponents);
1978
+ else applySparseChangesNoHooks(ctx.sparseStore, entityId, changeset);
1967
1979
  for (const [componentType, component] of changeset.adds) {
1968
- if (isDontFragmentRelation(componentType)) continue;
1980
+ if (isSparseRelation(componentType)) continue;
1969
1981
  currentArchetype.set(entityId, componentType, component);
1970
1982
  }
1971
1983
  return currentArchetype;
1972
1984
  }
1973
- /**
1974
- * No-hooks variant of applyDontFragmentChanges that skips tracking removed component data.
1975
- *
1976
- * Rewritten for the new DontFragmentStore interface (ComponentId-primary storage).
1977
- */
1978
- function applyDontFragmentChanges(dontFragmentRelations, entityId, changeset, removedComponents) {
1979
- for (const componentType of changeset.removes) if (isDontFragmentRelation(componentType)) {
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);
1985
+ function applySparseChanges(sparseStore, entityId, changeset, removedComponents) {
1986
+ for (const componentType of changeset.removes) if (isSparseRelation(componentType)) {
1987
+ const removedValue = sparseStore.getValue(entityId, componentType);
1988
+ if (removedValue !== void 0 || sparseStore.getAllForEntity(entityId).some(([t]) => t === componentType)) removedComponents.set(componentType, removedValue);
1989
+ sparseStore.deleteValue(entityId, componentType);
1983
1990
  }
1984
- for (const [componentType, component] of changeset.adds) if (isDontFragmentRelation(componentType)) dontFragmentRelations.setValue(entityId, componentType, component);
1991
+ for (const [componentType, component] of changeset.adds) if (isSparseRelation(componentType)) sparseStore.setValue(entityId, componentType, component);
1985
1992
  }
1986
- function applyDontFragmentChangesNoHooks(dontFragmentRelations, entityId, changeset) {
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);
1993
+ function applySparseChangesNoHooks(sparseStore, entityId, changeset) {
1994
+ for (const componentType of changeset.removes) if (isSparseRelation(componentType)) sparseStore.deleteValue(entityId, componentType);
1995
+ for (const [componentType, component] of changeset.adds) if (isSparseRelation(componentType)) sparseStore.setValue(entityId, componentType, component);
1989
1996
  }
1990
1997
  function filterRegularComponentTypes(componentTypes) {
1991
1998
  const regularTypes = [];
1992
1999
  for (const componentType of componentTypes) {
1993
- if (isDontFragmentWildcard(componentType)) {
2000
+ if (isSparseWildcard(componentType)) {
1994
2001
  regularTypes.push(componentType);
1995
2002
  continue;
1996
2003
  }
1997
- if (isDontFragmentRelation(componentType)) continue;
2004
+ if (isSparseRelation(componentType)) continue;
1998
2005
  regularTypes.push(componentType);
1999
2006
  }
2000
2007
  return regularTypes;
@@ -2002,9 +2009,15 @@ function filterRegularComponentTypes(componentTypes) {
2002
2009
  //#endregion
2003
2010
  //#region src/world/hooks.ts
2004
2011
  /**
2012
+ * Debug-only counter incremented on every invokeHook call when armed.
2013
+ * World reads and resets this during armed syncs.
2014
+ */
2015
+ const debugHookExecutionCounter = { value: 0 };
2016
+ /**
2005
2017
  * Unified hook invocation: prefers entry.callback (callback style) over hook.on_* (object style).
2006
2018
  */
2007
2019
  function invokeHook(entry, event, entityId, components) {
2020
+ debugHookExecutionCounter.value++;
2008
2021
  if (entry.callback) {
2009
2022
  entry.callback(event, entityId, ...components);
2010
2023
  return;
@@ -2303,9 +2316,9 @@ function getEntityReferences(entityReferences, targetEntityId) {
2303
2316
  //#endregion
2304
2317
  //#region src/storage/serialization.ts
2305
2318
  /**
2306
- * Encode an internal EntityId into a SerializedEntityId for snapshots
2319
+ * Core encoding logic (no cache). Extracted so cached wrapper can reuse it without duplication.
2307
2320
  */
2308
- function encodeEntityId(id) {
2321
+ function encodeEntityIdCore(id) {
2309
2322
  const detailed = getDetailedIdType(id);
2310
2323
  switch (detailed.type) {
2311
2324
  case "component": {
@@ -2343,6 +2356,20 @@ function encodeEntityId(id) {
2343
2356
  }
2344
2357
  }
2345
2358
  /**
2359
+ * Encode an EntityId, using an optional cache Map to avoid repeated getDetailedIdType
2360
+ * + name lookup work for IDs that appear many times (typical during full world snapshot).
2361
+ */
2362
+ function encodeEntityIdCached(id, cache) {
2363
+ if (cache) {
2364
+ const cached = cache.get(id);
2365
+ if (cached !== void 0) return cached;
2366
+ const result = encodeEntityIdCore(id);
2367
+ cache.set(id, result);
2368
+ return result;
2369
+ }
2370
+ return encodeEntityIdCore(id);
2371
+ }
2372
+ /**
2346
2373
  * Decode a SerializedEntityId back into an internal EntityId
2347
2374
  */
2348
2375
  function decodeSerializedId(sid) {
@@ -2383,24 +2410,16 @@ function decodeSerializedId(sid) {
2383
2410
  * Serializes the full world state to a plain JS object suitable for JSON encoding.
2384
2411
  */
2385
2412
  function serializeWorld(archetypes, componentEntities, entityIdManager) {
2413
+ const idCache = /* @__PURE__ */ new Map();
2386
2414
  const entities = [];
2387
2415
  for (const archetype of archetypes) {
2388
- const dumpedEntities = archetype.dump();
2389
- for (const { entity, components } of dumpedEntities) entities.push({
2390
- id: encodeEntityId(entity),
2391
- components: Array.from(components.entries()).map(([rawType, value]) => ({
2392
- type: encodeEntityId(rawType),
2393
- value: value === MISSING_COMPONENT ? void 0 : value
2394
- }))
2395
- });
2416
+ const encodedComponentTypes = archetype.componentTypes.map((t) => encodeEntityIdCached(t, idCache));
2417
+ archetype.appendSerializedEntities(entities, (id) => encodeEntityIdCached(id, idCache), encodedComponentTypes);
2396
2418
  }
2397
2419
  const componentEntitiesArr = [];
2398
2420
  for (const [entityId, components] of componentEntities.entries()) componentEntitiesArr.push({
2399
- id: encodeEntityId(entityId),
2400
- components: Array.from(components.entries()).map(([rawType, value]) => ({
2401
- type: encodeEntityId(rawType),
2402
- value: value === MISSING_COMPONENT ? void 0 : value
2403
- }))
2421
+ id: encodeEntityIdCached(entityId, idCache),
2422
+ components: serializeComponentsFromMap(components, idCache)
2404
2423
  });
2405
2424
  return {
2406
2425
  version: 1,
@@ -2409,6 +2428,15 @@ function serializeWorld(archetypes, componentEntities, entityIdManager) {
2409
2428
  componentEntities: componentEntitiesArr
2410
2429
  };
2411
2430
  }
2431
+ /** Small helper to avoid duplicating the "Map → SerializedComponent[] with cache" pattern. */
2432
+ function serializeComponentsFromMap(components, idCache) {
2433
+ const result = [];
2434
+ for (const [rawType, value] of components) result.push({
2435
+ type: encodeEntityIdCached(rawType, idCache),
2436
+ value: value === MISSING_COMPONENT ? void 0 : value
2437
+ });
2438
+ return result;
2439
+ }
2412
2440
  /**
2413
2441
  * Restores world state from a snapshot into the provided context.
2414
2442
  * Intended to be called from `World`'s constructor.
@@ -2430,12 +2458,11 @@ function deserializeWorld(ctx, snapshot) {
2430
2458
  const entityId = decodeSerializedId(entry.id);
2431
2459
  const componentsArray = entry.components || [];
2432
2460
  const componentMap = /* @__PURE__ */ new Map();
2433
- const componentTypes = [];
2434
2461
  for (const componentEntry of componentsArray) {
2435
2462
  const componentType = decodeSerializedId(componentEntry.type);
2436
2463
  componentMap.set(componentType, componentEntry.value);
2437
- componentTypes.push(componentType);
2438
2464
  }
2465
+ const componentTypes = Array.from(componentMap.keys());
2439
2466
  const archetype = ctx.ensureArchetype(componentTypes);
2440
2467
  archetype.addEntity(entityId, componentMap);
2441
2468
  ctx.setEntityToArchetype(entityId, archetype);
@@ -2461,18 +2488,22 @@ var World = class {
2461
2488
  entityReferences = /* @__PURE__ */ new Map();
2462
2489
  /** Reverse index: entity ID → set of archetypes whose componentTypes include that entity ID */
2463
2490
  entityToReferencingArchetypes = /* @__PURE__ */ new Map();
2464
- /** DontFragment relation storage, shared with all Archetype instances */
2465
- dontFragmentStore = new DontFragmentStoreImpl();
2491
+ /** Sparse relation storage (for components created with `sparse: true`), shared with all Archetype instances */
2492
+ sparseStore = new SparseStoreImpl();
2466
2493
  /** Component entity (singleton) storage */
2467
2494
  componentEntities = new ComponentEntityStore();
2468
2495
  queryRegistry = new QueryRegistry();
2469
2496
  hooks = /* @__PURE__ */ new Set();
2497
+ _debugCollectors = /* @__PURE__ */ new Set();
2498
+ _debugMigrations = 0;
2499
+ _debugArchetypesCreated = 0;
2500
+ _debugArchetypesRemoved = 0;
2470
2501
  commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
2471
2502
  _changeset = new ComponentChangeset();
2472
2503
  _removeChangeset = new ComponentChangeset();
2473
2504
  /** Cached command processor context to avoid per-entity object allocation */
2474
2505
  _commandCtx = {
2475
- dontFragmentStore: this.dontFragmentStore,
2506
+ sparseStore: this.sparseStore,
2476
2507
  ensureArchetype: (ct) => this.ensureArchetype(ct)
2477
2508
  };
2478
2509
  /** Cached hooks context to avoid per-entity object allocation */
@@ -2678,9 +2709,9 @@ var World = class {
2678
2709
  const archetype = this.entityToArchetype.get(entityId);
2679
2710
  if (!archetype) return false;
2680
2711
  if (archetype.componentTypeSet.has(componentType)) return true;
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);
2712
+ if (isSparseRelation(componentType)) {
2713
+ if (this.sparseStore.getValue(entityId, componentType) !== void 0) return true;
2714
+ return this.sparseStore.getAllForEntity(entityId).some(([t]) => t === componentType);
2684
2715
  }
2685
2716
  return false;
2686
2717
  }
@@ -2693,8 +2724,8 @@ var World = class {
2693
2724
  if (!archetype) throw new Error(`Entity ${entityId} does not exist`);
2694
2725
  if (componentType >= 0 || componentType % RELATION_SHIFT !== 0) {
2695
2726
  const inArchetype = archetype.componentTypeSet.has(componentType);
2696
- const hasDontFragment = isDontFragmentRelation(componentType);
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().`);
2727
+ const hasSparse = isSparseRelation(componentType);
2728
+ if (!(inArchetype || hasSparse && (this.sparseStore.getValue(entityId, componentType) !== void 0 || this.sparseStore.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().`);
2698
2729
  }
2699
2730
  return archetype.get(entityId, componentType);
2700
2731
  }
@@ -2716,6 +2747,163 @@ var World = class {
2716
2747
  }
2717
2748
  return archetype.getOptional(entityId, componentType);
2718
2749
  }
2750
+ /**
2751
+ * Retrieves all targets (and their associated data) for relations of a given
2752
+ * base component on an entity.
2753
+ *
2754
+ * This is the ergonomic replacement for the common pattern:
2755
+ * world.get(entity, relation(Comp, "*"))
2756
+ *
2757
+ * @example
2758
+ * const ChildOf = component({ exclusive: true, sparse: true });
2759
+ * const children = world.getRelationTargets(parent, ChildOf); // usually []
2760
+ * const items = world.getRelationTargets(player, InInventory);
2761
+ *
2762
+ * // For common hierarchy use cases, prefer the higher-level helpers:
2763
+ * // world.getChildren(parent, ChildOf), world.getParent(child, ChildOf)
2764
+ */
2765
+ getRelationTargets(entityId, relationComp) {
2766
+ this.assertEntityExists(entityId, "Entity");
2767
+ const wildcard = relation(relationComp, "*");
2768
+ if (this.componentEntities.exists(entityId)) return this.componentEntities.getWildcard(entityId, wildcard);
2769
+ return this.get(entityId, wildcard);
2770
+ }
2771
+ /**
2772
+ * Returns every entity that currently holds a relation of the given base
2773
+ * component pointing at `targetId`.
2774
+ *
2775
+ * This is the efficient **reverse** lookup. For common hierarchy cases,
2776
+ * prefer the higher-level `world.getChildren(parent, ChildOf)` instead.
2777
+ *
2778
+ * @example
2779
+ * const ChildOf = component({ exclusive: true, sparse: true });
2780
+ * const directChildren = world.getRelationSources(ship, ChildOf);
2781
+ */
2782
+ getRelationSources(targetId, relationComp) {
2783
+ const refs = getEntityReferences(this.entityReferences, targetId);
2784
+ const result = [];
2785
+ for (const [source, relType] of refs) {
2786
+ if (!this.entityToArchetype.has(source) && !this.componentEntities.exists(source)) continue;
2787
+ if (getComponentIdFromRelationId(relType) === relationComp) result.push(source);
2788
+ }
2789
+ return result;
2790
+ }
2791
+ /**
2792
+ * Returns true if the entity has any (or a specific-target) relation of the
2793
+ * given base component.
2794
+ */
2795
+ hasRelation(entityId, relationComp, targetId) {
2796
+ this.assertEntityExists(entityId, "Entity");
2797
+ if (targetId !== void 0) {
2798
+ const specific = relation(relationComp, targetId);
2799
+ return this.has(entityId, specific);
2800
+ }
2801
+ return this.getRelationTargets(entityId, relationComp).length > 0;
2802
+ }
2803
+ /**
2804
+ * Returns the number of relations of the given base component held by the entity.
2805
+ */
2806
+ countRelations(entityId, relationComp) {
2807
+ this.assertEntityExists(entityId, "Entity");
2808
+ return this.getRelationTargets(entityId, relationComp).length;
2809
+ }
2810
+ /**
2811
+ * For an *exclusive* relation (e.g. ChildOf, Owner), returns the single
2812
+ * target entity (or undefined if none).
2813
+ *
2814
+ * When the component was declared `exclusive: true`, this is the preferred
2815
+ * accessor (clearer intent than array destructuring).
2816
+ */
2817
+ getSingleRelationTarget(entityId, relationComp) {
2818
+ const targets = this.getRelationTargets(entityId, relationComp);
2819
+ return targets.length > 0 ? targets[0][0] : void 0;
2820
+ }
2821
+ /**
2822
+ * Returns the direct children of `parent` for the given relationship component
2823
+ * (typically a `ChildOf` or similar exclusive `sparse` relation).
2824
+ *
2825
+ * This is the recommended high-level API for hierarchy traversal.
2826
+ * It uses the internal reverse reference index for efficiency.
2827
+ *
2828
+ * @example
2829
+ * const ChildOf = component({ exclusive: true, sparse: true });
2830
+ * const kids = world.getChildren(ship, ChildOf);
2831
+ */
2832
+ getChildren(parent, childOf) {
2833
+ return this.getRelationSources(parent, childOf);
2834
+ }
2835
+ /**
2836
+ * Returns the parent of `child` for the given relationship component
2837
+ * (typically an exclusive `ChildOf` relation).
2838
+ *
2839
+ * @example
2840
+ * const ChildOf = component({ exclusive: true, sparse: true });
2841
+ * const parent = world.getParent(turret, ChildOf);
2842
+ */
2843
+ getParent(child, childOf) {
2844
+ return this.getSingleRelationTarget(child, childOf);
2845
+ }
2846
+ /**
2847
+ * Returns the ancestor chain from the immediate parent up to (but not
2848
+ * including) the root for the given relationship component.
2849
+ *
2850
+ * @example
2851
+ * const ChildOf = component({ exclusive: true, sparse: true });
2852
+ * const ancestors = world.getAncestors(muzzle, ChildOf); // [turret, ship]
2853
+ */
2854
+ getAncestors(entity, childOf) {
2855
+ const ancestors = [];
2856
+ let cur = this.getParent(entity, childOf);
2857
+ while (cur !== void 0) {
2858
+ ancestors.push(cur);
2859
+ cur = this.getParent(cur, childOf);
2860
+ }
2861
+ return ancestors;
2862
+ }
2863
+ /**
2864
+ * Iteratively traverses all descendants of `root` in DFS pre-order.
2865
+ * This is a generator and is safe for very deep hierarchies.
2866
+ *
2867
+ * @example
2868
+ * for (const { entity, depth, parent } of world.iterateDescendants(root, ChildOf)) {
2869
+ * console.log(depth, entity);
2870
+ * }
2871
+ */
2872
+ *iterateDescendants(root, childOf, opts = {}) {
2873
+ const { includeSelf = false, maxDepth } = opts;
2874
+ const stack = [];
2875
+ if (includeSelf) stack.push({
2876
+ entity: root,
2877
+ depth: 0,
2878
+ parent: null
2879
+ });
2880
+ else for (const child of this.getChildren(root, childOf)) stack.push({
2881
+ entity: child,
2882
+ depth: 1,
2883
+ parent: root
2884
+ });
2885
+ while (stack.length > 0) {
2886
+ const current = stack.pop();
2887
+ if (maxDepth !== void 0 && current.depth > maxDepth) continue;
2888
+ yield current;
2889
+ const kids = this.getChildren(current.entity, childOf);
2890
+ for (let i = kids.length - 1; i >= 0; i--) {
2891
+ const k = kids[i];
2892
+ stack.push({
2893
+ entity: k,
2894
+ depth: current.depth + 1,
2895
+ parent: current.entity
2896
+ });
2897
+ }
2898
+ }
2899
+ }
2900
+ /**
2901
+ * Callback-based descendant traversal (hot path friendly).
2902
+ * Return `false` from the visitor to stop early.
2903
+ */
2904
+ traverseDescendants(root, childOf, visitor, opts = {}) {
2905
+ for (const { entity, depth, parent } of this.iterateDescendants(root, childOf, opts)) if (visitor(entity, depth, parent) === false) return;
2906
+ }
2719
2907
  hook(componentTypes, hook, filter) {
2720
2908
  const isCallback = typeof hook === "function";
2721
2909
  const callback = isCallback ? hook : void 0;
@@ -2751,6 +2939,74 @@ var World = class {
2751
2939
  };
2752
2940
  }
2753
2941
  /**
2942
+ * Creates a debug stats collector that will receive a `SyncDebugStats` payload
2943
+ * after every subsequent `sync()`.
2944
+ *
2945
+ * The returned object is a pure lifecycle handle. It does not store data.
2946
+ * Collection stops when you call `[Symbol.dispose]()` (or use a `using` declaration).
2947
+ *
2948
+ * All active collectors receive the exact same stats object for a given sync.
2949
+ * Exceptions thrown by callbacks are ignored.
2950
+ *
2951
+ * This is intended for development/debugging and leak detection.
2952
+ */
2953
+ createDebugStatsCollector(callback) {
2954
+ this._debugCollectors.add(callback);
2955
+ return { [Symbol.dispose]: () => {
2956
+ this._debugCollectors.delete(callback);
2957
+ } };
2958
+ }
2959
+ _resetDebugActivityCounters() {
2960
+ this._debugMigrations = 0;
2961
+ this._debugArchetypesCreated = 0;
2962
+ this._debugArchetypesRemoved = 0;
2963
+ debugHookExecutionCounter.value = 0;
2964
+ }
2965
+ _deliverDebugStats(timings) {
2966
+ const entityCount = this.entityToArchetype.size;
2967
+ let emptyArchetypes = 0;
2968
+ for (const arch of this.archetypes) if (arch.size === 0) emptyArchetypes++;
2969
+ let archetypesByComponentSize = 0;
2970
+ for (const set of this.archetypesByComponent.values()) archetypesByComponentSize += set.size;
2971
+ const stats = {
2972
+ timestamps: {
2973
+ syncStart: timings.syncStart,
2974
+ syncEnd: timings.syncEnd,
2975
+ commandBufferStart: timings.commandBufferStart,
2976
+ commandBufferEnd: timings.commandBufferEnd
2977
+ },
2978
+ commandIterations: timings.commandIterations,
2979
+ entities: {
2980
+ total: entityCount,
2981
+ freelistSize: this.entityIdManager.getFreelistSize(),
2982
+ nextId: this.entityIdManager.getNextId()
2983
+ },
2984
+ archetypes: {
2985
+ total: this.archetypes.length,
2986
+ empty: emptyArchetypes
2987
+ },
2988
+ queries: {
2989
+ cached: this.queryRegistry.cache?.size ?? 0,
2990
+ registered: this.queryRegistry.queries?.size ?? 0
2991
+ },
2992
+ hooks: { total: this.hooks.size },
2993
+ indices: {
2994
+ entityReferences: this.entityReferences.size,
2995
+ entityToReferencingArchetypes: this.entityToReferencingArchetypes.size,
2996
+ archetypesByComponent: archetypesByComponentSize
2997
+ },
2998
+ activity: {
2999
+ migrations: this._debugMigrations,
3000
+ hooksExecuted: debugHookExecutionCounter.value,
3001
+ archetypesCreated: this._debugArchetypesCreated,
3002
+ archetypesRemoved: this._debugArchetypesRemoved
3003
+ }
3004
+ };
3005
+ for (const cb of this._debugCollectors) try {
3006
+ cb(stats);
3007
+ } catch {}
3008
+ }
3009
+ /**
2754
3010
  * Synchronizes all buffered commands (set/remove/delete) to the world.
2755
3011
  * This method must be called after making changes via `set()`, `remove()`, or `delete()` for them to take effect.
2756
3012
  * Typically called once per frame at the end of your game loop.
@@ -2761,7 +3017,20 @@ var World = class {
2761
3017
  * world.sync(); // Apply all buffered changes
2762
3018
  */
2763
3019
  sync() {
2764
- this.commandBuffer.execute();
3020
+ const hasCollectors = this._debugCollectors.size > 0;
3021
+ const syncStart = hasCollectors ? performance.now() : 0;
3022
+ if (hasCollectors) this._resetDebugActivityCounters();
3023
+ const commandBufferStart = hasCollectors ? performance.now() : 0;
3024
+ const commandIterations = this.commandBuffer.execute();
3025
+ const commandBufferEnd = hasCollectors ? performance.now() : 0;
3026
+ const syncEnd = hasCollectors ? performance.now() : 0;
3027
+ if (hasCollectors) this._deliverDebugStats({
3028
+ syncStart,
3029
+ syncEnd,
3030
+ commandBufferStart,
3031
+ commandBufferEnd,
3032
+ commandIterations
3033
+ });
2765
3034
  }
2766
3035
  /**
2767
3036
  * Creates a cached query for efficiently iterating entities with specific components.
@@ -2935,13 +3204,15 @@ var World = class {
2935
3204
  });
2936
3205
  const hasStructuralChange = changeset.removes.size > 0 || changeset.adds.size > 0;
2937
3206
  if (this.hooks.size === 0) {
2938
- applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype, null);
3207
+ const newArchetype = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype, null);
2939
3208
  if (hasStructuralChange) this.updateEntityReferences(entityId, changeset);
3209
+ if (this._debugCollectors.size > 0 && newArchetype !== currentArchetype) this._debugMigrations++;
2940
3210
  return;
2941
3211
  }
2942
3212
  const removedComponents = /* @__PURE__ */ new Map();
2943
3213
  const newArchetype = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype, removedComponents);
2944
3214
  if (hasStructuralChange) this.updateEntityReferences(entityId, changeset);
3215
+ if (this._debugCollectors.size > 0 && newArchetype !== currentArchetype) this._debugMigrations++;
2945
3216
  triggerLifecycleHooks(this.createHooksContext(), entityId, changeset.adds, removedComponents, currentArchetype, newArchetype);
2946
3217
  }
2947
3218
  createHooksContext() {
@@ -3004,8 +3275,9 @@ var World = class {
3004
3275
  }
3005
3276
  }
3006
3277
  createNewArchetype(componentTypes) {
3007
- const newArchetype = new Archetype(componentTypes, this.dontFragmentStore);
3278
+ const newArchetype = new Archetype(componentTypes, this.sparseStore);
3008
3279
  this.archetypes.push(newArchetype);
3280
+ if (this._debugCollectors.size > 0) this._debugArchetypesCreated++;
3009
3281
  for (const componentType of componentTypes) {
3010
3282
  let archetypes = this.archetypesByComponent.get(componentType);
3011
3283
  if (!archetypes) {
@@ -3028,11 +3300,11 @@ var World = class {
3028
3300
  archetypeMatchesHook(archetype, entry) {
3029
3301
  return entry.requiredComponents.every((c) => {
3030
3302
  if (isWildcardRelationId(c)) {
3031
- if (isDontFragmentWildcard(c)) return true;
3303
+ if (isSparseWildcard(c)) return true;
3032
3304
  const componentId = getComponentIdFromRelationId(c);
3033
3305
  return componentId !== void 0 && archetype.hasRelationWithComponentId(componentId);
3034
3306
  }
3035
- return archetype.componentTypeSet.has(c) || isDontFragmentRelation(c);
3307
+ return archetype.componentTypeSet.has(c) || isSparseRelation(c);
3036
3308
  }) && matchesFilter(archetype, entry.filter);
3037
3309
  }
3038
3310
  cleanupArchetypesReferencingEntity(entityId) {
@@ -3048,6 +3320,7 @@ var World = class {
3048
3320
  this.archetypes[index] = last;
3049
3321
  this.archetypes.pop();
3050
3322
  }
3323
+ if (this._debugCollectors.size > 0) this._debugArchetypesRemoved++;
3051
3324
  this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
3052
3325
  for (const componentType of archetype.componentTypes) {
3053
3326
  const archetypes = this.archetypesByComponent.get(componentType);
@@ -3085,6 +3358,6 @@ var World = class {
3085
3358
  }
3086
3359
  };
3087
3360
  //#endregion
3088
- export { getComponentIdByName as a, isWildcardRelationId as c, isEntityId as d, isRelationId as f, component as i, relation as l, Query as n, getComponentNameById as o, EntityBuilder as r, decodeRelationId as s, World as t, isComponentId as u };
3361
+ export { getComponentIdByName as a, isSparseRelation as c, isWildcardRelationId as d, relation as f, isRelationId as h, component as i, isSparseWildcard as l, isEntityId as m, Query as n, getComponentNameById as o, isComponentId as p, EntityBuilder as r, isSparseComponent as s, World as t, decodeRelationId as u };
3089
3362
 
3090
3363
  //# sourceMappingURL=world.mjs.map