@codehz/ecs 0.8.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.en.md +26 -3
  2. package/README.md +41 -4
  3. package/dist/builder.d.mts +348 -83
  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 +1922 -1400
  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/examples/spatial-grid.ts +1 -1
  14. package/package.json +1 -1
  15. package/skills/ecs/SKILL.md +4 -4
  16. package/src/__tests__/component/singleton.test.ts +116 -35
  17. package/src/__tests__/core/archetype.test.ts +155 -13
  18. package/src/__tests__/core/bitset.test.ts +12 -0
  19. package/src/__tests__/entity/entity.test.ts +33 -0
  20. package/src/__tests__/entity/id-system.test.ts +40 -0
  21. package/src/__tests__/perf/comprehensive.perf.test.ts +6 -9
  22. package/src/__tests__/perf/serialization.perf.test.ts +242 -0
  23. package/src/__tests__/perf/{dontfragment-wildcard.perf.test.ts → sparse-wildcard.perf.test.ts} +13 -16
  24. package/src/__tests__/query/caching.test.ts +62 -0
  25. package/src/__tests__/query/filter.test.ts +16 -22
  26. package/src/__tests__/query/perf.test.ts +3 -5
  27. package/src/__tests__/relations/hierarchy.test.ts +208 -0
  28. package/src/__tests__/relations/{dont-fragment → sparse}/basic.test.ts +64 -69
  29. package/src/__tests__/relations/{dont-fragment → sparse}/query-notification.test.ts +17 -9
  30. package/src/__tests__/serialization/bounds.test.ts +133 -1
  31. package/src/__tests__/world/commands.test.ts +337 -0
  32. package/src/__tests__/world/component-management.test.ts +6 -5
  33. package/src/__tests__/world/debug-stats.test.ts +206 -0
  34. package/src/__tests__/world/multi-component-hooks.test.ts +44 -0
  35. package/src/__tests__/world/serialize.test.ts +17 -0
  36. package/src/__tests__/world/wildcard-relation-hooks.test.ts +127 -0
  37. package/src/archetype/archetype.ts +96 -46
  38. package/src/archetype/helpers.ts +7 -29
  39. package/src/archetype/store.ts +35 -20
  40. package/src/commands/buffer.ts +5 -2
  41. package/src/commands/changeset.ts +0 -31
  42. package/src/component/registry.ts +64 -63
  43. package/src/entity/index.ts +6 -3
  44. package/src/index.ts +15 -0
  45. package/src/query/filter.ts +4 -10
  46. package/src/query/query.ts +12 -12
  47. package/src/storage/serialization.ts +29 -2
  48. package/src/types/index.ts +71 -0
  49. package/src/world/archetype-manager.ts +283 -0
  50. package/src/world/command-executor.ts +258 -0
  51. package/src/world/commands.ts +44 -56
  52. package/src/world/debug-stats.ts +147 -0
  53. package/src/world/hooks.ts +8 -0
  54. package/src/world/operations.ts +88 -0
  55. package/src/world/serialization.ts +32 -18
  56. package/src/world/singleton.ts +51 -0
  57. package/src/world/world.ts +429 -457
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.
@@ -634,497 +622,125 @@ var EntityBuilder = class {
634
622
  }
635
623
  };
636
624
  //#endregion
637
- //#region src/component/type-utils.ts
638
- /**
639
- * Normalize component type collections into a stable ascending order.
640
- * This keeps cache keys and archetype signatures deterministic.
641
- */
642
- function normalizeComponentTypes(componentTypes) {
643
- return [...componentTypes].sort((a, b) => a - b);
644
- }
645
- //#endregion
646
- //#region src/types/index.ts
647
- function isOptionalEntityId(type) {
648
- return typeof type === "object" && type !== null && "optional" in type;
649
- }
650
- //#endregion
651
- //#region src/utils/utils.ts
652
- /**
653
- * Utility functions for ECS library
654
- */
625
+ //#region src/world/singleton.ts
655
626
  /**
656
- * Get a value from cache or compute and cache it if not present
657
- * @param cache The cache map
658
- * @param key The cache key
659
- * @param compute Function to compute the value if not cached (may have side effects)
660
- * @returns The cached or computed value
627
+ * Explicit handle for a singleton component (component-as-entity).
628
+ *
629
+ * This provides an explicit and concise API for singleton components without
630
+ * overloading `world.set()` semantics.
631
+ *
632
+ * @example
633
+ * const config = world.singleton(Config);
634
+ * config.set({ debug: true });
635
+ * world.sync();
636
+ * console.log(config.get());
661
637
  */
662
- function getOrCompute(cache, key, compute) {
663
- let value = cache.get(key);
664
- if (value === void 0) {
665
- value = compute();
666
- cache.set(key, value);
638
+ var SingletonHandle = class {
639
+ componentId;
640
+ ops;
641
+ constructor(componentId, ops) {
642
+ this.componentId = componentId;
643
+ this.ops = ops;
667
644
  }
668
- return value;
669
- }
670
- //#endregion
671
- //#region src/archetype/helpers.ts
672
- /**
673
- * Check if a components map has any wildcard relations matching a component ID
674
- * @param components - Component entity's components map
675
- * @param wildcardComponentId - The component ID to match
676
- * @returns True if at least one matching relation exists
677
- */
678
- function hasWildcardRelation(components, wildcardComponentId) {
679
- for (const relId of components.keys()) if (isRelationId(relId)) {
680
- if (getComponentIdFromRelationId(relId) === wildcardComponentId) return true;
645
+ has() {
646
+ return this.ops.has();
681
647
  }
682
- return false;
683
- }
684
- /**
685
- * Check if a detailed type represents a relation (entity or component)
686
- */
687
- function isRelationType(detailedType) {
688
- return detailedType.type === "entity-relation" || detailedType.type === "component-relation";
689
- }
690
- /**
691
- * Check if a component type matches a given component ID for relations
692
- */
693
- function matchesRelationComponentId(componentType, componentId) {
694
- const detailedType = getDetailedIdType(componentType);
695
- return isRelationType(detailedType) && detailedType.componentId === componentId;
696
- }
697
- /**
698
- * Build cache key for component types
699
- */
700
- function buildCacheKey(componentTypes) {
701
- return componentTypes.map((id) => isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`).join(",");
702
- }
703
- /**
704
- * Get data source for wildcard relations from component types
705
- */
706
- function getWildcardRelationDataSource(componentTypes, componentId, optional) {
707
- const matchingRelations = componentTypes.filter((ct) => matchesRelationComponentId(ct, componentId));
708
- return optional ? matchingRelations.length > 0 ? matchingRelations : void 0 : matchingRelations;
709
- }
710
- /**
711
- * Build wildcard relation value from matching relations.
712
- * Now receives the DontFragmentStore directly for efficient per-component lookups.
713
- */
714
- function buildWildcardRelationValue(wildcardRelationType, matchingRelations, getDataAtIndex, dontFragmentStore, entityId, optional) {
715
- const relations = [];
716
- const targetComponentId = getComponentIdFromRelationId(wildcardRelationType);
717
- for (const relType of matchingRelations || []) {
718
- const data = getDataAtIndex(relType);
719
- const targetId = getTargetIdFromRelationId(relType);
720
- relations.push([targetId, data === MISSING_COMPONENT ? void 0 : data]);
648
+ get() {
649
+ return this.ops.get();
721
650
  }
722
- if (targetComponentId !== void 0) {
723
- const dfMatches = dontFragmentStore.getRelationsForComponent(entityId, targetComponentId);
724
- for (const m of dfMatches) relations.push(m);
651
+ getOptional() {
652
+ return this.ops.getOptional();
725
653
  }
726
- if (relations.length === 0) {
727
- if (!optional) {
728
- const componentId = getComponentIdFromRelationId(wildcardRelationType);
729
- throw new Error(`No matching relations found for mandatory wildcard relation component ${componentId} on entity ${entityId}`);
730
- }
731
- return;
654
+ remove() {
655
+ this.ops.remove();
732
656
  }
733
- return optional ? { value: relations } : relations;
734
- }
735
- /**
736
- * Build regular component value from data source
737
- */
738
- function buildRegularComponentValue(dataSource, entityIndex, optional) {
739
- if (dataSource === void 0) {
740
- if (optional) return void 0;
741
- throw new Error(`Component data not found for mandatory component type`);
657
+ set(...args) {
658
+ this.ops.set(args[0]);
742
659
  }
743
- const data = dataSource[entityIndex];
744
- const result = data === MISSING_COMPONENT ? void 0 : data;
745
- return optional ? { value: result } : result;
746
- }
747
- /**
748
- * Build a single component value based on its type
749
- */
750
- function buildSingleComponent(compType, dataSource, entityIndex, entityId, getComponentData, dontFragmentRelations) {
751
- const optional = isOptionalEntityId(compType);
752
- const actualType = optional ? compType.optional : compType;
753
- if (getIdType(actualType) === "wildcard-relation") return buildWildcardRelationValue(actualType, dataSource, (relType) => getComponentData(relType)[entityIndex], dontFragmentRelations, entityId, optional);
754
- else return buildRegularComponentValue(dataSource, entityIndex, optional);
755
- }
660
+ };
756
661
  //#endregion
757
- //#region src/archetype/archetype.ts
758
- /**
759
- * Special value to represent missing component data
760
- */
761
- const MISSING_COMPONENT = Symbol("missing component");
662
+ //#region src/archetype/store.ts
762
663
  /**
763
- * Archetype class for ECS architecture
764
- * Represents a group of entities that share the same set of components
765
- * Optimized for fast iteration and component access
664
+ * Production implementation of SparseStore.
665
+ *
666
+ * Internal layout (optimized):
667
+ * - byComponent: baseComponentId → (entityId → RelationEntry)
668
+ * RelationEntry uses a single-value form for the common exclusive case (1 target),
669
+ * avoiding Map allocation for the vast majority of usage.
670
+ * - entityIndex: entityId → Set<baseComponentId>
671
+ * Lightweight reverse index.
766
672
  */
767
- var Archetype = class {
768
- /**
769
- * The component types that define this archetype
770
- */
771
- componentTypes;
772
- /**
773
- * Set version of componentTypes for O(1) lookups in hot paths
774
- */
775
- componentTypeSet;
776
- /**
777
- * List of entities in this archetype
778
- */
779
- entities = [];
780
- /**
781
- * Component data storage - maps component type to array of component data
782
- * Each array index corresponds to the entity index in the entities array
783
- */
784
- componentData = /* @__PURE__ */ new Map();
785
- /**
786
- * Reverse mapping from entity to its index in this archetype
787
- */
788
- entityToIndex = /* @__PURE__ */ new Map();
789
- /**
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.
793
- */
794
- dontFragmentRelations;
795
- /**
796
- * Multi-hooks that match this archetype
797
- */
798
- matchingMultiHooks = /* @__PURE__ */ new Set();
673
+ var SparseStoreImpl = class {
799
674
  /**
800
- * Cache for pre-computed component data sources to avoid repeated calculations
675
+ * Primary storage, keyed by the base relation component ID.
801
676
  */
802
- componentDataSourcesCache = /* @__PURE__ */ new Map();
803
- constructor(componentTypes, dontFragmentRelations) {
804
- this.componentTypes = normalizeComponentTypes(componentTypes);
805
- this.componentTypeSet = new Set(this.componentTypes);
806
- this.dontFragmentRelations = dontFragmentRelations;
807
- for (const componentType of this.componentTypes) this.componentData.set(componentType, []);
808
- }
809
- get size() {
810
- return this.entities.length;
811
- }
677
+ byComponent = /* @__PURE__ */ new Map();
812
678
  /**
813
- * Check if the given component types match this archetype
814
- * @param componentTypes - Component types to check (can be in any order)
815
- * @returns true if the types match this archetype's component set
816
- * @note This method handles unsorted input by internally sorting for comparison
679
+ * Reverse index: which base component kinds an entity participates in.
680
+ * Only required to support getAllForEntity and deleteEntity efficiently.
681
+ * The primary storage (byComponent) is deliberately not optimized for these operations.
817
682
  */
818
- matches(componentTypes) {
819
- if (this.componentTypes.length !== componentTypes.length) return false;
820
- const sortedTypes = normalizeComponentTypes(componentTypes);
821
- return this.componentTypes.every((type, index) => type === sortedTypes[index]);
822
- }
823
- addEntity(entityId, componentData) {
824
- if (this.entityToIndex.has(entityId)) throw new Error(`Entity ${entityId} is already in this archetype`);
825
- const index = this.entities.length;
826
- this.entities.push(entityId);
827
- this.entityToIndex.set(entityId, index);
828
- for (const componentType of this.componentTypes) {
829
- const data = componentData.get(componentType);
830
- this.getComponentData(componentType).push(!componentData.has(componentType) ? MISSING_COMPONENT : data);
683
+ entityIndex = /* @__PURE__ */ new Map();
684
+ getValue(entityId, relationType) {
685
+ const componentId = getComponentIdFromRelationId(relationType);
686
+ if (componentId === void 0) return void 0;
687
+ const entities = this.byComponent.get(componentId);
688
+ if (!entities) return void 0;
689
+ const entry = entities.get(entityId);
690
+ if (!entry) return void 0;
691
+ const targetId = getTargetIdFromRelationId(relationType);
692
+ if (entry.type === "single") return entry.target === targetId ? entry.data : void 0;
693
+ else {
694
+ const item = entry.targets.get(targetId);
695
+ return item ? item.data : void 0;
831
696
  }
832
- this.addDontFragmentRelations(entityId, componentData);
833
697
  }
834
- addDontFragmentRelations(entityId, componentData) {
835
- for (const [componentType, data] of componentData) {
836
- if (this.componentTypeSet.has(componentType)) continue;
837
- const detailedType = getDetailedIdType(componentType);
838
- if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId)) this.dontFragmentRelations.setValue(entityId, componentType, data);
698
+ setValue(entityId, relationType, data) {
699
+ const componentId = getComponentIdFromRelationId(relationType);
700
+ if (componentId === void 0) throw new Error("setValue called with a non-relation type on SparseStore");
701
+ let entities = this.byComponent.get(componentId);
702
+ if (!entities) {
703
+ entities = /* @__PURE__ */ new Map();
704
+ this.byComponent.set(componentId, entities);
839
705
  }
840
- }
841
- getEntity(entityId) {
842
- const index = this.entityToIndex.get(entityId);
843
- if (index === void 0) return void 0;
844
- const entityData = /* @__PURE__ */ new Map();
845
- for (const componentType of this.componentTypes) {
846
- const data = this.getComponentData(componentType)[index];
847
- entityData.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
706
+ const targetId = getTargetIdFromRelationId(relationType);
707
+ let entry = entities.get(entityId);
708
+ if (!entry) {
709
+ entry = {
710
+ type: "single",
711
+ relationType,
712
+ target: targetId,
713
+ data
714
+ };
715
+ entities.set(entityId, entry);
716
+ } else if (entry.type === "single") if (entry.target === targetId) {
717
+ entry.data = data;
718
+ entry.relationType = relationType;
719
+ } else {
720
+ const targets = /* @__PURE__ */ new Map();
721
+ targets.set(entry.target, {
722
+ relationType: entry.relationType,
723
+ data: entry.data
724
+ });
725
+ targets.set(targetId, {
726
+ relationType,
727
+ data
728
+ });
729
+ entities.set(entityId, {
730
+ type: "multi",
731
+ targets
732
+ });
848
733
  }
849
- const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entityId);
850
- for (const [componentType, data] of dontFragmentTuples) entityData.set(componentType, data);
851
- return entityData;
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
- */
859
- getEntityDontFragmentRelations(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;
865
- }
866
- dump() {
867
- return this.entities.map((entity, i) => {
868
- const components = /* @__PURE__ */ new Map();
869
- for (const componentType of this.componentTypes) {
870
- const data = this.getComponentData(componentType)[i];
871
- components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
872
- }
873
- const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entity);
874
- for (const [componentType, data] of dontFragmentTuples) components.set(componentType, data);
875
- return {
876
- entity,
877
- components
878
- };
879
- });
880
- }
881
- removeEntity(entityId) {
882
- const index = this.entityToIndex.get(entityId);
883
- if (index === void 0) return void 0;
884
- const removedData = /* @__PURE__ */ new Map();
885
- 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);
889
- this.entityToIndex.delete(entityId);
890
- const lastIndex = this.entities.length - 1;
891
- if (index !== lastIndex) {
892
- const lastEntity = this.entities[lastIndex];
893
- this.entities[index] = lastEntity;
894
- this.entityToIndex.set(lastEntity, index);
895
- for (const componentType of this.componentTypes) {
896
- const dataArray = this.getComponentData(componentType);
897
- dataArray[index] = dataArray[lastIndex];
898
- }
899
- }
900
- this.entities.pop();
901
- for (const componentType of this.componentTypes) this.getComponentData(componentType).pop();
902
- return removedData;
903
- }
904
- exists(entityId) {
905
- return this.entityToIndex.has(entityId);
906
- }
907
- get(entityId, componentType) {
908
- const index = this.entityToIndex.get(entityId);
909
- if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
910
- if (isWildcardRelationId(componentType)) return this.getWildcardRelations(entityId, index, componentType);
911
- return this.getRegularComponent(entityId, index, componentType);
912
- }
913
- getWildcardRelations(entityId, index, componentType) {
914
- const componentId = getComponentIdFromRelationId(componentType);
915
- const relations = [];
916
- for (const relType of this.componentTypes) {
917
- const relDetailed = getDetailedIdType(relType);
918
- if (isRelationType(relDetailed) && relDetailed.componentId === componentId) {
919
- const dataArray = this.getComponentData(relType);
920
- if (dataArray && dataArray[index] !== void 0) {
921
- const data = dataArray[index];
922
- relations.push([relDetailed.targetId, data === MISSING_COMPONENT ? void 0 : data]);
923
- }
924
- }
925
- }
926
- if (componentId !== void 0) {
927
- const matches = this.dontFragmentRelations.getRelationsForComponent(entityId, componentId);
928
- for (const m of matches) relations.push(m);
929
- }
930
- return relations;
931
- }
932
- getRegularComponent(entityId, index, componentType) {
933
- if (this.componentTypeSet.has(componentType)) {
934
- const data = this.getComponentData(componentType)[index];
935
- if (data === MISSING_COMPONENT) throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
936
- return data;
937
- }
938
- if (this.dontFragmentRelations.getValue(entityId, componentType) !== void 0 || this.dontFragmentRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) return this.dontFragmentRelations.getValue(entityId, componentType);
939
- throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
940
- }
941
- getOptional(entityId, componentType) {
942
- const index = this.entityToIndex.get(entityId);
943
- if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
944
- if (this.componentTypeSet.has(componentType)) {
945
- const data = this.getComponentData(componentType)[index];
946
- if (data === MISSING_COMPONENT) return void 0;
947
- return { value: data };
948
- }
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) };
952
- }
953
- set(entityId, componentType, data) {
954
- const index = this.entityToIndex.get(entityId);
955
- if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
956
- if (this.componentData.has(componentType)) {
957
- this.getComponentData(componentType)[index] = data;
958
- return;
959
- }
960
- const detailedType = getDetailedIdType(componentType);
961
- if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId)) {
962
- this.dontFragmentRelations.setValue(entityId, componentType, data);
963
- return;
964
- }
965
- throw new Error(`Component type ${componentType} is not in this archetype`);
966
- }
967
- getEntities() {
968
- return this.entities;
969
- }
970
- getEntityToIndexMap() {
971
- return this.entityToIndex;
972
- }
973
- getComponentData(componentType) {
974
- const data = this.componentData.get(componentType);
975
- if (!data) throw new Error(`Component type ${componentType} is not in this archetype`);
976
- return data;
977
- }
978
- getOptionalComponentData(componentType) {
979
- return this.componentData.get(componentType);
980
- }
981
- getCachedComponentDataSources(componentTypes) {
982
- const cacheKey = buildCacheKey(componentTypes);
983
- return getOrCompute(this.componentDataSourcesCache, cacheKey, () => componentTypes.map((compType) => this.getComponentDataSource(compType)));
984
- }
985
- getComponentDataSource(compType) {
986
- const optional = isOptionalEntityId(compType);
987
- const actualType = optional ? compType.optional : compType;
988
- if (getIdType(actualType) === "wildcard-relation") {
989
- const componentId = getComponentIdFromRelationId(actualType);
990
- return getWildcardRelationDataSource(this.componentTypes, componentId, optional);
991
- }
992
- return optional ? this.getOptionalComponentData(actualType) : this.getComponentData(actualType);
993
- }
994
- buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entityId) {
995
- return componentDataSources.map((dataSource, i) => buildSingleComponent(componentTypes[i], dataSource, entityIndex, entityId, (type) => this.getComponentData(type), this.dontFragmentRelations));
996
- }
997
- getEntitiesWithComponents(componentTypes) {
998
- const result = [];
999
- this.appendEntitiesWithComponents(componentTypes, result);
1000
- return result;
1001
- }
1002
- appendEntitiesWithComponents(componentTypes, result) {
1003
- this.forEachWithComponents(componentTypes, (entity, ...components) => {
1004
- result.push({
1005
- entity,
1006
- components
1007
- });
1008
- });
1009
- }
1010
- *iterateWithComponents(componentTypes) {
1011
- const componentDataSources = this.getCachedComponentDataSources(componentTypes);
1012
- for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
1013
- const entity = this.entities[entityIndex];
1014
- yield [entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity)];
1015
- }
1016
- }
1017
- forEachWithComponents(componentTypes, callback) {
1018
- const componentDataSources = this.getCachedComponentDataSources(componentTypes);
1019
- for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
1020
- const entity = this.entities[entityIndex];
1021
- callback(entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity));
1022
- }
1023
- }
1024
- forEach(callback) {
1025
- for (let i = 0; i < this.entities.length; i++) {
1026
- const entity = this.entities[i];
1027
- const components = /* @__PURE__ */ new Map();
1028
- for (const componentType of this.componentTypes) {
1029
- const data = this.getComponentData(componentType)[i];
1030
- components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
1031
- }
1032
- const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entity);
1033
- for (const [componentType, data] of dontFragmentTuples) components.set(componentType, data);
1034
- callback(entity, components);
1035
- }
1036
- }
1037
- hasRelationWithComponentId(componentId) {
1038
- for (const componentType of this.componentTypes) {
1039
- const detailedType = getDetailedIdType(componentType);
1040
- if (isRelationType(detailedType) && detailedType.componentId === componentId) return true;
1041
- }
1042
- for (const entityId of this.entities) if (this.dontFragmentRelations.getRelationsForComponent(entityId, componentId).length > 0) return true;
1043
- return false;
1044
- }
1045
- };
1046
- //#endregion
1047
- //#region src/archetype/store.ts
1048
- /**
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
- */
1058
- var DontFragmentStoreImpl = class {
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);
734
+ else entry.targets.set(targetId, {
735
+ relationType,
736
+ data
737
+ });
738
+ let components = this.entityIndex.get(entityId);
739
+ if (!components) {
740
+ components = /* @__PURE__ */ new Set();
741
+ this.entityIndex.set(entityId, components);
742
+ }
743
+ components.add(componentId);
1128
744
  }
1129
745
  deleteValue(entityId, relationType) {
1130
746
  const componentId = getComponentIdFromRelationId(relationType);
@@ -1199,6 +815,14 @@ var DontFragmentStoreImpl = class {
1199
815
  }
1200
816
  this.entityIndex.delete(entityId);
1201
817
  }
818
+ getAllForEntities(entityIds) {
819
+ const result = /* @__PURE__ */ new Map();
820
+ for (const eid of entityIds) {
821
+ const data = this.getAllForEntity(eid);
822
+ if (data.length > 0) result.set(eid, data);
823
+ }
824
+ return result;
825
+ }
1202
826
  };
1203
827
  //#endregion
1204
828
  //#region src/commands/buffer.ts
@@ -1249,7 +873,8 @@ var CommandBuffer = class {
1249
873
  });
1250
874
  }
1251
875
  /**
1252
- * Execute all commands and clear the buffer
876
+ * Execute all commands and clear the buffer.
877
+ * Returns the number of iterations performed (for debug stats).
1253
878
  */
1254
879
  execute() {
1255
880
  let iterations = 0;
@@ -1269,6 +894,7 @@ var CommandBuffer = class {
1269
894
  for (const [entityId, commands] of entityCommands) this.executeEntityCommands(entityId, commands);
1270
895
  entityCommands.clear();
1271
896
  }
897
+ return iterations;
1272
898
  }
1273
899
  /**
1274
900
  * Get current commands (for testing)
@@ -1284,727 +910,1315 @@ var CommandBuffer = class {
1284
910
  }
1285
911
  };
1286
912
  //#endregion
1287
- //#region src/commands/changeset.ts
1288
- /**
1289
- * @internal Represents a set of component changes to be applied to an entity
1290
- */
1291
- var ComponentChangeset = class {
1292
- adds = /* @__PURE__ */ new Map();
1293
- removes = /* @__PURE__ */ new Set();
1294
- /**
1295
- * Add a component to the changeset
1296
- */
1297
- set(componentType, component) {
1298
- this.adds.set(componentType, component);
1299
- this.removes.delete(componentType);
913
+ //#region src/types/index.ts
914
+ function isOptionalEntityId(type) {
915
+ return typeof type === "object" && type !== null && "optional" in type;
916
+ }
917
+ //#endregion
918
+ //#region src/component/type-utils.ts
919
+ /**
920
+ * Normalize component type collections into a stable ascending order.
921
+ * This keeps cache keys and archetype signatures deterministic.
922
+ */
923
+ function normalizeComponentTypes(componentTypes) {
924
+ return [...componentTypes].sort((a, b) => a - b);
925
+ }
926
+ //#endregion
927
+ //#region src/utils/utils.ts
928
+ /**
929
+ * Utility functions for ECS library
930
+ */
931
+ /**
932
+ * Get a value from cache or compute and cache it if not present
933
+ * @param cache The cache map
934
+ * @param key The cache key
935
+ * @param compute Function to compute the value if not cached (may have side effects)
936
+ * @returns The cached or computed value
937
+ */
938
+ function getOrCompute(cache, key, compute) {
939
+ let value = cache.get(key);
940
+ if (value === void 0) {
941
+ value = compute();
942
+ cache.set(key, value);
1300
943
  }
944
+ return value;
945
+ }
946
+ //#endregion
947
+ //#region src/archetype/archetype.ts
948
+ /**
949
+ * Special value to represent missing component data
950
+ */
951
+ const MISSING_COMPONENT = Symbol("missing component");
952
+ /**
953
+ * Archetype class for ECS architecture
954
+ * Represents a group of entities that share the same set of components
955
+ * Optimized for fast iteration and component access
956
+ */
957
+ var Archetype = class {
1301
958
  /**
1302
- * Remove a component from the changeset
959
+ * The component types that define this archetype
1303
960
  */
1304
- delete(componentType) {
1305
- this.removes.add(componentType);
1306
- this.adds.delete(componentType);
1307
- }
961
+ componentTypes;
1308
962
  /**
1309
- * Check if the changeset has any changes
963
+ * Set version of componentTypes for O(1) lookups in hot paths
1310
964
  */
1311
- hasChanges() {
1312
- return this.adds.size > 0 || this.removes.size > 0;
1313
- }
965
+ componentTypeSet;
1314
966
  /**
1315
- * Clear all changes
967
+ * List of entities in this archetype
1316
968
  */
1317
- clear() {
1318
- this.adds.clear();
1319
- this.removes.clear();
1320
- }
969
+ entities = [];
1321
970
  /**
1322
- * Merge another changeset into this one
971
+ * Component data storage - maps component type to array of component data
972
+ * Each array index corresponds to the entity index in the entities array
1323
973
  */
1324
- merge(other) {
1325
- for (const [componentType, component] of other.adds) {
1326
- this.adds.set(componentType, component);
1327
- this.removes.delete(componentType);
1328
- }
1329
- for (const componentType of other.removes) {
1330
- this.removes.add(componentType);
1331
- this.adds.delete(componentType);
1332
- }
1333
- }
974
+ componentData = /* @__PURE__ */ new Map();
1334
975
  /**
1335
- * Apply the changeset to existing components and return the final state
976
+ * Reverse mapping from entity to its index in this archetype
1336
977
  */
1337
- applyTo(existingComponents) {
1338
- for (const componentType of this.removes) existingComponents.delete(componentType);
1339
- for (const [componentType, component] of this.adds) existingComponents.set(componentType, component);
1340
- return existingComponents;
1341
- }
978
+ entityToIndex = /* @__PURE__ */ new Map();
1342
979
  /**
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
980
+ * SparseStore used for relations declared with `sparse: true`.
981
+ * See store.ts for implementation details.
1346
982
  */
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
- };
1366
- //#endregion
1367
- //#region src/component/entity-store.ts
1368
- /**
1369
- * Manages component entity (singleton) storage.
1370
- *
1371
- * Component entities use a flat Map-based storage rather than the Archetype-based
1372
- * storage used for regular entities. Their IDs are in the component ID range
1373
- * (or are relation IDs), distinguishing them from regular entity IDs.
1374
- */
1375
- var ComponentEntityStore = class {
1376
- componentEntityComponents = /* @__PURE__ */ new Map();
1377
- relationEntityIdsByTarget = /* @__PURE__ */ new Map();
983
+ sparseRelations;
1378
984
  /**
1379
- * Check if an entity ID is a component entity type.
1380
- * Returns true for component IDs, component-relation IDs, and entity-relation IDs —
1381
- * i.e. anything that is NOT a plain entity or an invalid ID.
985
+ * Multi-hooks that match this archetype
1382
986
  */
1383
- exists(entityId) {
1384
- const detailed = getDetailedIdType(entityId);
1385
- return detailed.type !== "entity" && detailed.type !== "invalid";
1386
- }
987
+ matchingMultiHooks = /* @__PURE__ */ new Set();
1387
988
  /**
1388
- * Check if a component entity has a specific component.
989
+ * Cache for pre-computed component data sources to avoid repeated calculations
1389
990
  */
1390
- has(entityId, componentType) {
1391
- return this.componentEntityComponents.get(entityId)?.has(componentType) ?? false;
991
+ componentDataSourcesCache = /* @__PURE__ */ new Map();
992
+ constructor(componentTypes, sparseStore) {
993
+ this.componentTypes = normalizeComponentTypes(componentTypes);
994
+ this.componentTypeSet = new Set(this.componentTypes);
995
+ this.sparseRelations = sparseStore;
996
+ for (const componentType of this.componentTypes) this.componentData.set(componentType, []);
1392
997
  }
1393
- /**
1394
- * Check if a singleton component has data — the has(componentId) overload.
1395
- * In singleton usage the entity ID and the component type are the same value.
1396
- */
1397
- hasSingleton(componentId) {
1398
- return this.componentEntityComponents.get(componentId)?.has(componentId) ?? false;
998
+ get size() {
999
+ return this.entities.length;
1399
1000
  }
1400
1001
  /**
1401
- * Check if a component entity has any wildcard relations matching a component ID.
1002
+ * Check if the given component types match this archetype
1003
+ * @param componentTypes - Component types to check (can be in any order)
1004
+ * @returns true if the types match this archetype's component set
1005
+ * @note This method handles unsorted input by internally sorting for comparison
1402
1006
  */
1403
- hasWildcard(entityId, componentId) {
1404
- const data = this.componentEntityComponents.get(entityId);
1405
- if (!data) return false;
1406
- return hasWildcardRelation(data, componentId);
1007
+ matches(componentTypes) {
1008
+ if (this.componentTypes.length !== componentTypes.length) return false;
1009
+ const sortedTypes = normalizeComponentTypes(componentTypes);
1010
+ return this.componentTypes.every((type, index) => type === sortedTypes[index]);
1407
1011
  }
1408
- /**
1409
- * Get a component value from a component entity.
1410
- * Throws if the component does not exist.
1411
- */
1412
- get(entityId, componentType) {
1413
- const data = this.componentEntityComponents.get(entityId);
1414
- if (!data || !data.has(componentType)) throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
1415
- return data.get(componentType);
1012
+ addEntity(entityId, componentData) {
1013
+ if (this.entityToIndex.has(entityId)) throw new Error(`Entity ${entityId} is already in this archetype`);
1014
+ const index = this.entities.length;
1015
+ this.entities.push(entityId);
1016
+ this.entityToIndex.set(entityId, index);
1017
+ for (const componentType of this.componentTypes) {
1018
+ const data = componentData.get(componentType);
1019
+ this.getComponentData(componentType).push(!componentData.has(componentType) ? MISSING_COMPONENT : data);
1020
+ }
1021
+ this.addSparseRelations(entityId, componentData);
1416
1022
  }
1417
- /**
1418
- * Get an optional component value from a component entity.
1419
- * Returns undefined if the component does not exist.
1420
- */
1421
- getOptional(entityId, componentType) {
1422
- const data = this.componentEntityComponents.get(entityId);
1423
- if (!data || !data.has(componentType)) return void 0;
1424
- return { value: data.get(componentType) };
1023
+ addSparseRelations(entityId, componentData) {
1024
+ for (const [componentType, data] of componentData) {
1025
+ if (this.componentTypeSet.has(componentType)) continue;
1026
+ const detailedType = getDetailedIdType(componentType);
1027
+ if (isRelationType(detailedType) && isSparseComponent(detailedType.componentId)) this.sparseRelations.setValue(entityId, componentType, data);
1028
+ }
1425
1029
  }
1426
- /**
1427
- * Get all wildcard relations of a given type from a component entity.
1428
- */
1429
- getWildcard(entityId, wildcardComponentType) {
1430
- const componentId = getComponentIdFromRelationId(wildcardComponentType);
1431
- const data = this.componentEntityComponents.get(entityId);
1432
- if (componentId === void 0 || !data) return [];
1433
- const relations = [];
1434
- for (const [key, value] of data.entries()) {
1435
- if (getComponentIdFromRelationId(key) !== componentId) continue;
1436
- const detailed = getDetailedIdType(key);
1437
- if (detailed.type === "entity-relation" || detailed.type === "component-relation") relations.push([detailed.targetId, value]);
1030
+ getEntity(entityId) {
1031
+ const index = this.entityToIndex.get(entityId);
1032
+ if (index === void 0) return void 0;
1033
+ const entityData = /* @__PURE__ */ new Map();
1034
+ for (const componentType of this.componentTypes) {
1035
+ const data = this.getComponentData(componentType)[index];
1036
+ entityData.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
1438
1037
  }
1439
- return relations;
1038
+ const sparseTuples = this.sparseRelations.getAllForEntity(entityId);
1039
+ for (const [componentType, data] of sparseTuples) entityData.set(componentType, data);
1040
+ return entityData;
1440
1041
  }
1441
1042
  /**
1442
- * Clear all data for a component entity.
1043
+ * Returns all sparse-stored relations for the given entity.
1044
+ * Internal helper used by command processing and tests.
1443
1045
  */
1444
- clear(entityId) {
1445
- if (this.componentEntityComponents.delete(entityId)) this.unregisterRelationEntityId(entityId);
1046
+ getEntitySparseRelations(entityId) {
1047
+ const tuples = this.sparseRelations.getAllForEntity(entityId);
1048
+ if (tuples.length === 0) return void 0;
1049
+ const map = /* @__PURE__ */ new Map();
1050
+ for (const [relType, data] of tuples) map.set(relType, data);
1051
+ return map;
1446
1052
  }
1447
- /**
1448
- * Cleanup all component entities that reference a given target entity.
1449
- * Called when a target entity is destroyed.
1450
- */
1451
- cleanupReferencesTo(targetId) {
1452
- const relationEntities = this.relationEntityIdsByTarget.get(targetId);
1453
- if (!relationEntities) return;
1454
- for (const relationEntityId of relationEntities) this.componentEntityComponents.delete(relationEntityId);
1455
- this.relationEntityIdsByTarget.delete(targetId);
1053
+ dump() {
1054
+ return this.entities.map((entity, i) => {
1055
+ const components = /* @__PURE__ */ new Map();
1056
+ for (const componentType of this.componentTypes) {
1057
+ const data = this.getComponentData(componentType)[i];
1058
+ components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
1059
+ }
1060
+ const sparseTuples = this.sparseRelations.getAllForEntity(entity);
1061
+ for (const [componentType, data] of sparseTuples) components.set(componentType, data);
1062
+ return {
1063
+ entity,
1064
+ components
1065
+ };
1066
+ });
1456
1067
  }
1457
1068
  /**
1458
- * Execute a batch of commands for a component entity.
1069
+ * @internal Serialization fast-path.
1070
+ *
1071
+ * Appends SerializedEntity records directly from the archetype's column storage
1072
+ * (componentData arrays) plus sparse relations, avoiding per-entity Map
1073
+ * allocation and repeated Array.from(entries()).
1074
+ *
1075
+ * Component type IDs should be pre-encoded by the caller (once per archetype)
1076
+ * and passed in `encodedComponentTypes` (same order and length as this.componentTypes).
1077
+ *
1078
+ * The provided `encode` function should be the cached variant for best performance
1079
+ * on entity IDs and any sparse relation type IDs.
1080
+ *
1081
+ * `sparseByEntity` is an optional pre-fetched map from a bulk
1082
+ * `SparseStore.getAllForEntities` call (further reduces per-entity calls).
1459
1083
  */
1460
- executeCommands(entityId, commands) {
1461
- if (commands.some((cmd) => cmd.type === "destroy")) {
1462
- this.clear(entityId);
1463
- return;
1464
- }
1465
- const pendingSetValues = /* @__PURE__ */ new Map();
1466
- for (const command of commands) if (command.type === "set" && command.componentType) {
1467
- const merge = getComponentMerge(command.componentType);
1468
- let nextValue = command.component;
1469
- if (merge !== void 0 && pendingSetValues.has(command.componentType)) nextValue = merge(pendingSetValues.get(command.componentType), command.component);
1470
- pendingSetValues.set(command.componentType, nextValue);
1471
- let data = this.componentEntityComponents.get(entityId);
1472
- if (!data) {
1473
- data = /* @__PURE__ */ new Map();
1474
- this.componentEntityComponents.set(entityId, data);
1475
- this.registerRelationEntityId(entityId);
1084
+ appendSerializedEntities(out, encode, encodedComponentTypes, sparseByEntity) {
1085
+ if (encodedComponentTypes.length !== this.componentTypes.length) throw new Error("encodedComponentTypes length must match archetype componentTypes");
1086
+ for (let i = 0; i < this.entities.length; i++) {
1087
+ const entity = this.entities[i];
1088
+ const components = [];
1089
+ for (let c = 0; c < this.componentTypes.length; c++) {
1090
+ const data = this.getComponentData(this.componentTypes[c])[i];
1091
+ components.push({
1092
+ type: encodedComponentTypes[c],
1093
+ value: data === MISSING_COMPONENT ? void 0 : data
1094
+ });
1476
1095
  }
1477
- data.set(command.componentType, nextValue);
1478
- } else if (command.type === "delete" && command.componentType) {
1479
- const data = this.componentEntityComponents.get(entityId);
1480
- if (isWildcardRelationId(command.componentType)) {
1481
- const componentId = getComponentIdFromRelationId(command.componentType);
1482
- if (componentId !== void 0) {
1483
- if (data) {
1484
- for (const key of Array.from(data.keys())) if (getComponentIdFromRelationId(key) === componentId) data.delete(key);
1485
- }
1486
- for (const key of Array.from(pendingSetValues.keys())) if (getComponentIdFromRelationId(key) === componentId) pendingSetValues.delete(key);
1096
+ const sparseTuples = sparseByEntity?.get(entity) ?? this.sparseRelations.getAllForEntity(entity);
1097
+ for (const [componentType, data] of sparseTuples) components.push({
1098
+ type: encode(componentType),
1099
+ value: data
1100
+ });
1101
+ out.push({
1102
+ id: encode(entity),
1103
+ components
1104
+ });
1105
+ }
1106
+ }
1107
+ removeEntity(entityId) {
1108
+ const index = this.entityToIndex.get(entityId);
1109
+ if (index === void 0) return void 0;
1110
+ const removedData = /* @__PURE__ */ new Map();
1111
+ for (const componentType of this.componentTypes) removedData.set(componentType, this.getComponentData(componentType)[index]);
1112
+ const sparseTuples = this.sparseRelations.getAllForEntity(entityId);
1113
+ for (const [componentType, data] of sparseTuples) removedData.set(componentType, data);
1114
+ this.sparseRelations.deleteEntity(entityId);
1115
+ this.entityToIndex.delete(entityId);
1116
+ const lastIndex = this.entities.length - 1;
1117
+ if (index !== lastIndex) {
1118
+ const lastEntity = this.entities[lastIndex];
1119
+ this.entities[index] = lastEntity;
1120
+ this.entityToIndex.set(lastEntity, index);
1121
+ for (const componentType of this.componentTypes) {
1122
+ const dataArray = this.getComponentData(componentType);
1123
+ dataArray[index] = dataArray[lastIndex];
1124
+ }
1125
+ }
1126
+ this.entities.pop();
1127
+ for (const componentType of this.componentTypes) this.getComponentData(componentType).pop();
1128
+ return removedData;
1129
+ }
1130
+ exists(entityId) {
1131
+ return this.entityToIndex.has(entityId);
1132
+ }
1133
+ get(entityId, componentType) {
1134
+ const index = this.entityToIndex.get(entityId);
1135
+ if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
1136
+ if (isWildcardRelationId(componentType)) return this.getWildcardRelations(entityId, index, componentType);
1137
+ return this.getRegularComponent(entityId, index, componentType);
1138
+ }
1139
+ getWildcardRelations(entityId, index, componentType) {
1140
+ const componentId = getComponentIdFromRelationId(componentType);
1141
+ const relations = [];
1142
+ for (const relType of this.componentTypes) {
1143
+ const relDetailed = getDetailedIdType(relType);
1144
+ if (isRelationType(relDetailed) && relDetailed.componentId === componentId) {
1145
+ const dataArray = this.getComponentData(relType);
1146
+ if (dataArray && dataArray[index] !== void 0) {
1147
+ const data = dataArray[index];
1148
+ relations.push([relDetailed.targetId, data === MISSING_COMPONENT ? void 0 : data]);
1487
1149
  }
1488
- } else {
1489
- data?.delete(command.componentType);
1490
- pendingSetValues.delete(command.componentType);
1491
1150
  }
1492
- if (data?.size === 0) this.clear(entityId);
1493
1151
  }
1152
+ if (componentId !== void 0) {
1153
+ const matches = this.sparseRelations.getRelationsForComponent(entityId, componentId);
1154
+ for (const m of matches) relations.push(m);
1155
+ }
1156
+ return relations;
1494
1157
  }
1495
- /**
1496
- * Initialize a component entity from a deserialization snapshot.
1497
- */
1498
- initFromSnapshot(entityId, componentMap) {
1499
- this.componentEntityComponents.set(entityId, componentMap);
1500
- this.registerRelationEntityId(entityId);
1158
+ getRegularComponent(entityId, index, componentType) {
1159
+ if (this.componentTypeSet.has(componentType)) {
1160
+ const data = this.getComponentData(componentType)[index];
1161
+ if (data === MISSING_COMPONENT) throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
1162
+ return data;
1163
+ }
1164
+ if (this.sparseRelations.getValue(entityId, componentType) !== void 0 || this.sparseRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) return this.sparseRelations.getValue(entityId, componentType);
1165
+ throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
1501
1166
  }
1502
- /**
1503
- * Iterate over all component entity entries.
1504
- * Used for serialization.
1505
- */
1506
- entries() {
1507
- return this.componentEntityComponents.entries();
1167
+ getOptional(entityId, componentType) {
1168
+ const index = this.entityToIndex.get(entityId);
1169
+ if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
1170
+ if (this.componentTypeSet.has(componentType)) {
1171
+ const data = this.getComponentData(componentType)[index];
1172
+ if (data === MISSING_COMPONENT) return void 0;
1173
+ return { value: data };
1174
+ }
1175
+ const value = this.sparseRelations.getValue(entityId, componentType);
1176
+ if (value !== void 0) return { value };
1177
+ if (this.sparseRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) return { value: this.sparseRelations.getValue(entityId, componentType) };
1508
1178
  }
1509
- registerRelationEntityId(entityId) {
1510
- const detailed = getDetailedIdType(entityId);
1511
- if (detailed.type !== "entity-relation") return;
1512
- const targetId = detailed.targetId;
1513
- if (targetId === void 0) return;
1514
- const existing = this.relationEntityIdsByTarget.get(targetId);
1515
- if (existing) {
1516
- existing.add(entityId);
1179
+ set(entityId, componentType, data) {
1180
+ const index = this.entityToIndex.get(entityId);
1181
+ if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
1182
+ if (this.componentData.has(componentType)) {
1183
+ this.getComponentData(componentType)[index] = data;
1517
1184
  return;
1518
1185
  }
1519
- this.relationEntityIdsByTarget.set(targetId, new Set([entityId]));
1186
+ const detailedType = getDetailedIdType(componentType);
1187
+ if (isRelationType(detailedType) && isSparseComponent(detailedType.componentId)) {
1188
+ this.sparseRelations.setValue(entityId, componentType, data);
1189
+ return;
1190
+ }
1191
+ throw new Error(`Component type ${componentType} is not in this archetype`);
1520
1192
  }
1521
- unregisterRelationEntityId(entityId) {
1522
- const detailed = getDetailedIdType(entityId);
1523
- if (detailed.type !== "entity-relation") return;
1524
- const targetId = detailed.targetId;
1525
- if (targetId === void 0) return;
1526
- const existing = this.relationEntityIdsByTarget.get(targetId);
1527
- if (!existing) return;
1528
- existing.delete(entityId);
1529
- if (existing.size === 0) this.relationEntityIdsByTarget.delete(targetId);
1193
+ getEntities() {
1194
+ return this.entities;
1195
+ }
1196
+ getEntityToIndexMap() {
1197
+ return this.entityToIndex;
1198
+ }
1199
+ getComponentData(componentType) {
1200
+ const data = this.componentData.get(componentType);
1201
+ if (!data) throw new Error(`Component type ${componentType} is not in this archetype`);
1202
+ return data;
1203
+ }
1204
+ getOptionalComponentData(componentType) {
1205
+ return this.componentData.get(componentType);
1206
+ }
1207
+ getCachedComponentDataSources(componentTypes) {
1208
+ const cacheKey = buildCacheKey(componentTypes);
1209
+ return getOrCompute(this.componentDataSourcesCache, cacheKey, () => componentTypes.map((compType) => this.getComponentDataSource(compType)));
1210
+ }
1211
+ getComponentDataSource(compType) {
1212
+ const optional = isOptionalEntityId(compType);
1213
+ const actualType = optional ? compType.optional : compType;
1214
+ if (getIdType(actualType) === "wildcard-relation") {
1215
+ const componentId = getComponentIdFromRelationId(actualType);
1216
+ return getWildcardRelationDataSource(this.componentTypes, componentId, optional);
1217
+ }
1218
+ return optional ? this.getOptionalComponentData(actualType) : this.getComponentData(actualType);
1219
+ }
1220
+ buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entityId) {
1221
+ return componentDataSources.map((dataSource, i) => buildSingleComponent(componentTypes[i], dataSource, entityIndex, entityId, (type) => this.getComponentData(type), this.sparseRelations));
1222
+ }
1223
+ getEntitiesWithComponents(componentTypes) {
1224
+ const result = [];
1225
+ this.appendEntitiesWithComponents(componentTypes, result);
1226
+ return result;
1227
+ }
1228
+ appendEntitiesWithComponents(componentTypes, result) {
1229
+ this.forEachWithComponents(componentTypes, (entity, ...components) => {
1230
+ result.push({
1231
+ entity,
1232
+ components
1233
+ });
1234
+ });
1235
+ }
1236
+ *iterateWithComponents(componentTypes) {
1237
+ const componentDataSources = this.getCachedComponentDataSources(componentTypes);
1238
+ for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
1239
+ const entity = this.entities[entityIndex];
1240
+ yield [entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity)];
1241
+ }
1242
+ }
1243
+ forEachWithComponents(componentTypes, callback) {
1244
+ const componentDataSources = this.getCachedComponentDataSources(componentTypes);
1245
+ for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
1246
+ const entity = this.entities[entityIndex];
1247
+ callback(entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity));
1248
+ }
1249
+ }
1250
+ forEach(callback) {
1251
+ for (let i = 0; i < this.entities.length; i++) {
1252
+ const entity = this.entities[i];
1253
+ const components = /* @__PURE__ */ new Map();
1254
+ for (const componentType of this.componentTypes) {
1255
+ const data = this.getComponentData(componentType)[i];
1256
+ components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
1257
+ }
1258
+ const sparseTuples = this.sparseRelations.getAllForEntity(entity);
1259
+ for (const [componentType, data] of sparseTuples) components.set(componentType, data);
1260
+ callback(entity, components);
1261
+ }
1262
+ }
1263
+ hasRelationWithComponentId(componentId) {
1264
+ for (const componentType of this.componentTypes) {
1265
+ const detailedType = getDetailedIdType(componentType);
1266
+ if (isRelationType(detailedType) && detailedType.componentId === componentId) return true;
1267
+ }
1268
+ for (const entityId of this.entities) if (this.sparseRelations.getRelationsForComponent(entityId, componentId).length > 0) return true;
1269
+ return false;
1530
1270
  }
1531
1271
  };
1532
1272
  //#endregion
1533
- //#region src/query/filter.ts
1273
+ //#region src/archetype/helpers.ts
1534
1274
  /**
1535
- * Serialize a QueryFilter into a deterministic string suitable for cache keys.
1536
- * Currently only serializes `negativeComponentTypes`.
1275
+ * Check if a components map has any wildcard relations matching a component ID
1276
+ * @param components - Component entity's components map
1277
+ * @param wildcardComponentId - The component ID to match
1278
+ * @returns True if at least one matching relation exists
1537
1279
  */
1538
- function serializeQueryFilter(filter = {}) {
1539
- const negative = (filter.negativeComponentTypes || []).slice().sort((a, b) => a - b);
1540
- if (negative.length === 0) return "";
1541
- return `neg:${negative.join(",")}`;
1280
+ function hasWildcardRelation(components, wildcardComponentId) {
1281
+ for (const relId of components.keys()) if (isRelationId(relId)) {
1282
+ if (getComponentIdFromRelationId(relId) === wildcardComponentId) return true;
1283
+ }
1284
+ return false;
1542
1285
  }
1543
1286
  /**
1544
- * Check if an archetype matches the given component types
1287
+ * Check if a detailed type represents a relation (entity or component)
1545
1288
  */
1546
- function matchesComponentTypes(archetype, componentTypes) {
1547
- return componentTypes.every((type) => {
1548
- const detailedType = getDetailedIdType(type);
1549
- if (detailedType.type === "wildcard-relation") return archetype.componentTypes.some((archetypeType) => {
1550
- if (!isRelationId(archetypeType)) return false;
1551
- return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
1552
- });
1553
- else if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId !== void 0 && isDontFragmentComponent(detailedType.componentId)) {
1554
- const wildcardMarker = relation(detailedType.componentId, "*");
1555
- return archetype.componentTypeSet.has(wildcardMarker);
1556
- } else return archetype.componentTypeSet.has(type);
1557
- });
1289
+ function isRelationType(detailedType) {
1290
+ return detailedType.type === "entity-relation" || detailedType.type === "component-relation";
1558
1291
  }
1559
1292
  /**
1560
- * Check if an archetype matches the filter conditions (only filtering logic)
1293
+ * Check if a component type matches a given component ID for relations
1561
1294
  */
1562
- function matchesFilter(archetype, filter) {
1563
- return (filter.negativeComponentTypes || []).every((type) => {
1564
- const detailedType = getDetailedIdType(type);
1565
- if (detailedType.type === "wildcard-relation") return !archetype.componentTypes.some((archetypeType) => {
1566
- if (!isRelationId(archetypeType)) return false;
1567
- return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
1568
- });
1569
- else return !archetype.componentTypeSet.has(type);
1570
- });
1295
+ function matchesRelationComponentId(componentType, componentId) {
1296
+ const detailedType = getDetailedIdType(componentType);
1297
+ return isRelationType(detailedType) && detailedType.componentId === componentId;
1571
1298
  }
1572
- //#endregion
1573
- //#region src/query/query.ts
1574
1299
  /**
1575
- * Cached query for efficiently iterating entities with specific components.
1576
- *
1577
- * Queries are created via {@link World.createQuery} and should be **reused across frames**
1578
- * for optimal performance. The world automatically keeps the query's internal archetype cache
1579
- * up to date as entities are created and destroyed.
1580
- *
1581
- * @example
1582
- * const movementQuery = world.createQuery([Position, Velocity]);
1583
- *
1584
- * // In the game loop
1585
- * movementQuery.forEach([Position, Velocity], (entity, pos, vel) => {
1586
- * pos.x += vel.x;
1587
- * pos.y += vel.y;
1588
- * });
1300
+ * Build cache key for component types
1589
1301
  */
1590
- var Query = class {
1591
- world;
1592
- componentTypes;
1593
- filter;
1594
- cachedArchetypes = [];
1595
- isDisposed = false;
1596
- /** Cache key assigned by World for O(1) releaseQuery lookup */
1597
- _cacheKey;
1598
- /** Cached wildcard component types for faster entity filtering */
1599
- wildcardTypes;
1600
- /** Cached specific dontFragment relation types that need entity-level filtering */
1601
- specificDontFragmentTypes;
1602
- /**
1603
- * @internal Queries should be created via {@link World.createQuery}, not instantiated directly.
1604
- */
1605
- constructor(world, componentTypes, filter = {}, registry) {
1606
- this.world = world;
1607
- this.componentTypes = normalizeComponentTypes(componentTypes);
1608
- this.filter = filter;
1609
- this.wildcardTypes = this.componentTypes.filter((ct) => getDetailedIdType(ct).type === "wildcard-relation");
1610
- this.specificDontFragmentTypes = this.componentTypes.filter((ct) => {
1611
- const detailedType = getDetailedIdType(ct);
1612
- return (detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId !== void 0 && isDontFragmentComponent(detailedType.componentId);
1613
- });
1614
- this.updateCache();
1615
- if (registry) registry.register(this);
1616
- }
1617
- /**
1618
- * Check if query is disposed and throw error if so
1619
- */
1620
- ensureNotDisposed() {
1621
- if (this.isDisposed) throw new Error("Query has been disposed");
1302
+ function buildCacheKey(componentTypes) {
1303
+ return componentTypes.map((id) => isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`).join(",");
1304
+ }
1305
+ /**
1306
+ * Get data source for wildcard relations from component types
1307
+ */
1308
+ function getWildcardRelationDataSource(componentTypes, componentId, optional) {
1309
+ const matchingRelations = componentTypes.filter((ct) => matchesRelationComponentId(ct, componentId));
1310
+ return optional ? matchingRelations.length > 0 ? matchingRelations : void 0 : matchingRelations;
1311
+ }
1312
+ /**
1313
+ * Build wildcard relation value from matching relations.
1314
+ * Receives the SparseStore directly for efficient per-component lookups.
1315
+ */
1316
+ function buildWildcardRelationValue(wildcardRelationType, matchingRelations, getDataAtIndex, sparseStore, entityId, optional) {
1317
+ const relations = [];
1318
+ const targetComponentId = getComponentIdFromRelationId(wildcardRelationType);
1319
+ for (const relType of matchingRelations || []) {
1320
+ const data = getDataAtIndex(relType);
1321
+ const targetId = getTargetIdFromRelationId(relType);
1322
+ relations.push([targetId, data === MISSING_COMPONENT ? void 0 : data]);
1622
1323
  }
1623
- /**
1624
- * Returns all entity IDs that match this query.
1625
- *
1626
- * @returns Array of matching entity IDs
1627
- *
1628
- * @example
1629
- * const entities = query.getEntities();
1630
- * for (const entity of entities) {
1631
- * const pos = world.get(entity, Position);
1632
- * }
1633
- */
1634
- getEntities() {
1635
- this.ensureNotDisposed();
1636
- if (this.wildcardTypes.length === 0 && this.specificDontFragmentTypes.length === 0) {
1637
- const result = [];
1638
- for (const archetype of this.cachedArchetypes) for (const entity of archetype.getEntities()) result.push(entity);
1639
- return result;
1640
- }
1641
- const result = [];
1642
- for (const archetype of this.cachedArchetypes) for (const entity of archetype.getEntities()) if (this.entityMatchesQuery(archetype, entity)) result.push(entity);
1643
- return result;
1324
+ if (targetComponentId !== void 0) {
1325
+ const dfMatches = sparseStore.getRelationsForComponent(entityId, targetComponentId);
1326
+ for (const m of dfMatches) relations.push(m);
1644
1327
  }
1645
- /**
1646
- * Check if entity matches all query requirements (wildcards and specific dontFragment relations)
1647
- */
1648
- entityMatchesQuery(archetype, entity) {
1649
- for (const wildcardType of this.wildcardTypes) {
1650
- const relations = archetype.get(entity, wildcardType);
1651
- if (!relations || relations.length === 0) return false;
1328
+ if (relations.length === 0) {
1329
+ if (!optional) {
1330
+ const componentId = getComponentIdFromRelationId(wildcardRelationType);
1331
+ throw new Error(`No matching relations found for mandatory wildcard relation component ${componentId} on entity ${entityId}`);
1652
1332
  }
1653
- for (const specificType of this.specificDontFragmentTypes) if (archetype.getOptional(entity, specificType) === void 0) return false;
1654
- return true;
1333
+ return;
1655
1334
  }
1656
- /**
1657
- * Returns all matching entities along with their component data.
1658
- *
1659
- * @param componentTypes - Array of component types to retrieve
1660
- * @returns Array of objects containing the entity ID and its component tuple
1661
- *
1662
- * @example
1663
- * const results = query.getEntitiesWithComponents([Position, Velocity]);
1664
- * results.forEach(({ entity, components: [pos, vel] }) => {
1665
- * pos.x += vel.x;
1666
- * });
1667
- */
1668
- getEntitiesWithComponents(componentTypes) {
1669
- this.ensureNotDisposed();
1670
- const result = [];
1671
- for (const archetype of this.cachedArchetypes) archetype.appendEntitiesWithComponents(componentTypes, result);
1672
- return result;
1335
+ return optional ? { value: relations } : relations;
1336
+ }
1337
+ /**
1338
+ * Build regular component value from data source
1339
+ */
1340
+ function buildRegularComponentValue(dataSource, entityIndex, optional) {
1341
+ if (dataSource === void 0) {
1342
+ if (optional) return void 0;
1343
+ throw new Error(`Component data not found for mandatory component type`);
1673
1344
  }
1345
+ const data = dataSource[entityIndex];
1346
+ const result = data === MISSING_COMPONENT ? void 0 : data;
1347
+ return optional ? { value: result } : result;
1348
+ }
1349
+ /**
1350
+ * Build a single component value based on its type
1351
+ */
1352
+ function buildSingleComponent(compType, dataSource, entityIndex, entityId, getComponentData, sparseRelations) {
1353
+ const optional = isOptionalEntityId(compType);
1354
+ const actualType = optional ? compType.optional : compType;
1355
+ if (getIdType(actualType) === "wildcard-relation") return buildWildcardRelationValue(actualType, dataSource, (relType) => getComponentData(relType)[entityIndex], sparseRelations, entityId, optional);
1356
+ else return buildRegularComponentValue(dataSource, entityIndex, optional);
1357
+ }
1358
+ //#endregion
1359
+ //#region src/component/entity-store.ts
1360
+ /**
1361
+ * Manages component entity (singleton) storage.
1362
+ *
1363
+ * Component entities use a flat Map-based storage rather than the Archetype-based
1364
+ * storage used for regular entities. Their IDs are in the component ID range
1365
+ * (or are relation IDs), distinguishing them from regular entity IDs.
1366
+ */
1367
+ var ComponentEntityStore = class {
1368
+ componentEntityComponents = /* @__PURE__ */ new Map();
1369
+ relationEntityIdsByTarget = /* @__PURE__ */ new Map();
1674
1370
  /**
1675
- * Iterates over all matching entities and invokes the callback with their component data.
1676
- * This is the preferred way to read and mutate components in a hot loop.
1677
- *
1678
- * @param componentTypes - Array of component types to retrieve
1679
- * @param callback - Function called for each matching entity with its components
1680
- *
1681
- * @example
1682
- * query.forEach([Position, Velocity], (entity, pos, vel) => {
1683
- * pos.x += vel.x;
1684
- * pos.y += vel.y;
1685
- * });
1371
+ * Check if an entity ID is a component entity type.
1372
+ * Returns true for component IDs, component-relation IDs, and entity-relation IDs
1373
+ * i.e. anything that is NOT a plain entity or an invalid ID.
1686
1374
  */
1687
- forEach(componentTypes, callback) {
1688
- this.ensureNotDisposed();
1689
- for (const archetype of this.cachedArchetypes) archetype.forEachWithComponents(componentTypes, callback);
1375
+ exists(entityId) {
1376
+ const detailed = getDetailedIdType(entityId);
1377
+ return detailed.type !== "entity" && detailed.type !== "invalid";
1690
1378
  }
1691
1379
  /**
1692
- * Generator that yields each matching entity together with its component data.
1693
- *
1694
- * @param componentTypes - Array of component types to retrieve
1695
- * @yields Tuples of `[entityId, ...components]`
1696
- *
1697
- * @example
1698
- * for (const [entity, pos, vel] of query.iterate([Position, Velocity])) {
1699
- * pos.x += vel.x;
1700
- * }
1380
+ * Check if a component entity has a specific component.
1701
1381
  */
1702
- *iterate(componentTypes) {
1703
- this.ensureNotDisposed();
1704
- for (const archetype of this.cachedArchetypes) yield* archetype.iterateWithComponents(componentTypes);
1382
+ has(entityId, componentType) {
1383
+ return this.componentEntityComponents.get(entityId)?.has(componentType) ?? false;
1705
1384
  }
1706
1385
  /**
1707
- * Returns an array containing the data of a single component for every matching entity.
1708
- *
1709
- * @param componentType - The component type to retrieve
1710
- * @returns Array of component data (one entry per matching entity)
1711
- *
1712
- * @example
1713
- * const positions = query.getComponentData(Position);
1386
+ * Check if a singleton component has data the has(componentId) overload.
1387
+ * In singleton usage the entity ID and the component type are the same value.
1714
1388
  */
1715
- getComponentData(componentType) {
1716
- this.ensureNotDisposed();
1717
- const result = [];
1718
- for (const archetype of this.cachedArchetypes) for (const data of archetype.getComponentData(componentType)) result.push(data);
1719
- return result;
1389
+ hasSingleton(componentId) {
1390
+ return this.componentEntityComponents.get(componentId)?.has(componentId) ?? false;
1720
1391
  }
1721
1392
  /**
1722
- * @internal Rebuilds the cached archetype list. Called automatically by the world.
1393
+ * Check if a component entity has any wildcard relations matching a component ID.
1723
1394
  */
1724
- updateCache() {
1725
- if (this.isDisposed) return;
1726
- this.cachedArchetypes = this.world.getMatchingArchetypes(this.componentTypes).filter((archetype) => matchesFilter(archetype, this.filter));
1395
+ hasWildcard(entityId, componentId) {
1396
+ const data = this.componentEntityComponents.get(entityId);
1397
+ if (!data) return false;
1398
+ return hasWildcardRelation(data, componentId);
1727
1399
  }
1728
1400
  /**
1729
- * @internal Called by the world when a new archetype is created.
1401
+ * Get a component value from a component entity.
1402
+ * Throws if the component does not exist.
1730
1403
  */
1731
- checkNewArchetype(archetype) {
1732
- if (this.isDisposed) return;
1733
- if (matchesComponentTypes(archetype, this.componentTypes) && matchesFilter(archetype, this.filter) && !this.cachedArchetypes.includes(archetype)) this.cachedArchetypes.push(archetype);
1404
+ get(entityId, componentType) {
1405
+ const data = this.componentEntityComponents.get(entityId);
1406
+ if (!data || !data.has(componentType)) throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
1407
+ return data.get(componentType);
1734
1408
  }
1735
1409
  /**
1736
- * @internal Called by the world when an archetype is destroyed.
1410
+ * Get an optional component value from a component entity.
1411
+ * Returns undefined if the component does not exist.
1737
1412
  */
1738
- removeArchetype(archetype) {
1739
- if (this.isDisposed) return;
1740
- const index = this.cachedArchetypes.indexOf(archetype);
1741
- if (index !== -1) this.cachedArchetypes.splice(index, 1);
1413
+ getOptional(entityId, componentType) {
1414
+ const data = this.componentEntityComponents.get(entityId);
1415
+ if (!data || !data.has(componentType)) return void 0;
1416
+ return { value: data.get(componentType) };
1742
1417
  }
1743
1418
  /**
1744
- * Request disposal of this query.
1745
- * This will decrement the world's reference count for the query.
1746
- * The query will only be fully disposed when the ref count reaches zero.
1419
+ * Get all wildcard relations of a given type from a component entity.
1747
1420
  */
1748
- dispose() {
1749
- this.world.releaseQuery(this);
1421
+ getWildcard(entityId, wildcardComponentType) {
1422
+ const componentId = getComponentIdFromRelationId(wildcardComponentType);
1423
+ const data = this.componentEntityComponents.get(entityId);
1424
+ if (componentId === void 0 || !data) return [];
1425
+ const relations = [];
1426
+ for (const [key, value] of data.entries()) {
1427
+ if (getComponentIdFromRelationId(key) !== componentId) continue;
1428
+ const detailed = getDetailedIdType(key);
1429
+ if (detailed.type === "entity-relation" || detailed.type === "component-relation") relations.push([detailed.targetId, value]);
1430
+ }
1431
+ return relations;
1750
1432
  }
1751
1433
  /**
1752
- * @internal Fully disposes the query when the world's refCount reaches zero.
1434
+ * Clear all data for a component entity.
1753
1435
  */
1754
- _disposeInternal(registry) {
1755
- if (!this.isDisposed) {
1756
- if (registry) registry.unregister(this);
1757
- this.cachedArchetypes = [];
1758
- this.isDisposed = true;
1759
- }
1436
+ clear(entityId) {
1437
+ if (this.componentEntityComponents.delete(entityId)) this.unregisterRelationEntityId(entityId);
1760
1438
  }
1761
1439
  /**
1762
- * Using-with-disposals support. Calls {@link dispose} automatically.
1763
- *
1764
- * @example
1765
- * using query = world.createQuery([Position]);
1766
- * // query is released automatically when the block exits
1440
+ * Cleanup all component entities that reference a given target entity.
1441
+ * Called when a target entity is destroyed.
1767
1442
  */
1768
- [Symbol.dispose]() {
1769
- this.dispose();
1443
+ cleanupReferencesTo(targetId) {
1444
+ const relationEntities = this.relationEntityIdsByTarget.get(targetId);
1445
+ if (!relationEntities) return;
1446
+ for (const relationEntityId of relationEntities) this.componentEntityComponents.delete(relationEntityId);
1447
+ this.relationEntityIdsByTarget.delete(targetId);
1770
1448
  }
1771
1449
  /**
1772
- * Whether the query has been disposed and can no longer be used.
1450
+ * Execute a batch of commands for a component entity.
1773
1451
  */
1774
- get disposed() {
1775
- return this.isDisposed;
1776
- }
1777
- };
1778
- //#endregion
1779
- //#region src/query/registry.ts
1780
- /**
1781
- * Manages the lifecycle and caching of `Query` instances.
1452
+ executeCommands(entityId, commands) {
1453
+ if (commands.some((cmd) => cmd.type === "destroy")) {
1454
+ this.clear(entityId);
1455
+ return;
1456
+ }
1457
+ const pendingSetValues = /* @__PURE__ */ new Map();
1458
+ for (const command of commands) if (command.type === "set" && command.componentType) {
1459
+ const merge = getComponentMerge(command.componentType);
1460
+ let nextValue = command.component;
1461
+ if (merge !== void 0 && pendingSetValues.has(command.componentType)) nextValue = merge(pendingSetValues.get(command.componentType), command.component);
1462
+ pendingSetValues.set(command.componentType, nextValue);
1463
+ let data = this.componentEntityComponents.get(entityId);
1464
+ if (!data) {
1465
+ data = /* @__PURE__ */ new Map();
1466
+ this.componentEntityComponents.set(entityId, data);
1467
+ this.registerRelationEntityId(entityId);
1468
+ }
1469
+ data.set(command.componentType, nextValue);
1470
+ } else if (command.type === "delete" && command.componentType) {
1471
+ const data = this.componentEntityComponents.get(entityId);
1472
+ if (isWildcardRelationId(command.componentType)) {
1473
+ const componentId = getComponentIdFromRelationId(command.componentType);
1474
+ if (componentId !== void 0) {
1475
+ if (data) {
1476
+ for (const key of Array.from(data.keys())) if (getComponentIdFromRelationId(key) === componentId) data.delete(key);
1477
+ }
1478
+ for (const key of Array.from(pendingSetValues.keys())) if (getComponentIdFromRelationId(key) === componentId) pendingSetValues.delete(key);
1479
+ }
1480
+ } else {
1481
+ data?.delete(command.componentType);
1482
+ pendingSetValues.delete(command.componentType);
1483
+ }
1484
+ if (data?.size === 0) this.clear(entityId);
1485
+ }
1486
+ }
1487
+ /**
1488
+ * Initialize a component entity from a deserialization snapshot.
1489
+ */
1490
+ initFromSnapshot(entityId, componentMap) {
1491
+ this.componentEntityComponents.set(entityId, componentMap);
1492
+ this.registerRelationEntityId(entityId);
1493
+ }
1494
+ /**
1495
+ * Iterate over all component entity entries.
1496
+ * Used for serialization.
1497
+ */
1498
+ entries() {
1499
+ return this.componentEntityComponents.entries();
1500
+ }
1501
+ registerRelationEntityId(entityId) {
1502
+ const detailed = getDetailedIdType(entityId);
1503
+ if (detailed.type !== "entity-relation") return;
1504
+ const targetId = detailed.targetId;
1505
+ if (targetId === void 0) return;
1506
+ const existing = this.relationEntityIdsByTarget.get(targetId);
1507
+ if (existing) {
1508
+ existing.add(entityId);
1509
+ return;
1510
+ }
1511
+ this.relationEntityIdsByTarget.set(targetId, new Set([entityId]));
1512
+ }
1513
+ unregisterRelationEntityId(entityId) {
1514
+ const detailed = getDetailedIdType(entityId);
1515
+ if (detailed.type !== "entity-relation") return;
1516
+ const targetId = detailed.targetId;
1517
+ if (targetId === void 0) return;
1518
+ const existing = this.relationEntityIdsByTarget.get(targetId);
1519
+ if (!existing) return;
1520
+ existing.delete(entityId);
1521
+ if (existing.size === 0) this.relationEntityIdsByTarget.delete(targetId);
1522
+ }
1523
+ };
1524
+ //#endregion
1525
+ //#region src/query/filter.ts
1526
+ /**
1527
+ * Serialize a QueryFilter into a deterministic string suitable for cache keys.
1528
+ * Currently only serializes `negativeComponentTypes`.
1529
+ */
1530
+ function serializeQueryFilter(filter = {}) {
1531
+ const negative = (filter.negativeComponentTypes || []).slice().sort((a, b) => a - b);
1532
+ if (negative.length === 0) return "";
1533
+ return `neg:${negative.join(",")}`;
1534
+ }
1535
+ /**
1536
+ * Check if an archetype matches the given component types
1537
+ */
1538
+ function matchesComponentTypes(archetype, componentTypes) {
1539
+ return componentTypes.every((type) => {
1540
+ const detailedType = getDetailedIdType(type);
1541
+ if (detailedType.type === "wildcard-relation") return archetype.componentTypes.some((archetypeType) => {
1542
+ if (!isRelationId(archetypeType)) return false;
1543
+ return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
1544
+ });
1545
+ else if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId !== void 0 && isSparseComponent(detailedType.componentId)) {
1546
+ const wildcardMarker = relation(detailedType.componentId, "*");
1547
+ return archetype.componentTypeSet.has(wildcardMarker);
1548
+ } else return archetype.componentTypeSet.has(type);
1549
+ });
1550
+ }
1551
+ /**
1552
+ * Check if an archetype matches the filter conditions (only filtering logic)
1553
+ */
1554
+ function matchesFilter(archetype, filter) {
1555
+ return (filter.negativeComponentTypes || []).every((type) => {
1556
+ const detailedType = getDetailedIdType(type);
1557
+ if (detailedType.type === "wildcard-relation") return !archetype.componentTypes.some((archetypeType) => {
1558
+ if (!isRelationId(archetypeType)) return false;
1559
+ return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
1560
+ });
1561
+ else return !archetype.componentTypeSet.has(type);
1562
+ });
1563
+ }
1564
+ //#endregion
1565
+ //#region src/query/query.ts
1566
+ /**
1567
+ * Cached query for efficiently iterating entities with specific components.
1782
1568
  *
1783
- * Responsibilities:
1784
- * - Create / reuse cached queries keyed by component-type + filter signature.
1785
- * - Track reference counts so queries are only disposed when truly unused.
1786
- * - Notify registered queries when new archetypes are created or destroyed.
1569
+ * Queries are created via {@link World.createQuery} and should be **reused across frames**
1570
+ * for optimal performance. The world automatically keeps the query's internal archetype cache
1571
+ * up to date as entities are created and destroyed.
1787
1572
  *
1788
- * The `_cacheKey` string that was previously attached directly to `Query` is now
1789
- * kept in a private `WeakMap` so the `Query` class doesn't need to expose it.
1573
+ * @example
1574
+ * const movementQuery = world.createQuery([Position, Velocity]);
1575
+ *
1576
+ * // In the game loop
1577
+ * movementQuery.forEach([Position, Velocity], (entity, pos, vel) => {
1578
+ * pos.x += vel.x;
1579
+ * pos.y += vel.y;
1580
+ * });
1790
1581
  */
1791
- var QueryRegistry = class {
1792
- /** All live queries that should receive archetype notifications. */
1793
- queries = /* @__PURE__ */ new Set();
1794
- /** Cache of reusable queries keyed by a deterministic signature string. */
1795
- cache = /* @__PURE__ */ new Map();
1796
- /** Maps each query to its cache key without polluting the Query public API. */
1797
- cacheKeys = /* @__PURE__ */ new WeakMap();
1582
+ var Query = class {
1583
+ world;
1584
+ componentTypes;
1585
+ filter;
1586
+ cachedArchetypes = [];
1587
+ isDisposed = false;
1588
+ /** Cache key assigned by World for O(1) releaseQuery lookup */
1589
+ _cacheKey;
1590
+ /** Cached wildcard component types for faster entity filtering */
1591
+ wildcardTypes;
1592
+ /** Cached specific sparse relation types that need entity-level filtering */
1593
+ specificSparseRelationTypes;
1798
1594
  /**
1799
- * Returns (or creates) a cached query for the given component types and filter.
1800
- * Increments the reference count on cache hits.
1595
+ * @internal Queries should be created via {@link World.createQuery}, not instantiated directly.
1596
+ */
1597
+ constructor(world, componentTypes, filter = {}, registry) {
1598
+ this.world = world;
1599
+ this.componentTypes = normalizeComponentTypes(componentTypes);
1600
+ this.filter = filter;
1601
+ this.wildcardTypes = this.componentTypes.filter((ct) => getDetailedIdType(ct).type === "wildcard-relation");
1602
+ this.specificSparseRelationTypes = this.componentTypes.filter((ct) => {
1603
+ const detailedType = getDetailedIdType(ct);
1604
+ return (detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId !== void 0 && isSparseComponent(detailedType.componentId);
1605
+ });
1606
+ this.updateCache();
1607
+ if (registry) registry.register(this);
1608
+ }
1609
+ /**
1610
+ * Check if query is disposed and throw error if so
1611
+ */
1612
+ ensureNotDisposed() {
1613
+ if (this.isDisposed) throw new Error("Query has been disposed");
1614
+ }
1615
+ /**
1616
+ * Returns all entity IDs that match this query.
1801
1617
  *
1802
- * @param world The world that owns this registry.
1803
- * @param sortedTypes Normalized (sorted) component types.
1804
- * @param key Combined cache key (`types|filter`).
1805
- * @param filter The raw query filter (used when creating a new Query).
1618
+ * @returns Array of matching entity IDs
1619
+ *
1620
+ * @example
1621
+ * const entities = query.getEntities();
1622
+ * for (const entity of entities) {
1623
+ * const pos = world.get(entity, Position);
1624
+ * }
1806
1625
  */
1807
- getOrCreate(world, sortedTypes, key, filter) {
1808
- const cached = this.cache.get(key);
1809
- if (cached) {
1810
- cached.refCount++;
1811
- return cached.query;
1626
+ getEntities() {
1627
+ this.ensureNotDisposed();
1628
+ if (this.wildcardTypes.length === 0 && this.specificSparseRelationTypes.length === 0) {
1629
+ const result = [];
1630
+ for (const archetype of this.cachedArchetypes) for (const entity of archetype.getEntities()) result.push(entity);
1631
+ return result;
1812
1632
  }
1813
- const query = new Query(world, sortedTypes, filter, this);
1814
- this.cacheKeys.set(query, key);
1815
- this.cache.set(key, {
1816
- query,
1817
- refCount: 1
1818
- });
1819
- return query;
1633
+ const result = [];
1634
+ for (const archetype of this.cachedArchetypes) for (const entity of archetype.getEntities()) if (this.entityMatchesQuery(archetype, entity)) result.push(entity);
1635
+ return result;
1820
1636
  }
1821
1637
  /**
1822
- * Decrements the reference count for the given query.
1823
- * When the count reaches zero the query is fully disposed.
1638
+ * Check if entity matches all query requirements (wildcards and specific sparse relations)
1824
1639
  */
1825
- release(query) {
1826
- const key = this.cacheKeys.get(query);
1827
- if (!key) return;
1828
- const cached = this.cache.get(key);
1829
- if (!cached || cached.query !== query) return;
1830
- cached.refCount--;
1831
- if (cached.refCount <= 0) {
1832
- this.cache.delete(key);
1833
- cached.query._disposeInternal(this);
1640
+ entityMatchesQuery(archetype, entity) {
1641
+ for (const wildcardType of this.wildcardTypes) {
1642
+ const relations = archetype.get(entity, wildcardType);
1643
+ if (!relations || relations.length === 0) return false;
1834
1644
  }
1645
+ for (const specificType of this.specificSparseRelationTypes) if (archetype.getOptional(entity, specificType) === void 0) return false;
1646
+ return true;
1647
+ }
1648
+ /**
1649
+ * Returns all matching entities along with their component data.
1650
+ *
1651
+ * @param componentTypes - Array of component types to retrieve
1652
+ * @returns Array of objects containing the entity ID and its component tuple
1653
+ *
1654
+ * @example
1655
+ * const results = query.getEntitiesWithComponents([Position, Velocity]);
1656
+ * results.forEach(({ entity, components: [pos, vel] }) => {
1657
+ * pos.x += vel.x;
1658
+ * });
1659
+ */
1660
+ getEntitiesWithComponents(componentTypes) {
1661
+ this.ensureNotDisposed();
1662
+ const result = [];
1663
+ for (const archetype of this.cachedArchetypes) archetype.appendEntitiesWithComponents(componentTypes, result);
1664
+ return result;
1665
+ }
1666
+ /**
1667
+ * Iterates over all matching entities and invokes the callback with their component data.
1668
+ * This is the preferred way to read and mutate components in a hot loop.
1669
+ *
1670
+ * @param componentTypes - Array of component types to retrieve
1671
+ * @param callback - Function called for each matching entity with its components
1672
+ *
1673
+ * @example
1674
+ * query.forEach([Position, Velocity], (entity, pos, vel) => {
1675
+ * pos.x += vel.x;
1676
+ * pos.y += vel.y;
1677
+ * });
1678
+ */
1679
+ forEach(componentTypes, callback) {
1680
+ this.ensureNotDisposed();
1681
+ for (const archetype of this.cachedArchetypes) archetype.forEachWithComponents(componentTypes, callback);
1682
+ }
1683
+ /**
1684
+ * Generator that yields each matching entity together with its component data.
1685
+ *
1686
+ * @param componentTypes - Array of component types to retrieve
1687
+ * @yields Tuples of `[entityId, ...components]`
1688
+ *
1689
+ * @example
1690
+ * for (const [entity, pos, vel] of query.iterate([Position, Velocity])) {
1691
+ * pos.x += vel.x;
1692
+ * }
1693
+ */
1694
+ *iterate(componentTypes) {
1695
+ this.ensureNotDisposed();
1696
+ for (const archetype of this.cachedArchetypes) yield* archetype.iterateWithComponents(componentTypes);
1697
+ }
1698
+ /**
1699
+ * Returns an array containing the data of a single component for every matching entity.
1700
+ *
1701
+ * @param componentType - The component type to retrieve
1702
+ * @returns Array of component data (one entry per matching entity)
1703
+ *
1704
+ * @example
1705
+ * const positions = query.getComponentData(Position);
1706
+ */
1707
+ getComponentData(componentType) {
1708
+ this.ensureNotDisposed();
1709
+ const result = [];
1710
+ for (const archetype of this.cachedArchetypes) for (const data of archetype.getComponentData(componentType)) result.push(data);
1711
+ return result;
1712
+ }
1713
+ /**
1714
+ * @internal Rebuilds the cached archetype list. Called automatically by the world.
1715
+ */
1716
+ updateCache() {
1717
+ if (this.isDisposed) return;
1718
+ this.cachedArchetypes = this.world.getMatchingArchetypes(this.componentTypes).filter((archetype) => matchesFilter(archetype, this.filter));
1719
+ }
1720
+ /**
1721
+ * @internal Called by the world when a new archetype is created.
1722
+ */
1723
+ checkNewArchetype(archetype) {
1724
+ if (this.isDisposed) return;
1725
+ if (matchesComponentTypes(archetype, this.componentTypes) && matchesFilter(archetype, this.filter) && !this.cachedArchetypes.includes(archetype)) this.cachedArchetypes.push(archetype);
1726
+ }
1727
+ /**
1728
+ * @internal Called by the world when an archetype is destroyed.
1729
+ */
1730
+ removeArchetype(archetype) {
1731
+ if (this.isDisposed) return;
1732
+ const index = this.cachedArchetypes.indexOf(archetype);
1733
+ if (index !== -1) this.cachedArchetypes.splice(index, 1);
1734
+ }
1735
+ /**
1736
+ * Request disposal of this query.
1737
+ * This will decrement the world's reference count for the query.
1738
+ * The query will only be fully disposed when the ref count reaches zero.
1739
+ */
1740
+ dispose() {
1741
+ this.world.releaseQuery(this);
1742
+ }
1743
+ /**
1744
+ * @internal Fully disposes the query when the world's refCount reaches zero.
1745
+ */
1746
+ _disposeInternal(registry) {
1747
+ if (!this.isDisposed) {
1748
+ if (registry) registry.unregister(this);
1749
+ this.cachedArchetypes = [];
1750
+ this.isDisposed = true;
1751
+ }
1752
+ }
1753
+ /**
1754
+ * Using-with-disposals support. Calls {@link dispose} automatically.
1755
+ *
1756
+ * @example
1757
+ * using query = world.createQuery([Position]);
1758
+ * // query is released automatically when the block exits
1759
+ */
1760
+ [Symbol.dispose]() {
1761
+ this.dispose();
1762
+ }
1763
+ /**
1764
+ * Whether the query has been disposed and can no longer be used.
1765
+ */
1766
+ get disposed() {
1767
+ return this.isDisposed;
1768
+ }
1769
+ };
1770
+ //#endregion
1771
+ //#region src/query/registry.ts
1772
+ /**
1773
+ * Manages the lifecycle and caching of `Query` instances.
1774
+ *
1775
+ * Responsibilities:
1776
+ * - Create / reuse cached queries keyed by component-type + filter signature.
1777
+ * - Track reference counts so queries are only disposed when truly unused.
1778
+ * - Notify registered queries when new archetypes are created or destroyed.
1779
+ *
1780
+ * The `_cacheKey` string that was previously attached directly to `Query` is now
1781
+ * kept in a private `WeakMap` so the `Query` class doesn't need to expose it.
1782
+ */
1783
+ var QueryRegistry = class {
1784
+ /** All live queries that should receive archetype notifications. */
1785
+ queries = /* @__PURE__ */ new Set();
1786
+ /** Cache of reusable queries keyed by a deterministic signature string. */
1787
+ cache = /* @__PURE__ */ new Map();
1788
+ /** Maps each query to its cache key without polluting the Query public API. */
1789
+ cacheKeys = /* @__PURE__ */ new WeakMap();
1790
+ /**
1791
+ * Returns (or creates) a cached query for the given component types and filter.
1792
+ * Increments the reference count on cache hits.
1793
+ *
1794
+ * @param world The world that owns this registry.
1795
+ * @param sortedTypes Normalized (sorted) component types.
1796
+ * @param key Combined cache key (`types|filter`).
1797
+ * @param filter The raw query filter (used when creating a new Query).
1798
+ */
1799
+ getOrCreate(world, sortedTypes, key, filter) {
1800
+ const cached = this.cache.get(key);
1801
+ if (cached) {
1802
+ cached.refCount++;
1803
+ return cached.query;
1804
+ }
1805
+ const query = new Query(world, sortedTypes, filter, this);
1806
+ this.cacheKeys.set(query, key);
1807
+ this.cache.set(key, {
1808
+ query,
1809
+ refCount: 1
1810
+ });
1811
+ return query;
1812
+ }
1813
+ /**
1814
+ * Decrements the reference count for the given query.
1815
+ * When the count reaches zero the query is fully disposed.
1816
+ */
1817
+ release(query) {
1818
+ const key = this.cacheKeys.get(query);
1819
+ if (!key) return;
1820
+ const cached = this.cache.get(key);
1821
+ if (!cached || cached.query !== query) return;
1822
+ cached.refCount--;
1823
+ if (cached.refCount <= 0) {
1824
+ this.cache.delete(key);
1825
+ cached.query._disposeInternal(this);
1826
+ }
1827
+ }
1828
+ /**
1829
+ * Registers a query so it receives future archetype notifications.
1830
+ * Called automatically by the `Query` constructor via `world._registerQuery`.
1831
+ */
1832
+ register(query) {
1833
+ this.queries.add(query);
1834
+ }
1835
+ /**
1836
+ * Removes a query from the notification list.
1837
+ * Called by `Query._disposeInternal` via `world._unregisterQuery`.
1838
+ */
1839
+ unregister(query) {
1840
+ this.queries.delete(query);
1841
+ }
1842
+ /**
1843
+ * Notifies all live queries that a new archetype has been created.
1844
+ * Queries will add the archetype to their cache if it matches.
1845
+ */
1846
+ onNewArchetype(archetype) {
1847
+ for (const query of this.queries) query.checkNewArchetype(archetype);
1848
+ }
1849
+ /**
1850
+ * Notifies all live queries that an archetype has been destroyed.
1851
+ * Queries will remove the archetype from their internal cache.
1852
+ */
1853
+ onArchetypeRemoved(archetype) {
1854
+ for (const query of this.queries) query.removeArchetype(archetype);
1855
+ }
1856
+ };
1857
+ //#endregion
1858
+ //#region src/world/commands.ts
1859
+ function processCommands(entityId, currentArchetype, commands, changeset, handleExclusiveRelation) {
1860
+ for (const command of commands) if (command.type === "set") processSetCommand(entityId, currentArchetype, command.componentType, command.component, changeset, handleExclusiveRelation);
1861
+ else if (command.type === "delete") processDeleteCommand(entityId, currentArchetype, command.componentType, changeset);
1862
+ }
1863
+ function processSetCommand(entityId, currentArchetype, componentType, component, changeset, handleExclusiveRelation) {
1864
+ const componentId = getComponentIdFromRelationId(componentType);
1865
+ if (componentId !== void 0) {
1866
+ handleExclusiveRelation(entityId, currentArchetype, componentId);
1867
+ if (isSparseComponent(componentId)) {
1868
+ const wildcardMarker = relation(componentId, "*");
1869
+ if (!currentArchetype.componentTypeSet.has(wildcardMarker)) changeset.set(wildcardMarker, void 0);
1870
+ }
1871
+ }
1872
+ const merge = getComponentMerge(componentType);
1873
+ if (merge !== void 0 && changeset.adds.has(componentType)) {
1874
+ const prev = changeset.adds.get(componentType);
1875
+ changeset.set(componentType, merge(prev, component));
1876
+ return;
1877
+ }
1878
+ changeset.set(componentType, component);
1879
+ }
1880
+ function processDeleteCommand(entityId, currentArchetype, componentType, changeset) {
1881
+ const componentId = getComponentIdFromRelationId(componentType);
1882
+ if (isWildcardRelationId(componentType) && componentId !== void 0) removeWildcardRelations(entityId, currentArchetype, componentId, changeset);
1883
+ else {
1884
+ changeset.delete(componentType);
1885
+ maybeRemoveWildcardMarker(entityId, currentArchetype, componentType, componentId, changeset);
1886
+ }
1887
+ }
1888
+ function removeMatchingRelations(entityId, archetype, baseComponentId, changeset) {
1889
+ for (const componentType of archetype.componentTypes) {
1890
+ if (isWildcardRelationId(componentType)) continue;
1891
+ if (getComponentIdFromRelationId(componentType) === baseComponentId) changeset.delete(componentType);
1892
+ }
1893
+ const sparseData = archetype.getEntitySparseRelations(entityId);
1894
+ if (sparseData) {
1895
+ for (const componentType of sparseData.keys()) if (getComponentIdFromRelationId(componentType) === baseComponentId) changeset.delete(componentType);
1896
+ }
1897
+ }
1898
+ function removeWildcardRelations(entityId, currentArchetype, baseComponentId, changeset) {
1899
+ removeMatchingRelations(entityId, currentArchetype, baseComponentId, changeset);
1900
+ if (isSparseComponent(baseComponentId)) changeset.delete(relation(baseComponentId, "*"));
1901
+ }
1902
+ function maybeRemoveWildcardMarker(entityId, archetype, removedComponentType, componentId, changeset) {
1903
+ if (componentId === void 0 || !isSparseComponent(componentId)) return;
1904
+ const wildcardMarker = relation(componentId, "*");
1905
+ for (const otherComponentType of archetype.componentTypes) {
1906
+ if (otherComponentType === removedComponentType) continue;
1907
+ if (otherComponentType === wildcardMarker) continue;
1908
+ if (changeset.removes.has(otherComponentType)) continue;
1909
+ if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
1910
+ }
1911
+ const sparseData = archetype.getEntitySparseRelations(entityId);
1912
+ if (sparseData) for (const otherComponentType of sparseData.keys()) {
1913
+ if (otherComponentType === removedComponentType) continue;
1914
+ if (changeset.removes.has(otherComponentType)) continue;
1915
+ if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
1916
+ }
1917
+ for (const addedType of changeset.adds.keys()) {
1918
+ if (addedType === removedComponentType) continue;
1919
+ if (getComponentIdFromRelationId(addedType) === componentId) return;
1920
+ }
1921
+ changeset.delete(wildcardMarker);
1922
+ }
1923
+ function hasEntityComponent(archetype, entityId, componentType) {
1924
+ if (archetype.componentTypeSet.has(componentType)) return true;
1925
+ return archetype.getEntitySparseRelations(entityId)?.has(componentType) ?? false;
1926
+ }
1927
+ function pruneMissingRemovals(changeset, archetype, entityId) {
1928
+ let toPrune;
1929
+ for (const componentType of changeset.removes) if (!hasEntityComponent(archetype, entityId, componentType)) {
1930
+ if (toPrune === void 0) toPrune = [];
1931
+ toPrune.push(componentType);
1932
+ }
1933
+ if (toPrune !== void 0) for (const componentType of toPrune) changeset.removes.delete(componentType);
1934
+ }
1935
+ function hasArchetypeStructuralChange(changeset, currentArchetype) {
1936
+ for (const componentType of changeset.removes) if (!isSparseRelation(componentType) && currentArchetype.componentTypeSet.has(componentType)) return true;
1937
+ for (const componentType of changeset.adds.keys()) if (!isSparseRelation(componentType) && !currentArchetype.componentTypeSet.has(componentType)) return true;
1938
+ return false;
1939
+ }
1940
+ function buildFinalRegularComponentTypes(currentArchetype, changeset) {
1941
+ const finalRegularTypes = new Set(currentArchetype.componentTypes);
1942
+ for (const componentType of changeset.removes) if (!isSparseRelation(componentType)) finalRegularTypes.delete(componentType);
1943
+ for (const [componentType] of changeset.adds) if (!isSparseRelation(componentType)) finalRegularTypes.add(componentType);
1944
+ return Array.from(finalRegularTypes);
1945
+ }
1946
+ function applyChangeset(ctx, entityId, currentArchetype, changeset, entityToArchetype, removedComponents) {
1947
+ pruneMissingRemovals(changeset, currentArchetype, entityId);
1948
+ if (hasArchetypeStructuralChange(changeset, currentArchetype)) {
1949
+ const finalRegularTypes = buildFinalRegularComponentTypes(currentArchetype, changeset);
1950
+ const newArchetype = ctx.ensureArchetype(finalRegularTypes);
1951
+ const currentComponents = currentArchetype.removeEntity(entityId);
1952
+ if (removedComponents !== null) for (const componentType of changeset.removes) removedComponents.set(componentType, currentComponents.get(componentType));
1953
+ newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
1954
+ entityToArchetype.set(entityId, newArchetype);
1955
+ return newArchetype;
1956
+ }
1957
+ if (removedComponents !== null) applySparseChanges(ctx.sparseStore, entityId, changeset, removedComponents);
1958
+ else applySparseChangesNoHooks(ctx.sparseStore, entityId, changeset);
1959
+ for (const [componentType, component] of changeset.adds) {
1960
+ if (isSparseRelation(componentType)) continue;
1961
+ currentArchetype.set(entityId, componentType, component);
1962
+ }
1963
+ return currentArchetype;
1964
+ }
1965
+ function applySparseChanges(sparseStore, entityId, changeset, removedComponents) {
1966
+ for (const componentType of changeset.removes) if (isSparseRelation(componentType)) {
1967
+ const removedValue = sparseStore.getValue(entityId, componentType);
1968
+ if (removedValue !== void 0 || sparseStore.getAllForEntity(entityId).some(([t]) => t === componentType)) removedComponents.set(componentType, removedValue);
1969
+ sparseStore.deleteValue(entityId, componentType);
1970
+ }
1971
+ for (const [componentType, component] of changeset.adds) if (isSparseRelation(componentType)) sparseStore.setValue(entityId, componentType, component);
1972
+ }
1973
+ function applySparseChangesNoHooks(sparseStore, entityId, changeset) {
1974
+ for (const componentType of changeset.removes) if (isSparseRelation(componentType)) sparseStore.deleteValue(entityId, componentType);
1975
+ for (const [componentType, component] of changeset.adds) if (isSparseRelation(componentType)) sparseStore.setValue(entityId, componentType, component);
1976
+ }
1977
+ function filterRegularComponentTypes(componentTypes) {
1978
+ const regularTypes = [];
1979
+ for (const componentType of componentTypes) {
1980
+ if (isSparseWildcard(componentType)) {
1981
+ regularTypes.push(componentType);
1982
+ continue;
1983
+ }
1984
+ if (isSparseRelation(componentType)) continue;
1985
+ regularTypes.push(componentType);
1986
+ }
1987
+ return regularTypes;
1988
+ }
1989
+ //#endregion
1990
+ //#region src/world/archetype-manager.ts
1991
+ /**
1992
+ * Encapsulates all archetype storage, indexing, creation, removal, and reverse
1993
+ * referencing logic that was previously scattered as private methods + maps
1994
+ * directly on the World class.
1995
+ *
1996
+ * Responsibilities:
1997
+ * - Archetype memoization by signature
1998
+ * - Component-type reverse index (archetypesByComponent)
1999
+ * - Entity → current Archetype map
2000
+ * - Reverse "who references this entity via component/relation" index
2001
+ * - Creation + removal with proper notifications to QueryRegistry + hook matching
2002
+ * - Cleanup of empty archetypes after entity cascades
2003
+ *
2004
+ * This extraction shrinks World while keeping the same behavior and hot-path characteristics.
2005
+ */
2006
+ var ArchetypeManager = class {
2007
+ archetypes = [];
2008
+ archetypeBySignature = /* @__PURE__ */ new Map();
2009
+ entityToArchetype = /* @__PURE__ */ new Map();
2010
+ archetypesByComponent = /* @__PURE__ */ new Map();
2011
+ entityToReferencingArchetypes = /* @__PURE__ */ new Map();
2012
+ sparseStore;
2013
+ ctx;
2014
+ constructor(ctx, sparseStore) {
2015
+ this.ctx = ctx;
2016
+ this.sparseStore = sparseStore;
2017
+ }
2018
+ /** Primary entry point — memoized archetype creation/lookup. */
2019
+ ensureArchetype(componentTypes) {
2020
+ const sortedTypes = normalizeComponentTypes(filterRegularComponentTypes(componentTypes));
2021
+ const hashKey = this.createArchetypeSignature(sortedTypes);
2022
+ return getOrCompute(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
2023
+ }
2024
+ getArchetypeForEntity(entityId) {
2025
+ return this.entityToArchetype.get(entityId);
2026
+ }
2027
+ setEntityToArchetype(entityId, archetype) {
2028
+ this.entityToArchetype.set(entityId, archetype);
2029
+ }
2030
+ getMatchingArchetypes(componentTypes) {
2031
+ if (componentTypes.length === 0) return [...this.archetypes];
2032
+ const regularComponents = [];
2033
+ const wildcardRelations = [];
2034
+ for (const componentType of componentTypes) if (isWildcardRelationId(componentType)) {
2035
+ const componentId = getComponentIdFromRelationId(componentType);
2036
+ if (componentId !== void 0) wildcardRelations.push({
2037
+ componentId,
2038
+ relationId: componentType
2039
+ });
2040
+ } else regularComponents.push(componentType);
2041
+ let matchingArchetypes = this.getArchetypesWithComponents(regularComponents);
2042
+ for (const { componentId, relationId } of wildcardRelations) {
2043
+ const markerSet = this.archetypesByComponent.get(relationId);
2044
+ const archetypesWithMarker = markerSet ? Array.from(markerSet) : [];
2045
+ matchingArchetypes = matchingArchetypes.length === 0 ? archetypesWithMarker : matchingArchetypes.filter((a) => markerSet?.has(a) || a.hasRelationWithComponentId(componentId));
2046
+ }
2047
+ return matchingArchetypes;
2048
+ }
2049
+ getArchetypesWithComponents(componentTypes) {
2050
+ if (componentTypes.length === 0) return [...this.archetypes];
2051
+ if (componentTypes.length === 1) {
2052
+ const set = this.archetypesByComponent.get(componentTypes[0]);
2053
+ return set ? Array.from(set) : [];
2054
+ }
2055
+ const sets = componentTypes.map((type) => this.archetypesByComponent.get(type)).filter((s) => s !== void 0 && s.size > 0).sort((a, b) => a.size - b.size);
2056
+ if (sets.length === 0) return [];
2057
+ if (sets.length < componentTypes.length) return [];
2058
+ const smallest = sets[0];
2059
+ if (sets.length === 2) {
2060
+ const other = sets[1];
2061
+ return Array.from(smallest).filter((a) => other.has(a));
2062
+ }
2063
+ let result = new Set(smallest);
2064
+ for (let i = 1; i < sets.length; i++) {
2065
+ for (const item of result) if (!sets[i].has(item)) result.delete(item);
2066
+ if (result.size === 0) return [];
2067
+ }
2068
+ return Array.from(result);
2069
+ }
2070
+ createArchetypeSignature(componentTypes) {
2071
+ return componentTypes.join(",");
2072
+ }
2073
+ /** Deduplicated version of the original pair of methods. */
2074
+ updateReferencingIndex(componentType, archetype, isAdd) {
2075
+ const detailedType = getDetailedIdType(componentType);
2076
+ let entityId;
2077
+ if (detailedType.type === "entity") entityId = componentType;
2078
+ else if (detailedType.type === "entity-relation") entityId = detailedType.targetId;
2079
+ if (entityId !== void 0) {
2080
+ let refs = this.entityToReferencingArchetypes.get(entityId);
2081
+ if (isAdd) {
2082
+ if (!refs) {
2083
+ refs = /* @__PURE__ */ new Set();
2084
+ this.entityToReferencingArchetypes.set(entityId, refs);
2085
+ }
2086
+ refs.add(archetype);
2087
+ } else if (refs) {
2088
+ refs.delete(archetype);
2089
+ if (refs.size === 0) this.entityToReferencingArchetypes.delete(entityId);
2090
+ }
2091
+ }
2092
+ }
2093
+ createNewArchetype(componentTypes) {
2094
+ const newArchetype = new Archetype(componentTypes, this.sparseStore);
2095
+ this.archetypes.push(newArchetype);
2096
+ this.ctx.recordArchetypeCreated?.();
2097
+ for (const componentType of componentTypes) {
2098
+ let archetypes = this.archetypesByComponent.get(componentType);
2099
+ if (!archetypes) {
2100
+ archetypes = /* @__PURE__ */ new Set();
2101
+ this.archetypesByComponent.set(componentType, archetypes);
2102
+ }
2103
+ archetypes.add(newArchetype);
2104
+ this.updateReferencingIndex(componentType, newArchetype, true);
2105
+ }
2106
+ this.ctx.queryRegistry.onNewArchetype(newArchetype);
2107
+ this.updateArchetypeHookMatches(newArchetype);
2108
+ return newArchetype;
2109
+ }
2110
+ updateArchetypeHookMatches(archetype) {
2111
+ for (const entry of this.ctx.hooks) if (this.archetypeMatchesHook(archetype, entry)) {
2112
+ archetype.matchingMultiHooks.add(entry);
2113
+ if (entry.matchedArchetypes) entry.matchedArchetypes.add(archetype);
2114
+ }
2115
+ }
2116
+ archetypeMatchesHook(archetype, entry) {
2117
+ return entry.requiredComponents.every((c) => {
2118
+ if (isWildcardRelationId(c)) {
2119
+ if (isSparseWildcard(c)) return true;
2120
+ const componentId = getComponentIdFromRelationId(c);
2121
+ return componentId !== void 0 && archetype.hasRelationWithComponentId(componentId);
2122
+ }
2123
+ return archetype.componentTypeSet.has(c) || isSparseRelation(c);
2124
+ }) && matchesFilter(archetype, entry.filter);
2125
+ }
2126
+ /** Called during cascade deletion cleanup. */
2127
+ cleanupArchetypesReferencingEntity(entityId) {
2128
+ const refs = this.entityToReferencingArchetypes.get(entityId);
2129
+ if (!refs) return;
2130
+ for (const archetype of refs) if (archetype.getEntities().length === 0) this.removeArchetype(archetype);
2131
+ this.entityToReferencingArchetypes.delete(entityId);
2132
+ }
2133
+ removeArchetype(archetype) {
2134
+ const index = this.archetypes.indexOf(archetype);
2135
+ if (index !== -1) {
2136
+ const last = this.archetypes[this.archetypes.length - 1];
2137
+ this.archetypes[index] = last;
2138
+ this.archetypes.pop();
2139
+ }
2140
+ this.ctx.recordArchetypeRemoved?.();
2141
+ this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
2142
+ for (const componentType of archetype.componentTypes) {
2143
+ const archetypes = this.archetypesByComponent.get(componentType);
2144
+ if (archetypes) {
2145
+ archetypes.delete(archetype);
2146
+ if (archetypes.size === 0) this.archetypesByComponent.delete(componentType);
2147
+ }
2148
+ this.updateReferencingIndex(componentType, archetype, false);
2149
+ }
2150
+ this.ctx.queryRegistry.onArchetypeRemoved(archetype);
2151
+ }
2152
+ };
2153
+ //#endregion
2154
+ //#region src/commands/changeset.ts
2155
+ /**
2156
+ * @internal Represents a set of component changes to be applied to an entity
2157
+ */
2158
+ var ComponentChangeset = class {
2159
+ adds = /* @__PURE__ */ new Map();
2160
+ removes = /* @__PURE__ */ new Set();
2161
+ /**
2162
+ * Add a component to the changeset
2163
+ */
2164
+ set(componentType, component) {
2165
+ this.adds.set(componentType, component);
2166
+ this.removes.delete(componentType);
2167
+ }
2168
+ /**
2169
+ * Remove a component from the changeset
2170
+ */
2171
+ delete(componentType) {
2172
+ this.removes.add(componentType);
2173
+ this.adds.delete(componentType);
1835
2174
  }
1836
2175
  /**
1837
- * Registers a query so it receives future archetype notifications.
1838
- * Called automatically by the `Query` constructor via `world._registerQuery`.
2176
+ * Check if the changeset has any changes
1839
2177
  */
1840
- register(query) {
1841
- this.queries.add(query);
2178
+ hasChanges() {
2179
+ return this.adds.size > 0 || this.removes.size > 0;
1842
2180
  }
1843
2181
  /**
1844
- * Removes a query from the notification list.
1845
- * Called by `Query._disposeInternal` via `world._unregisterQuery`.
2182
+ * Clear all changes
1846
2183
  */
1847
- unregister(query) {
1848
- this.queries.delete(query);
2184
+ clear() {
2185
+ this.adds.clear();
2186
+ this.removes.clear();
1849
2187
  }
1850
2188
  /**
1851
- * Notifies all live queries that a new archetype has been created.
1852
- * Queries will add the archetype to their cache if it matches.
2189
+ * Merge another changeset into this one
1853
2190
  */
1854
- onNewArchetype(archetype) {
1855
- for (const query of this.queries) query.checkNewArchetype(archetype);
2191
+ merge(other) {
2192
+ for (const [componentType, component] of other.adds) {
2193
+ this.adds.set(componentType, component);
2194
+ this.removes.delete(componentType);
2195
+ }
2196
+ for (const componentType of other.removes) {
2197
+ this.removes.add(componentType);
2198
+ this.adds.delete(componentType);
2199
+ }
1856
2200
  }
1857
2201
  /**
1858
- * Notifies all live queries that an archetype has been destroyed.
1859
- * Queries will remove the archetype from their internal cache.
2202
+ * Apply the changeset to existing components and return the final state
1860
2203
  */
1861
- onArchetypeRemoved(archetype) {
1862
- for (const query of this.queries) query.removeArchetype(archetype);
2204
+ applyTo(existingComponents) {
2205
+ for (const componentType of this.removes) existingComponents.delete(componentType);
2206
+ for (const [componentType, component] of this.adds) existingComponents.set(componentType, component);
2207
+ return existingComponents;
1863
2208
  }
1864
2209
  };
1865
2210
  //#endregion
1866
- //#region src/world/commands.ts
1867
- function processCommands(entityId, currentArchetype, commands, changeset, handleExclusiveRelation) {
1868
- for (const command of commands) if (command.type === "set") processSetCommand(entityId, currentArchetype, command.componentType, command.component, changeset, handleExclusiveRelation);
1869
- else if (command.type === "delete") processDeleteCommand(entityId, currentArchetype, command.componentType, changeset);
1870
- }
1871
- function processSetCommand(entityId, currentArchetype, componentType, component, changeset, handleExclusiveRelation) {
1872
- const componentId = getComponentIdFromRelationId(componentType);
1873
- if (componentId !== void 0) {
1874
- handleExclusiveRelation(entityId, currentArchetype, componentId);
1875
- if (isDontFragmentComponent(componentId)) {
1876
- const wildcardMarker = relation(componentId, "*");
1877
- if (!currentArchetype.componentTypeSet.has(wildcardMarker)) changeset.set(wildcardMarker, void 0);
1878
- }
1879
- }
1880
- const merge = getComponentMerge(componentType);
1881
- if (merge !== void 0 && changeset.adds.has(componentType)) {
1882
- const prev = changeset.adds.get(componentType);
1883
- changeset.set(componentType, merge(prev, component));
1884
- return;
1885
- }
1886
- changeset.set(componentType, component);
1887
- }
1888
- function processDeleteCommand(entityId, currentArchetype, componentType, changeset) {
1889
- const componentId = getComponentIdFromRelationId(componentType);
1890
- if (isWildcardRelationId(componentType) && componentId !== void 0) removeWildcardRelations(entityId, currentArchetype, componentId, changeset);
1891
- else {
1892
- changeset.delete(componentType);
1893
- maybeRemoveWildcardMarker(entityId, currentArchetype, componentType, componentId, changeset);
1894
- }
1895
- }
1896
- function removeMatchingRelations(entityId, archetype, baseComponentId, changeset) {
1897
- for (const componentType of archetype.componentTypes) {
1898
- if (isWildcardRelationId(componentType)) continue;
1899
- if (getComponentIdFromRelationId(componentType) === baseComponentId) changeset.delete(componentType);
1900
- }
1901
- const dontFragmentData = archetype.getEntityDontFragmentRelations(entityId);
1902
- if (dontFragmentData) {
1903
- for (const componentType of dontFragmentData.keys()) if (getComponentIdFromRelationId(componentType) === baseComponentId) changeset.delete(componentType);
1904
- }
1905
- }
1906
- function removeWildcardRelations(entityId, currentArchetype, baseComponentId, changeset) {
1907
- removeMatchingRelations(entityId, currentArchetype, baseComponentId, changeset);
1908
- if (isDontFragmentComponent(baseComponentId)) changeset.delete(relation(baseComponentId, "*"));
1909
- }
1910
- function maybeRemoveWildcardMarker(entityId, archetype, removedComponentType, componentId, changeset) {
1911
- if (componentId === void 0 || !isDontFragmentComponent(componentId)) return;
1912
- const wildcardMarker = relation(componentId, "*");
1913
- for (const otherComponentType of archetype.componentTypes) {
1914
- if (otherComponentType === removedComponentType) continue;
1915
- if (otherComponentType === wildcardMarker) continue;
1916
- if (changeset.removes.has(otherComponentType)) continue;
1917
- if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
1918
- }
1919
- const dontFragmentData = archetype.getEntityDontFragmentRelations(entityId);
1920
- if (dontFragmentData) for (const otherComponentType of dontFragmentData.keys()) {
1921
- if (otherComponentType === removedComponentType) continue;
1922
- if (changeset.removes.has(otherComponentType)) continue;
1923
- if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
1924
- }
1925
- for (const addedType of changeset.adds.keys()) {
1926
- if (addedType === removedComponentType) continue;
1927
- if (getComponentIdFromRelationId(addedType) === componentId) return;
1928
- }
1929
- changeset.delete(wildcardMarker);
1930
- }
1931
- function hasEntityComponent(archetype, entityId, componentType) {
1932
- if (archetype.componentTypeSet.has(componentType)) return true;
1933
- return archetype.getEntityDontFragmentRelations(entityId)?.has(componentType) ?? false;
1934
- }
1935
- function pruneMissingRemovals(changeset, archetype, entityId) {
1936
- let toPrune;
1937
- for (const componentType of changeset.removes) if (!hasEntityComponent(archetype, entityId, componentType)) {
1938
- if (toPrune === void 0) toPrune = [];
1939
- toPrune.push(componentType);
1940
- }
1941
- if (toPrune !== void 0) for (const componentType of toPrune) changeset.removes.delete(componentType);
1942
- }
1943
- 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;
1946
- return false;
1947
- }
1948
- function buildFinalRegularComponentTypes(currentArchetype, changeset) {
1949
- 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);
1952
- return Array.from(finalRegularTypes);
1953
- }
1954
- function applyChangeset(ctx, entityId, currentArchetype, changeset, entityToArchetype, removedComponents) {
1955
- pruneMissingRemovals(changeset, currentArchetype, entityId);
1956
- if (hasArchetypeStructuralChange(changeset, currentArchetype)) {
1957
- const finalRegularTypes = buildFinalRegularComponentTypes(currentArchetype, changeset);
1958
- const newArchetype = ctx.ensureArchetype(finalRegularTypes);
1959
- const currentComponents = currentArchetype.removeEntity(entityId);
1960
- if (removedComponents !== null) for (const componentType of changeset.removes) removedComponents.set(componentType, currentComponents.get(componentType));
1961
- newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
1962
- entityToArchetype.set(entityId, newArchetype);
1963
- return newArchetype;
1964
- }
1965
- if (removedComponents !== null) applyDontFragmentChanges(ctx.dontFragmentStore, entityId, changeset, removedComponents);
1966
- else applyDontFragmentChangesNoHooks(ctx.dontFragmentStore, entityId, changeset);
1967
- for (const [componentType, component] of changeset.adds) {
1968
- if (isDontFragmentRelation(componentType)) continue;
1969
- currentArchetype.set(entityId, componentType, component);
1970
- }
1971
- return currentArchetype;
1972
- }
2211
+ //#region src/world/hooks.ts
1973
2212
  /**
1974
- * No-hooks variant of applyDontFragmentChanges that skips tracking removed component data.
1975
- *
1976
- * Rewritten for the new DontFragmentStore interface (ComponentId-primary storage).
2213
+ * Debug-only counter incremented on every invokeHook call when armed.
2214
+ * World reads and resets this during armed syncs.
1977
2215
  */
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);
1983
- }
1984
- for (const [componentType, component] of changeset.adds) if (isDontFragmentRelation(componentType)) dontFragmentRelations.setValue(entityId, componentType, component);
1985
- }
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);
1989
- }
1990
- function filterRegularComponentTypes(componentTypes) {
1991
- const regularTypes = [];
1992
- for (const componentType of componentTypes) {
1993
- if (isDontFragmentWildcard(componentType)) {
1994
- regularTypes.push(componentType);
1995
- continue;
1996
- }
1997
- if (isDontFragmentRelation(componentType)) continue;
1998
- regularTypes.push(componentType);
1999
- }
2000
- return regularTypes;
2001
- }
2002
- //#endregion
2003
- //#region src/world/hooks.ts
2216
+ const debugHookExecutionCounter = { value: 0 };
2004
2217
  /**
2005
2218
  * Unified hook invocation: prefers entry.callback (callback style) over hook.on_* (object style).
2006
2219
  */
2007
2220
  function invokeHook(entry, event, entityId, components) {
2221
+ debugHookExecutionCounter.value++;
2008
2222
  if (entry.callback) {
2009
2223
  entry.callback(event, entityId, ...components);
2010
2224
  return;
@@ -2301,11 +2515,275 @@ function getEntityReferences(entityReferences, targetEntityId) {
2301
2515
  return entityReferences.get(targetEntityId) ?? new MultiMap();
2302
2516
  }
2303
2517
  //#endregion
2518
+ //#region src/world/command-executor.ts
2519
+ /**
2520
+ * Encapsulates the command execution pipeline, reusable changesets,
2521
+ * and related orchestration that was previously private methods + fields on World.
2522
+ *
2523
+ * Responsibilities:
2524
+ * - executeEntityCommands (routing for singletons / destroy / structural changes)
2525
+ * - applyEntityCommands (changeset processing + exclusive relations + apply + refs + hooks)
2526
+ * - removeComponentImmediate (used by cascade deletion)
2527
+ * - updateEntityReferences (keeps the reverse index in sync)
2528
+ *
2529
+ * This extraction significantly reduces World line count while preserving
2530
+ * every fast-path branch and allocation-avoidance characteristic.
2531
+ */
2532
+ var CommandExecutor = class {
2533
+ ctx;
2534
+ _changeset = new ComponentChangeset();
2535
+ _removeChangeset = new ComponentChangeset();
2536
+ _commandCtx;
2537
+ _hooksCtx;
2538
+ constructor(ctx) {
2539
+ this.ctx = ctx;
2540
+ this._commandCtx = {
2541
+ sparseStore: ctx.sparseStore,
2542
+ ensureArchetype: ctx.ensureArchetype
2543
+ };
2544
+ this._hooksCtx = {
2545
+ multiHooks: ctx.hooks,
2546
+ has: ctx.has,
2547
+ get: ctx.get,
2548
+ getOptional: ctx.getOptional
2549
+ };
2550
+ }
2551
+ /**
2552
+ * Entry point used by the CommandBuffer.
2553
+ * Routes to singleton handling, destroy fast path, or structural apply.
2554
+ */
2555
+ executeEntityCommands(entityId, commands) {
2556
+ this._changeset.clear();
2557
+ if (this.ctx.componentEntities.exists(entityId)) {
2558
+ this.ctx.componentEntities.executeCommands(entityId, commands);
2559
+ return;
2560
+ }
2561
+ if (commands.some((cmd) => cmd.type === "destroy")) {
2562
+ this.ctx.destroyEntityImmediate(entityId);
2563
+ return;
2564
+ }
2565
+ this.applyEntityCommands(entityId, commands);
2566
+ }
2567
+ applyEntityCommands(entityId, commands) {
2568
+ const currentArchetype = this.ctx.entityToArchetype.get(entityId);
2569
+ if (!currentArchetype) return;
2570
+ const changeset = this._changeset;
2571
+ processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
2572
+ if (isExclusiveComponent(compId)) removeMatchingRelations(eid, arch, compId, changeset);
2573
+ });
2574
+ const hasStructuralChange = changeset.removes.size > 0 || changeset.adds.size > 0;
2575
+ if (this.ctx.hooks.size === 0) {
2576
+ const newArchetype = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.ctx.entityToArchetype, null);
2577
+ if (hasStructuralChange) this.updateEntityReferences(entityId, changeset);
2578
+ if (newArchetype !== currentArchetype) this.ctx.incrementMigrations();
2579
+ return;
2580
+ }
2581
+ const removedComponents = /* @__PURE__ */ new Map();
2582
+ const newArchetype = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.ctx.entityToArchetype, removedComponents);
2583
+ if (hasStructuralChange) this.updateEntityReferences(entityId, changeset);
2584
+ if (newArchetype !== currentArchetype) this.ctx.incrementMigrations();
2585
+ this.ctx.triggerLifecycleHooks(this._hooksCtx, entityId, changeset.adds, removedComponents, currentArchetype, newArchetype);
2586
+ }
2587
+ /**
2588
+ * Immediate (non-buffered) component removal used during cascade deletion.
2589
+ * Called from destroy* paths (which remain in World).
2590
+ */
2591
+ removeComponentImmediate(entityId, componentType, targetEntityId) {
2592
+ const sourceArchetype = this.ctx.entityToArchetype.get(entityId);
2593
+ if (!sourceArchetype) return;
2594
+ const changeset = this._removeChangeset;
2595
+ changeset.clear();
2596
+ changeset.delete(componentType);
2597
+ maybeRemoveWildcardMarker(entityId, sourceArchetype, componentType, getComponentIdFromRelationId(componentType), changeset);
2598
+ const removedComponent = sourceArchetype.get(entityId, componentType);
2599
+ const newArchetype = applyChangeset(this._commandCtx, entityId, sourceArchetype, changeset, this.ctx.entityToArchetype, null);
2600
+ untrackEntityReference(this.ctx.entityReferences, entityId, componentType, targetEntityId);
2601
+ this.ctx.triggerLifecycleHooks(this._hooksCtx, entityId, /* @__PURE__ */ new Map(), new Map([[componentType, removedComponent]]), sourceArchetype, newArchetype);
2602
+ }
2603
+ /**
2604
+ * Keeps the entity reference reverse index in sync after structural changes.
2605
+ * Called from apply paths.
2606
+ */
2607
+ updateEntityReferences(entityId, changeset) {
2608
+ for (const componentType of changeset.removes) if (isEntityRelation(componentType)) {
2609
+ const targetId = getTargetIdFromRelationId(componentType);
2610
+ untrackEntityReference(this.ctx.entityReferences, entityId, componentType, targetId);
2611
+ } else if (componentType >= 1024) untrackEntityReference(this.ctx.entityReferences, entityId, componentType, componentType);
2612
+ for (const [componentType] of changeset.adds) if (isEntityRelation(componentType)) {
2613
+ const targetId = getTargetIdFromRelationId(componentType);
2614
+ trackEntityReference(this.ctx.entityReferences, entityId, componentType, targetId);
2615
+ } else if (componentType >= 1024) trackEntityReference(this.ctx.entityReferences, entityId, componentType, componentType);
2616
+ }
2617
+ /**
2618
+ * Exposed for any future direct needs (currently not required outside the executor).
2619
+ */
2620
+ getHooksContext() {
2621
+ return this._hooksCtx;
2622
+ }
2623
+ };
2624
+ //#endregion
2625
+ //#region src/world/debug-stats.ts
2626
+ /**
2627
+ * Manages debug stats collectors and transient activity counters for World#sync().
2628
+ *
2629
+ * Extracted from World to shrink the main class while keeping the entire debug/observability
2630
+ * path isolated, zero-cost when no collectors are active, and easy to test/maintain.
2631
+ *
2632
+ * Follows the same context/callback injection style as ArchetypeManager, CommandProcessorContext,
2633
+ * and HooksContext to avoid tight coupling.
2634
+ *
2635
+ * All collectors receive the *exact same* stats object for a given sync (as before).
2636
+ * Exceptions in user callbacks are swallowed (as before).
2637
+ */
2638
+ var DebugStatsManager = class {
2639
+ collectors = /* @__PURE__ */ new Set();
2640
+ migrations = 0;
2641
+ archetypesCreated = 0;
2642
+ archetypesRemoved = 0;
2643
+ /** Fast check used to arm timing + reset + counting in hot paths. */
2644
+ hasActiveCollectors() {
2645
+ return this.collectors.size > 0;
2646
+ }
2647
+ /**
2648
+ * Registers a collector. Returns a disposable handle (supports `using`).
2649
+ * Collection stops when the handle is disposed.
2650
+ */
2651
+ createCollector(callback) {
2652
+ this.collectors.add(callback);
2653
+ return { [Symbol.dispose]: () => {
2654
+ this.collectors.delete(callback);
2655
+ } };
2656
+ }
2657
+ recordArchetypeCreated() {
2658
+ if (this.hasActiveCollectors()) this.archetypesCreated++;
2659
+ }
2660
+ recordArchetypeRemoved() {
2661
+ if (this.hasActiveCollectors()) this.archetypesRemoved++;
2662
+ }
2663
+ incrementMigrations() {
2664
+ if (this.hasActiveCollectors()) this.migrations++;
2665
+ }
2666
+ /** Reset all activity counters + the shared hook execution counter. Called at start of an armed sync. */
2667
+ resetActivity() {
2668
+ this.migrations = 0;
2669
+ this.archetypesCreated = 0;
2670
+ this.archetypesRemoved = 0;
2671
+ debugHookExecutionCounter.value = 0;
2672
+ }
2673
+ /**
2674
+ * Build and deliver a SyncDebugStats payload to every active collector.
2675
+ * World supplies the pre-computed snapshot numbers (keeps debug manager decoupled from
2676
+ * internal World maps/registries while preserving exact original stats shape and values).
2677
+ */
2678
+ deliver(timings, data) {
2679
+ const stats = {
2680
+ timestamps: {
2681
+ syncStart: timings.syncStart,
2682
+ syncEnd: timings.syncEnd,
2683
+ commandBufferStart: timings.commandBufferStart,
2684
+ commandBufferEnd: timings.commandBufferEnd
2685
+ },
2686
+ commandIterations: timings.commandIterations,
2687
+ entities: {
2688
+ total: data.entityCount,
2689
+ freelistSize: data.freelistSize,
2690
+ nextId: data.nextId
2691
+ },
2692
+ archetypes: {
2693
+ total: data.archetypeCount,
2694
+ empty: data.emptyArchetypes
2695
+ },
2696
+ queries: {
2697
+ cached: data.cachedQueryCount,
2698
+ registered: data.registeredQueryCount
2699
+ },
2700
+ hooks: { total: data.hookCount },
2701
+ indices: {
2702
+ entityReferences: data.entityReferencesSize,
2703
+ entityToReferencingArchetypes: data.entityToReferencingArchetypesSize,
2704
+ archetypesByComponent: data.archetypesByComponentSize
2705
+ },
2706
+ activity: {
2707
+ migrations: this.migrations,
2708
+ hooksExecuted: debugHookExecutionCounter.value,
2709
+ archetypesCreated: this.archetypesCreated,
2710
+ archetypesRemoved: this.archetypesRemoved
2711
+ }
2712
+ };
2713
+ for (const cb of this.collectors) try {
2714
+ cb(stats);
2715
+ } catch {}
2716
+ }
2717
+ };
2718
+ //#endregion
2719
+ //#region src/world/operations.ts
2720
+ /**
2721
+ * Validation and overload-resolution helpers extracted from World.
2722
+ *
2723
+ * These were previously private methods on World. Moving them reduces line count
2724
+ * in the core class with almost zero coupling (the only dep is a liveness predicate
2725
+ * for assertEntityExists, supplied by the caller).
2726
+ *
2727
+ * Pure type checks (assert*TypeValid) and the resolve* helpers for set/remove
2728
+ * overloads live here.
2729
+ */
2730
+ /**
2731
+ * Assert that an entity (or component-entity) is alive in the world.
2732
+ * The caller supplies the liveness check (World.exists or equivalent) to keep
2733
+ * this module free of direct references to stores.
2734
+ */
2735
+ function assertEntityExists(entityId, label, exists) {
2736
+ if (!exists(entityId)) throw new Error(`${label} ${entityId} does not exist`);
2737
+ }
2738
+ function assertComponentTypeValid(componentType) {
2739
+ if (getDetailedIdType(componentType).type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
2740
+ }
2741
+ function assertSetComponentTypeValid(componentType) {
2742
+ const detailedType = getDetailedIdType(componentType);
2743
+ if (detailedType.type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
2744
+ if (detailedType.type === "wildcard-relation") throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
2745
+ }
2746
+ /**
2747
+ * Resolve the (entity, componentType, value) for a set() call.
2748
+ */
2749
+ function resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent, exists = () => true) {
2750
+ const targetEntityId = entityId;
2751
+ const componentType = componentTypeOrComponent;
2752
+ assertEntityExists(targetEntityId, "Entity", exists);
2753
+ assertSetComponentTypeValid(componentType);
2754
+ return {
2755
+ entityId: targetEntityId,
2756
+ componentType,
2757
+ component: maybeComponent
2758
+ };
2759
+ }
2760
+ /**
2761
+ * Resolve the (entity, componentType) for a remove() call, handling the
2762
+ * singleton component overload (remove(componentId)).
2763
+ */
2764
+ function resolveRemoveOperation(entityId, componentType, exists = () => true) {
2765
+ if (componentType === void 0) {
2766
+ const componentId = entityId;
2767
+ assertEntityExists(componentId, "Component entity", exists);
2768
+ return {
2769
+ entityId: componentId,
2770
+ componentType: componentId
2771
+ };
2772
+ }
2773
+ const targetEntityId = entityId;
2774
+ assertEntityExists(targetEntityId, "Entity", exists);
2775
+ assertComponentTypeValid(componentType);
2776
+ return {
2777
+ entityId: targetEntityId,
2778
+ componentType
2779
+ };
2780
+ }
2781
+ //#endregion
2304
2782
  //#region src/storage/serialization.ts
2305
2783
  /**
2306
- * Encode an internal EntityId into a SerializedEntityId for snapshots
2784
+ * Core encoding logic (no cache). Extracted so cached wrapper can reuse it without duplication.
2307
2785
  */
2308
- function encodeEntityId(id) {
2786
+ function encodeEntityIdCore(id) {
2309
2787
  const detailed = getDetailedIdType(id);
2310
2788
  switch (detailed.type) {
2311
2789
  case "component": {
@@ -2343,6 +2821,20 @@ function encodeEntityId(id) {
2343
2821
  }
2344
2822
  }
2345
2823
  /**
2824
+ * Encode an EntityId, using an optional cache Map to avoid repeated getDetailedIdType
2825
+ * + name lookup work for IDs that appear many times (typical during full world snapshot).
2826
+ */
2827
+ function encodeEntityIdCached(id, cache) {
2828
+ if (cache) {
2829
+ const cached = cache.get(id);
2830
+ if (cached !== void 0) return cached;
2831
+ const result = encodeEntityIdCore(id);
2832
+ cache.set(id, result);
2833
+ return result;
2834
+ }
2835
+ return encodeEntityIdCore(id);
2836
+ }
2837
+ /**
2346
2838
  * Decode a SerializedEntityId back into an internal EntityId
2347
2839
  */
2348
2840
  function decodeSerializedId(sid) {
@@ -2383,24 +2875,16 @@ function decodeSerializedId(sid) {
2383
2875
  * Serializes the full world state to a plain JS object suitable for JSON encoding.
2384
2876
  */
2385
2877
  function serializeWorld(archetypes, componentEntities, entityIdManager) {
2878
+ const idCache = /* @__PURE__ */ new Map();
2386
2879
  const entities = [];
2387
2880
  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
- });
2881
+ const encodedComponentTypes = archetype.componentTypes.map((t) => encodeEntityIdCached(t, idCache));
2882
+ archetype.appendSerializedEntities(entities, (id) => encodeEntityIdCached(id, idCache), encodedComponentTypes);
2396
2883
  }
2397
2884
  const componentEntitiesArr = [];
2398
2885
  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
- }))
2886
+ id: encodeEntityIdCached(entityId, idCache),
2887
+ components: serializeComponentsFromMap(components, idCache)
2404
2888
  });
2405
2889
  return {
2406
2890
  version: 1,
@@ -2409,6 +2893,15 @@ function serializeWorld(archetypes, componentEntities, entityIdManager) {
2409
2893
  componentEntities: componentEntitiesArr
2410
2894
  };
2411
2895
  }
2896
+ /** Small helper to avoid duplicating the "Map → SerializedComponent[] with cache" pattern. */
2897
+ function serializeComponentsFromMap(components, idCache) {
2898
+ const result = [];
2899
+ for (const [rawType, value] of components) result.push({
2900
+ type: encodeEntityIdCached(rawType, idCache),
2901
+ value: value === MISSING_COMPONENT ? void 0 : value
2902
+ });
2903
+ return result;
2904
+ }
2412
2905
  /**
2413
2906
  * Restores world state from a snapshot into the provided context.
2414
2907
  * Intended to be called from `World`'s constructor.
@@ -2430,12 +2923,11 @@ function deserializeWorld(ctx, snapshot) {
2430
2923
  const entityId = decodeSerializedId(entry.id);
2431
2924
  const componentsArray = entry.components || [];
2432
2925
  const componentMap = /* @__PURE__ */ new Map();
2433
- const componentTypes = [];
2434
2926
  for (const componentEntry of componentsArray) {
2435
2927
  const componentType = decodeSerializedId(componentEntry.type);
2436
2928
  componentMap.set(componentType, componentEntry.value);
2437
- componentTypes.push(componentType);
2438
2929
  }
2930
+ const componentTypes = Array.from(componentMap.keys());
2439
2931
  const archetype = ctx.ensureArchetype(componentTypes);
2440
2932
  archetype.addEntity(entityId, componentMap);
2441
2933
  ctx.setEntityToArchetype(entityId, archetype);
@@ -2454,45 +2946,60 @@ function deserializeWorld(ctx, snapshot) {
2454
2946
  */
2455
2947
  var World = class {
2456
2948
  entityIdManager = new EntityIdManager();
2457
- archetypes = [];
2458
- archetypeBySignature = /* @__PURE__ */ new Map();
2459
- entityToArchetype = /* @__PURE__ */ new Map();
2460
- archetypesByComponent = /* @__PURE__ */ new Map();
2461
2949
  entityReferences = /* @__PURE__ */ new Map();
2462
- /** Reverse index: entity ID set of archetypes whose componentTypes include that entity ID */
2463
- entityToReferencingArchetypes = /* @__PURE__ */ new Map();
2464
- /** DontFragment relation storage, shared with all Archetype instances */
2465
- dontFragmentStore = new DontFragmentStoreImpl();
2950
+ /** Sparse relation storage (for components created with `sparse: true`), shared with all Archetype instances */
2951
+ sparseStore = new SparseStoreImpl();
2466
2952
  /** Component entity (singleton) storage */
2467
2953
  componentEntities = new ComponentEntityStore();
2954
+ archetypeManager;
2955
+ get archetypes() {
2956
+ return this.archetypeManager.archetypes;
2957
+ }
2958
+ get entityToArchetype() {
2959
+ return this.archetypeManager.entityToArchetype;
2960
+ }
2961
+ get archetypesByComponent() {
2962
+ return this.archetypeManager.archetypesByComponent;
2963
+ }
2964
+ get entityToReferencingArchetypes() {
2965
+ return this.archetypeManager.entityToReferencingArchetypes;
2966
+ }
2468
2967
  queryRegistry = new QueryRegistry();
2469
2968
  hooks = /* @__PURE__ */ new Set();
2470
- commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
2471
- _changeset = new ComponentChangeset();
2472
- _removeChangeset = new ComponentChangeset();
2473
- /** Cached command processor context to avoid per-entity object allocation */
2474
- _commandCtx = {
2475
- dontFragmentStore: this.dontFragmentStore,
2476
- ensureArchetype: (ct) => this.ensureArchetype(ct)
2477
- };
2478
- /** Cached hooks context to avoid per-entity object allocation */
2479
- _hooksCtx = {
2480
- multiHooks: this.hooks,
2481
- has: (eid, ct) => this.has(eid, ct),
2482
- get: (eid, ct) => this.get(eid, ct),
2483
- getOptional: (eid, ct) => this.getOptional(eid, ct)
2484
- };
2969
+ debugStats = new DebugStatsManager();
2970
+ commandBuffer;
2971
+ commandExecutor;
2485
2972
  constructor(snapshot) {
2973
+ this.archetypeManager = new ArchetypeManager({
2974
+ queryRegistry: this.queryRegistry,
2975
+ hooks: this.hooks,
2976
+ recordArchetypeCreated: () => this.debugStats.recordArchetypeCreated(),
2977
+ recordArchetypeRemoved: () => this.debugStats.recordArchetypeRemoved()
2978
+ }, this.sparseStore);
2486
2979
  if (snapshot && typeof snapshot === "object") deserializeWorld({
2487
2980
  entityIdManager: this.entityIdManager,
2488
2981
  componentEntities: this.componentEntities,
2489
2982
  entityReferences: this.entityReferences,
2490
2983
  ensureArchetype: (ct) => this.ensureArchetype(ct),
2491
- setEntityToArchetype: (eid, arch) => this.entityToArchetype.set(eid, arch)
2984
+ setEntityToArchetype: (eid, arch) => this.archetypeManager.entityToArchetype.set(eid, arch)
2492
2985
  }, snapshot);
2493
- }
2494
- createArchetypeSignature(componentTypes) {
2495
- return componentTypes.join(",");
2986
+ const execCtx = {
2987
+ componentEntities: this.componentEntities,
2988
+ entityReferences: this.entityReferences,
2989
+ hooks: this.hooks,
2990
+ entityToArchetype: this.entityToArchetype,
2991
+ ensureArchetype: (ct) => this.ensureArchetype(ct),
2992
+ sparseStore: this.sparseStore,
2993
+ has: (eid, ct) => this.has(eid, ct),
2994
+ get: (eid, ct) => this.get(eid, ct),
2995
+ getOptional: (eid, ct) => this.getOptional(eid, ct),
2996
+ destroyEntityImmediate: (eid) => this.destroyEntityImmediate(eid),
2997
+ incrementMigrations: () => this.debugStats.incrementMigrations(),
2998
+ triggerLifecycleHooks,
2999
+ triggerRemoveHooksForEntityDeletion
3000
+ };
3001
+ this.commandExecutor = new CommandExecutor(execCtx);
3002
+ this.commandBuffer = new CommandBuffer((entityId, commands) => this.commandExecutor.executeEntityCommands(entityId, commands));
2496
3003
  }
2497
3004
  /**
2498
3005
  * Creates a new entity.
@@ -2588,64 +3095,12 @@ var World = class {
2588
3095
  if (this.componentEntities.exists(entityId)) return true;
2589
3096
  return this.entityToArchetype.has(entityId);
2590
3097
  }
2591
- assertEntityExists(entityId, label) {
2592
- if (!this.exists(entityId)) throw new Error(`${label} ${entityId} does not exist`);
2593
- }
2594
- assertComponentTypeValid(componentType) {
2595
- if (getDetailedIdType(componentType).type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
2596
- }
2597
- assertSetComponentTypeValid(componentType) {
2598
- const detailedType = getDetailedIdType(componentType);
2599
- if (detailedType.type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
2600
- if (detailedType.type === "wildcard-relation") throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
2601
- }
2602
- resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent) {
2603
- if (maybeComponent === void 0 && componentTypeOrComponent !== void 0) {
2604
- const detailedType = getDetailedIdType(entityId);
2605
- if (detailedType.type === "component" || detailedType.type === "component-relation") {
2606
- const componentId = entityId;
2607
- this.assertEntityExists(componentId, "Component entity");
2608
- this.assertSetComponentTypeValid(componentId);
2609
- return {
2610
- entityId: componentId,
2611
- componentType: componentId,
2612
- component: componentTypeOrComponent
2613
- };
2614
- }
2615
- }
2616
- const targetEntityId = entityId;
2617
- const componentType = componentTypeOrComponent;
2618
- this.assertEntityExists(targetEntityId, "Entity");
2619
- this.assertSetComponentTypeValid(componentType);
2620
- return {
2621
- entityId: targetEntityId,
2622
- componentType,
2623
- component: maybeComponent
2624
- };
2625
- }
2626
- resolveRemoveOperation(entityId, componentType) {
2627
- if (componentType === void 0) {
2628
- const componentId = entityId;
2629
- this.assertEntityExists(componentId, "Component entity");
2630
- return {
2631
- entityId: componentId,
2632
- componentType: componentId
2633
- };
2634
- }
2635
- const targetEntityId = entityId;
2636
- this.assertEntityExists(targetEntityId, "Entity");
2637
- this.assertComponentTypeValid(componentType);
2638
- return {
2639
- entityId: targetEntityId,
2640
- componentType
2641
- };
2642
- }
2643
3098
  set(entityId, componentTypeOrComponent, maybeComponent) {
2644
- const { entityId: targetEntityId, componentType, component } = this.resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent);
3099
+ const { entityId: targetEntityId, componentType, component } = resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent, (id) => this.exists(id));
2645
3100
  this.commandBuffer.set(targetEntityId, componentType, component);
2646
3101
  }
2647
3102
  remove(entityId, componentType) {
2648
- const { entityId: targetEntityId, componentType: targetComponentType } = this.resolveRemoveOperation(entityId, componentType);
3103
+ const { entityId: targetEntityId, componentType: targetComponentType } = resolveRemoveOperation(entityId, componentType, (id) => this.exists(id));
2649
3104
  this.commandBuffer.remove(targetEntityId, targetComponentType);
2650
3105
  }
2651
3106
  /**
@@ -2662,6 +3117,30 @@ var World = class {
2662
3117
  delete(entityId) {
2663
3118
  this.commandBuffer.delete(entityId);
2664
3119
  }
3120
+ /**
3121
+ * Returns an explicit handle for a singleton component (component-as-entity).
3122
+ *
3123
+ * This is the preferred API for singleton components.
3124
+ *
3125
+ * @example
3126
+ * const config = world.singleton(GlobalConfig);
3127
+ * config.set({ debug: true });
3128
+ * world.sync();
3129
+ * console.log(config.get());
3130
+ */
3131
+ singleton(componentId) {
3132
+ assertEntityExists(componentId, "Component entity", (id) => this.exists(id));
3133
+ assertSetComponentTypeValid(componentId);
3134
+ return new SingletonHandle(componentId, {
3135
+ has: () => this.componentEntities.hasSingleton(componentId),
3136
+ get: () => this.get(componentId),
3137
+ getOptional: () => this.getOptional(componentId),
3138
+ remove: () => this.commandBuffer.remove(componentId, componentId),
3139
+ set: (value) => {
3140
+ this.commandBuffer.set(componentId, componentId, value);
3141
+ }
3142
+ });
3143
+ }
2665
3144
  has(entityId, componentType) {
2666
3145
  if (componentType === void 0) {
2667
3146
  const componentId = entityId;
@@ -2678,9 +3157,9 @@ var World = class {
2678
3157
  const archetype = this.entityToArchetype.get(entityId);
2679
3158
  if (!archetype) return false;
2680
3159
  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);
3160
+ if (isSparseRelation(componentType)) {
3161
+ if (this.sparseStore.getValue(entityId, componentType) !== void 0) return true;
3162
+ return this.sparseStore.getAllForEntity(entityId).some(([t]) => t === componentType);
2684
3163
  }
2685
3164
  return false;
2686
3165
  }
@@ -2693,8 +3172,8 @@ var World = class {
2693
3172
  if (!archetype) throw new Error(`Entity ${entityId} does not exist`);
2694
3173
  if (componentType >= 0 || componentType % RELATION_SHIFT !== 0) {
2695
3174
  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().`);
3175
+ const hasSparse = isSparseRelation(componentType);
3176
+ 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
3177
  }
2699
3178
  return archetype.get(entityId, componentType);
2700
3179
  }
@@ -2716,6 +3195,163 @@ var World = class {
2716
3195
  }
2717
3196
  return archetype.getOptional(entityId, componentType);
2718
3197
  }
3198
+ /**
3199
+ * Retrieves all targets (and their associated data) for relations of a given
3200
+ * base component on an entity.
3201
+ *
3202
+ * This is the ergonomic replacement for the common pattern:
3203
+ * world.get(entity, relation(Comp, "*"))
3204
+ *
3205
+ * @example
3206
+ * const ChildOf = component({ exclusive: true, sparse: true });
3207
+ * const children = world.getRelationTargets(parent, ChildOf); // usually []
3208
+ * const items = world.getRelationTargets(player, InInventory);
3209
+ *
3210
+ * // For common hierarchy use cases, prefer the higher-level helpers:
3211
+ * // world.getChildren(parent, ChildOf), world.getParent(child, ChildOf)
3212
+ */
3213
+ getRelationTargets(entityId, relationComp) {
3214
+ assertEntityExists(entityId, "Entity", (id) => this.exists(id));
3215
+ const wildcard = relation(relationComp, "*");
3216
+ if (this.componentEntities.exists(entityId)) return this.componentEntities.getWildcard(entityId, wildcard);
3217
+ return this.get(entityId, wildcard);
3218
+ }
3219
+ /**
3220
+ * Returns every entity that currently holds a relation of the given base
3221
+ * component pointing at `targetId`.
3222
+ *
3223
+ * This is the efficient **reverse** lookup. For common hierarchy cases,
3224
+ * prefer the higher-level `world.getChildren(parent, ChildOf)` instead.
3225
+ *
3226
+ * @example
3227
+ * const ChildOf = component({ exclusive: true, sparse: true });
3228
+ * const directChildren = world.getRelationSources(ship, ChildOf);
3229
+ */
3230
+ getRelationSources(targetId, relationComp) {
3231
+ const refs = getEntityReferences(this.entityReferences, targetId);
3232
+ const result = [];
3233
+ for (const [source, relType] of refs) {
3234
+ if (!this.entityToArchetype.has(source) && !this.componentEntities.exists(source)) continue;
3235
+ if (getComponentIdFromRelationId(relType) === relationComp) result.push(source);
3236
+ }
3237
+ return result;
3238
+ }
3239
+ /**
3240
+ * Returns true if the entity has any (or a specific-target) relation of the
3241
+ * given base component.
3242
+ */
3243
+ hasRelation(entityId, relationComp, targetId) {
3244
+ assertEntityExists(entityId, "Entity", (id) => this.exists(id));
3245
+ if (targetId !== void 0) {
3246
+ const specific = relation(relationComp, targetId);
3247
+ return this.has(entityId, specific);
3248
+ }
3249
+ return this.getRelationTargets(entityId, relationComp).length > 0;
3250
+ }
3251
+ /**
3252
+ * Returns the number of relations of the given base component held by the entity.
3253
+ */
3254
+ countRelations(entityId, relationComp) {
3255
+ assertEntityExists(entityId, "Entity", (id) => this.exists(id));
3256
+ return this.getRelationTargets(entityId, relationComp).length;
3257
+ }
3258
+ /**
3259
+ * For an *exclusive* relation (e.g. ChildOf, Owner), returns the single
3260
+ * target entity (or undefined if none).
3261
+ *
3262
+ * When the component was declared `exclusive: true`, this is the preferred
3263
+ * accessor (clearer intent than array destructuring).
3264
+ */
3265
+ getSingleRelationTarget(entityId, relationComp) {
3266
+ const targets = this.getRelationTargets(entityId, relationComp);
3267
+ return targets.length > 0 ? targets[0][0] : void 0;
3268
+ }
3269
+ /**
3270
+ * Returns the direct children of `parent` for the given relationship component
3271
+ * (typically a `ChildOf` or similar exclusive `sparse` relation).
3272
+ *
3273
+ * This is the recommended high-level API for hierarchy traversal.
3274
+ * It uses the internal reverse reference index for efficiency.
3275
+ *
3276
+ * @example
3277
+ * const ChildOf = component({ exclusive: true, sparse: true });
3278
+ * const kids = world.getChildren(ship, ChildOf);
3279
+ */
3280
+ getChildren(parent, childOf) {
3281
+ return this.getRelationSources(parent, childOf);
3282
+ }
3283
+ /**
3284
+ * Returns the parent of `child` for the given relationship component
3285
+ * (typically an exclusive `ChildOf` relation).
3286
+ *
3287
+ * @example
3288
+ * const ChildOf = component({ exclusive: true, sparse: true });
3289
+ * const parent = world.getParent(turret, ChildOf);
3290
+ */
3291
+ getParent(child, childOf) {
3292
+ return this.getSingleRelationTarget(child, childOf);
3293
+ }
3294
+ /**
3295
+ * Returns the ancestor chain from the immediate parent up to (but not
3296
+ * including) the root for the given relationship component.
3297
+ *
3298
+ * @example
3299
+ * const ChildOf = component({ exclusive: true, sparse: true });
3300
+ * const ancestors = world.getAncestors(muzzle, ChildOf); // [turret, ship]
3301
+ */
3302
+ getAncestors(entity, childOf) {
3303
+ const ancestors = [];
3304
+ let cur = this.getParent(entity, childOf);
3305
+ while (cur !== void 0) {
3306
+ ancestors.push(cur);
3307
+ cur = this.getParent(cur, childOf);
3308
+ }
3309
+ return ancestors;
3310
+ }
3311
+ /**
3312
+ * Iteratively traverses all descendants of `root` in DFS pre-order.
3313
+ * This is a generator and is safe for very deep hierarchies.
3314
+ *
3315
+ * @example
3316
+ * for (const { entity, depth, parent } of world.iterateDescendants(root, ChildOf)) {
3317
+ * console.log(depth, entity);
3318
+ * }
3319
+ */
3320
+ *iterateDescendants(root, childOf, opts = {}) {
3321
+ const { includeSelf = false, maxDepth } = opts;
3322
+ const stack = [];
3323
+ if (includeSelf) stack.push({
3324
+ entity: root,
3325
+ depth: 0,
3326
+ parent: null
3327
+ });
3328
+ else for (const child of this.getChildren(root, childOf)) stack.push({
3329
+ entity: child,
3330
+ depth: 1,
3331
+ parent: root
3332
+ });
3333
+ while (stack.length > 0) {
3334
+ const current = stack.pop();
3335
+ if (maxDepth !== void 0 && current.depth > maxDepth) continue;
3336
+ yield current;
3337
+ const kids = this.getChildren(current.entity, childOf);
3338
+ for (let i = kids.length - 1; i >= 0; i--) {
3339
+ const k = kids[i];
3340
+ stack.push({
3341
+ entity: k,
3342
+ depth: current.depth + 1,
3343
+ parent: current.entity
3344
+ });
3345
+ }
3346
+ }
3347
+ }
3348
+ /**
3349
+ * Callback-based descendant traversal (hot path friendly).
3350
+ * Return `false` from the visitor to stop early.
3351
+ */
3352
+ traverseDescendants(root, childOf, visitor, opts = {}) {
3353
+ for (const { entity, depth, parent } of this.iterateDescendants(root, childOf, opts)) if (visitor(entity, depth, parent) === false) return;
3354
+ }
2719
3355
  hook(componentTypes, hook, filter) {
2720
3356
  const isCallback = typeof hook === "function";
2721
3357
  const callback = isCallback ? hook : void 0;
@@ -2751,6 +3387,21 @@ var World = class {
2751
3387
  };
2752
3388
  }
2753
3389
  /**
3390
+ * Creates a debug stats collector that will receive a `SyncDebugStats` payload
3391
+ * after every subsequent `sync()`.
3392
+ *
3393
+ * The returned object is a pure lifecycle handle. It does not store data.
3394
+ * Collection stops when you call `[Symbol.dispose]()` (or use a `using` declaration).
3395
+ *
3396
+ * All active collectors receive the exact same stats object for a given sync.
3397
+ * Exceptions thrown by callbacks are ignored.
3398
+ *
3399
+ * This is intended for development/debugging and leak detection.
3400
+ */
3401
+ createDebugStatsCollector(callback) {
3402
+ return this.debugStats.createCollector(callback);
3403
+ }
3404
+ /**
2754
3405
  * Synchronizes all buffered commands (set/remove/delete) to the world.
2755
3406
  * This method must be called after making changes via `set()`, `remove()`, or `delete()` for them to take effect.
2756
3407
  * Typically called once per frame at the end of your game loop.
@@ -2761,7 +3412,40 @@ var World = class {
2761
3412
  * world.sync(); // Apply all buffered changes
2762
3413
  */
2763
3414
  sync() {
2764
- this.commandBuffer.execute();
3415
+ if (!this.debugStats.hasActiveCollectors()) {
3416
+ this.commandBuffer.execute();
3417
+ return;
3418
+ }
3419
+ const syncStart = performance.now();
3420
+ this.debugStats.resetActivity();
3421
+ const commandBufferStart = performance.now();
3422
+ const commandIterations = this.commandBuffer.execute();
3423
+ const commandBufferEnd = performance.now();
3424
+ const syncEnd = performance.now();
3425
+ const entityCount = this.entityToArchetype.size;
3426
+ let emptyArchetypes = 0;
3427
+ for (const arch of this.archetypes) if (arch.size === 0) emptyArchetypes++;
3428
+ let archetypesByComponentSize = 0;
3429
+ for (const set of this.archetypesByComponent.values()) archetypesByComponentSize += set.size;
3430
+ this.debugStats.deliver({
3431
+ syncStart,
3432
+ syncEnd,
3433
+ commandBufferStart,
3434
+ commandBufferEnd,
3435
+ commandIterations
3436
+ }, {
3437
+ entityCount,
3438
+ freelistSize: this.entityIdManager.getFreelistSize(),
3439
+ nextId: this.entityIdManager.getNextId(),
3440
+ archetypeCount: this.archetypes.length,
3441
+ emptyArchetypes,
3442
+ archetypesByComponentSize,
3443
+ cachedQueryCount: this.queryRegistry.cache?.size ?? 0,
3444
+ registeredQueryCount: this.queryRegistry.queries?.size ?? 0,
3445
+ hookCount: this.hooks.size,
3446
+ entityReferencesSize: this.entityReferences.size,
3447
+ entityToReferencingArchetypesSize: this.entityToReferencingArchetypes.size
3448
+ });
2765
3449
  }
2766
3450
  /**
2767
3451
  * Creates a cached query for efficiently iterating entities with specific components.
@@ -2797,7 +3481,7 @@ var World = class {
2797
3481
  createQuery(componentTypes, filter = {}) {
2798
3482
  const sortedTypes = normalizeComponentTypes(componentTypes);
2799
3483
  const filterKey = serializeQueryFilter(filter);
2800
- const key = `${this.createArchetypeSignature(sortedTypes)}${filterKey ? `|${filterKey}` : ""}`;
3484
+ const key = `${sortedTypes.join(",")}${filterKey ? `|${filterKey}` : ""}`;
2801
3485
  return this.queryRegistry.getOrCreate(this, sortedTypes, key, filter);
2802
3486
  }
2803
3487
  /**
@@ -2863,44 +3547,7 @@ var World = class {
2863
3547
  * @internal
2864
3548
  */
2865
3549
  getMatchingArchetypes(componentTypes) {
2866
- if (componentTypes.length === 0) return [...this.archetypes];
2867
- const regularComponents = [];
2868
- const wildcardRelations = [];
2869
- for (const componentType of componentTypes) if (isWildcardRelationId(componentType)) {
2870
- const componentId = getComponentIdFromRelationId(componentType);
2871
- if (componentId !== void 0) wildcardRelations.push({
2872
- componentId,
2873
- relationId: componentType
2874
- });
2875
- } else regularComponents.push(componentType);
2876
- let matchingArchetypes = this.getArchetypesWithComponents(regularComponents);
2877
- for (const { componentId, relationId } of wildcardRelations) {
2878
- const markerSet = this.archetypesByComponent.get(relationId);
2879
- const archetypesWithMarker = markerSet ? Array.from(markerSet) : [];
2880
- matchingArchetypes = matchingArchetypes.length === 0 ? archetypesWithMarker : matchingArchetypes.filter((a) => markerSet?.has(a) || a.hasRelationWithComponentId(componentId));
2881
- }
2882
- return matchingArchetypes;
2883
- }
2884
- getArchetypesWithComponents(componentTypes) {
2885
- if (componentTypes.length === 0) return [...this.archetypes];
2886
- if (componentTypes.length === 1) {
2887
- const set = this.archetypesByComponent.get(componentTypes[0]);
2888
- return set ? Array.from(set) : [];
2889
- }
2890
- const sets = componentTypes.map((type) => this.archetypesByComponent.get(type)).filter((s) => s !== void 0 && s.size > 0).sort((a, b) => a.size - b.size);
2891
- if (sets.length === 0) return [];
2892
- if (sets.length < componentTypes.length) return [];
2893
- const smallest = sets[0];
2894
- if (sets.length === 2) {
2895
- const other = sets[1];
2896
- return Array.from(smallest).filter((a) => other.has(a));
2897
- }
2898
- let result = new Set(smallest);
2899
- for (let i = 1; i < sets.length; i++) {
2900
- for (const item of result) if (!sets[i].has(item)) result.delete(item);
2901
- if (result.size === 0) return [];
2902
- }
2903
- return Array.from(result);
3550
+ return this.archetypeManager.getMatchingArchetypes(componentTypes);
2904
3551
  }
2905
3552
  query(componentTypes, includeComponents) {
2906
3553
  const matchingArchetypes = this.getMatchingArchetypes(componentTypes);
@@ -2914,150 +3561,14 @@ var World = class {
2914
3561
  return result;
2915
3562
  }
2916
3563
  }
2917
- executeEntityCommands(entityId, commands) {
2918
- this._changeset.clear();
2919
- if (this.componentEntities.exists(entityId)) {
2920
- this.componentEntities.executeCommands(entityId, commands);
2921
- return;
2922
- }
2923
- if (commands.some((cmd) => cmd.type === "destroy")) {
2924
- this.destroyEntityImmediate(entityId);
2925
- return;
2926
- }
2927
- this.applyEntityCommands(entityId, commands);
2928
- }
2929
- applyEntityCommands(entityId, commands) {
2930
- const currentArchetype = this.entityToArchetype.get(entityId);
2931
- if (!currentArchetype) return;
2932
- const changeset = this._changeset;
2933
- processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
2934
- if (isExclusiveComponent(compId)) removeMatchingRelations(eid, arch, compId, changeset);
2935
- });
2936
- const hasStructuralChange = changeset.removes.size > 0 || changeset.adds.size > 0;
2937
- if (this.hooks.size === 0) {
2938
- applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype, null);
2939
- if (hasStructuralChange) this.updateEntityReferences(entityId, changeset);
2940
- return;
2941
- }
2942
- const removedComponents = /* @__PURE__ */ new Map();
2943
- const newArchetype = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype, removedComponents);
2944
- if (hasStructuralChange) this.updateEntityReferences(entityId, changeset);
2945
- triggerLifecycleHooks(this.createHooksContext(), entityId, changeset.adds, removedComponents, currentArchetype, newArchetype);
2946
- }
2947
- createHooksContext() {
2948
- return this._hooksCtx;
2949
- }
2950
- removeComponentImmediate(entityId, componentType, targetEntityId) {
2951
- const sourceArchetype = this.entityToArchetype.get(entityId);
2952
- if (!sourceArchetype) return;
2953
- const changeset = this._removeChangeset;
2954
- changeset.clear();
2955
- changeset.delete(componentType);
2956
- maybeRemoveWildcardMarker(entityId, sourceArchetype, componentType, getComponentIdFromRelationId(componentType), changeset);
2957
- const removedComponent = sourceArchetype.get(entityId, componentType);
2958
- const newArchetype = applyChangeset(this._commandCtx, entityId, sourceArchetype, changeset, this.entityToArchetype, null);
2959
- untrackEntityReference(this.entityReferences, entityId, componentType, targetEntityId);
2960
- triggerLifecycleHooks(this.createHooksContext(), entityId, /* @__PURE__ */ new Map(), new Map([[componentType, removedComponent]]), sourceArchetype, newArchetype);
2961
- }
2962
- updateEntityReferences(entityId, changeset) {
2963
- for (const componentType of changeset.removes) if (isEntityRelation(componentType)) {
2964
- const targetId = getTargetIdFromRelationId(componentType);
2965
- untrackEntityReference(this.entityReferences, entityId, componentType, targetId);
2966
- } else if (componentType >= 1024) untrackEntityReference(this.entityReferences, entityId, componentType, componentType);
2967
- for (const [componentType] of changeset.adds) if (isEntityRelation(componentType)) {
2968
- const targetId = getTargetIdFromRelationId(componentType);
2969
- trackEntityReference(this.entityReferences, entityId, componentType, targetId);
2970
- } else if (componentType >= 1024) trackEntityReference(this.entityReferences, entityId, componentType, componentType);
2971
- }
2972
3564
  ensureArchetype(componentTypes) {
2973
- const sortedTypes = normalizeComponentTypes(filterRegularComponentTypes(componentTypes));
2974
- const hashKey = this.createArchetypeSignature(sortedTypes);
2975
- return getOrCompute(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
2976
- }
2977
- /** Add componentType to the reverse index if it contains an entity ID */
2978
- addToReferencingIndex(componentType, archetype) {
2979
- const detailedType = getDetailedIdType(componentType);
2980
- let entityId;
2981
- if (detailedType.type === "entity") entityId = componentType;
2982
- else if (detailedType.type === "entity-relation") entityId = detailedType.targetId;
2983
- if (entityId !== void 0) {
2984
- let refs = this.entityToReferencingArchetypes.get(entityId);
2985
- if (!refs) {
2986
- refs = /* @__PURE__ */ new Set();
2987
- this.entityToReferencingArchetypes.set(entityId, refs);
2988
- }
2989
- refs.add(archetype);
2990
- }
2991
- }
2992
- /** Remove componentType from the reverse index */
2993
- removeFromReferencingIndex(componentType, archetype) {
2994
- const detailedType = getDetailedIdType(componentType);
2995
- let entityId;
2996
- if (detailedType.type === "entity") entityId = componentType;
2997
- else if (detailedType.type === "entity-relation") entityId = detailedType.targetId;
2998
- if (entityId !== void 0) {
2999
- const refs = this.entityToReferencingArchetypes.get(entityId);
3000
- if (refs) {
3001
- refs.delete(archetype);
3002
- if (refs.size === 0) this.entityToReferencingArchetypes.delete(entityId);
3003
- }
3004
- }
3005
- }
3006
- createNewArchetype(componentTypes) {
3007
- const newArchetype = new Archetype(componentTypes, this.dontFragmentStore);
3008
- this.archetypes.push(newArchetype);
3009
- for (const componentType of componentTypes) {
3010
- let archetypes = this.archetypesByComponent.get(componentType);
3011
- if (!archetypes) {
3012
- archetypes = /* @__PURE__ */ new Set();
3013
- this.archetypesByComponent.set(componentType, archetypes);
3014
- }
3015
- archetypes.add(newArchetype);
3016
- this.addToReferencingIndex(componentType, newArchetype);
3017
- }
3018
- this.queryRegistry.onNewArchetype(newArchetype);
3019
- this.updateArchetypeHookMatches(newArchetype);
3020
- return newArchetype;
3021
- }
3022
- updateArchetypeHookMatches(archetype) {
3023
- for (const entry of this.hooks) if (this.archetypeMatchesHook(archetype, entry)) {
3024
- archetype.matchingMultiHooks.add(entry);
3025
- if (entry.matchedArchetypes) entry.matchedArchetypes.add(archetype);
3026
- }
3027
- }
3028
- archetypeMatchesHook(archetype, entry) {
3029
- return entry.requiredComponents.every((c) => {
3030
- if (isWildcardRelationId(c)) {
3031
- if (isDontFragmentWildcard(c)) return true;
3032
- const componentId = getComponentIdFromRelationId(c);
3033
- return componentId !== void 0 && archetype.hasRelationWithComponentId(componentId);
3034
- }
3035
- return archetype.componentTypeSet.has(c) || isDontFragmentRelation(c);
3036
- }) && matchesFilter(archetype, entry.filter);
3565
+ return this.archetypeManager.ensureArchetype(componentTypes);
3037
3566
  }
3038
3567
  cleanupArchetypesReferencingEntity(entityId) {
3039
- const refs = this.entityToReferencingArchetypes.get(entityId);
3040
- if (!refs) return;
3041
- for (const archetype of refs) if (archetype.getEntities().length === 0) this.removeArchetype(archetype);
3042
- this.entityToReferencingArchetypes.delete(entityId);
3568
+ this.archetypeManager.cleanupArchetypesReferencingEntity(entityId);
3043
3569
  }
3044
- removeArchetype(archetype) {
3045
- const index = this.archetypes.indexOf(archetype);
3046
- if (index !== -1) {
3047
- const last = this.archetypes[this.archetypes.length - 1];
3048
- this.archetypes[index] = last;
3049
- this.archetypes.pop();
3050
- }
3051
- this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
3052
- for (const componentType of archetype.componentTypes) {
3053
- const archetypes = this.archetypesByComponent.get(componentType);
3054
- if (archetypes) {
3055
- archetypes.delete(archetype);
3056
- if (archetypes.size === 0) this.archetypesByComponent.delete(componentType);
3057
- }
3058
- this.removeFromReferencingIndex(componentType, archetype);
3059
- }
3060
- this.queryRegistry.onArchetypeRemoved(archetype);
3570
+ archetypeMatchesHook(archetype, entry) {
3571
+ return this.archetypeManager.archetypeMatchesHook(archetype, entry);
3061
3572
  }
3062
3573
  /**
3063
3574
  * Serializes the entire world state to a plain JavaScript object.
@@ -3080,11 +3591,22 @@ var World = class {
3080
3591
  * const savedData = JSON.parse(localStorage.getItem('save'));
3081
3592
  * const newWorld = new World(savedData);
3082
3593
  */
3594
+ removeComponentImmediate(entityId, componentType, targetEntityId) {
3595
+ this.commandExecutor.removeComponentImmediate(entityId, componentType, targetEntityId);
3596
+ }
3597
+ createHooksContext() {
3598
+ return this.commandExecutor.getHooksContext?.() ?? {
3599
+ multiHooks: this.hooks,
3600
+ has: (eid, ct) => this.has(eid, ct),
3601
+ get: (eid, ct) => this.get(eid, ct),
3602
+ getOptional: (eid, ct) => this.getOptional(eid, ct)
3603
+ };
3604
+ }
3083
3605
  serialize() {
3084
- return serializeWorld(this.archetypes, this.componentEntities, this.entityIdManager);
3606
+ return serializeWorld(this.archetypeManager.archetypes, this.componentEntities, this.entityIdManager);
3085
3607
  }
3086
3608
  };
3087
3609
  //#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 };
3610
+ export { component as a, isSparseComponent as c, decodeRelationId as d, isWildcardRelationId as f, isRelationId as g, isEntityId as h, EntityBuilder as i, isSparseRelation as l, isComponentId as m, Query as n, getComponentIdByName as o, relation as p, SingletonHandle as r, getComponentNameById as s, World as t, isSparseWildcard as u };
3089
3611
 
3090
3612
  //# sourceMappingURL=world.mjs.map