@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
@@ -248,7 +248,7 @@ interface ComponentOptions<T = any> {
248
248
  * `cascadeDelete`, deleting the target entity will both (a) delete the
249
249
  * referencing entity, and (b) the exclusivity constraint prevents the
250
250
  * entity from having multiple cascade-delete relations of the same type.
251
- * - **`dontFragment`**: Compatible. Exclusivity is enforced at the data level
251
+ * - **`sparse`**: Compatible. Exclusivity is enforced at the data level
252
252
  * regardless of whether the archetype is fragmented.
253
253
  *
254
254
  * @example
@@ -306,7 +306,8 @@ interface ComponentOptions<T = any> {
306
306
  */
307
307
  cascadeDelete?: boolean;
308
308
  /**
309
- * If true, relations with this component will not cause archetype fragmentation.
309
+ * If true, relations with this component use sparse storage and will not cause
310
+ * archetype fragmentation.
310
311
  *
311
312
  * **Problem it solves**: By default, each unique relation pair `(component, target)`
312
313
  * creates a **separate archetype**. If 100 entities each have a `ChildOf` relation
@@ -314,28 +315,28 @@ interface ComponentOptions<T = any> {
314
315
  * Queries that iterate over all entities with a `ChildOf` relation must check all
315
316
  * 100 archetypes, which degrades iteration performance and increases memory overhead.
316
317
  *
317
- * **How it works**: When `dontFragment` is enabled, the relation's target does **not**
318
- * contribute to the archetype signature. Entities with different targets for the same
319
- * relation component share a **single archetype**, and the per-entity target data is
320
- * stored in a separate `DontFragmentStore` (a `Map<EntityId, Map<EntityId, any>>`).
321
- * A wildcard relation marker (`relation(Comp, "*")`) is placed in the archetype
322
- * component list so queries can still discover matching archetypes.
318
+ * **How it works (sparse storage)**: When `sparse` is enabled, the relation's target
319
+ * does **not** contribute to the archetype signature. Entities with different targets
320
+ * for the same relation component share a **single archetype**, and the per-entity
321
+ * target data is stored in a separate side store (historically called
322
+ * `DontFragmentStore`, now `SparseStore`). A wildcard relation marker (`relation(Comp, "*")`) is placed
323
+ * in the archetype component list so queries can still discover matching archetypes.
323
324
  *
324
325
  * **Use cases**:
325
326
  * - **Hierarchy/ownership**: `ChildOf` relations where thousands of entities each
326
327
  * point to different parent entities.
327
328
  * - **Dynamic targeting**: Relations where targets change frequently (e.g., AI
328
- * targeting, inventory slots) — without `dontFragment`, each target change would
329
- * cause an archetype migration, which is expensive.
329
+ * targeting, inventory slots) — without `sparse`, each target change would cause
330
+ * an archetype migration, which is expensive.
330
331
  * - **High-cardinality relations**: Any relation where the number of unique targets
331
332
  * is large compared to the number of entities.
332
333
  *
333
334
  * **Performance implications**:
334
- * - **Without `dontFragment`**: Archetype count grows linearly with unique targets.
335
+ * - **Without `sparse`**: Archetype count grows linearly with unique targets.
335
336
  * Each archetype migration (changing a relation target) requires moving the entity's
336
337
  * data between component arrays.
337
- * - **With `dontFragment`**: Archetype count stays constant regardless of target
338
- * diversity. Changing a relation target is an O(1) update in the `DontFragmentStore`.
338
+ * - **With `sparse`**: Archetype count stays constant regardless of target diversity.
339
+ * Changing a relation target is an O(1) update in the sparse side store.
339
340
  * The trade-off is an extra map lookup when accessing the relation data.
340
341
  *
341
342
  * **Constraints**:
@@ -344,13 +345,16 @@ interface ComponentOptions<T = any> {
344
345
  * archetype carries a wildcard marker so queries can discover it.
345
346
  * - Works with `exclusive` and `cascadeDelete` simultaneously.
346
347
  *
348
+ * **Backward compatibility**: The legacy key `dontFragment` is still accepted and
349
+ * behaves identically. Prefer `sparse` in new code.
350
+ *
347
351
  * @example
348
352
  * ```ts
349
- * // Without dontFragment: 100 entities with different parents = 100 archetypes
353
+ * // Without sparse: 100 entities with different parents = 100 archetypes
350
354
  * const ChildOf = component(); // default: fragmentation happens
351
355
  *
352
- * // With dontFragment: 100 entities with different parents = 1 archetype
353
- * const ChildOf = component({ dontFragment: true });
356
+ * // With sparse: 100 entities with different parents = 1 archetype
357
+ * const ChildOf = component({ sparse: true });
354
358
  *
355
359
  * for (let i = 0; i < 100; i++) {
356
360
  * const parent = world.new();
@@ -359,11 +363,17 @@ interface ComponentOptions<T = any> {
359
363
  * world.set(child, relation(ChildOf, parent));
360
364
  * }
361
365
  * world.sync();
362
- * // dontFragment: 1 archetype for all 100 entities
366
+ * // sparse: 1 archetype for all 100 entities
363
367
  * // without: 100 archetypes, one per unique parent
364
368
  * ```
365
369
  *
366
- * Inspired by Flecs' `DontFragment` trait.
370
+ * Inspired by Flecs' `DontFragment` trait (now exposed as the clearer `sparse` option).
371
+ */
372
+ sparse?: boolean;
373
+ /**
374
+ * @deprecated Use `sparse: true` instead. This key is kept solely for backward
375
+ * compatibility; `component({ dontFragment: true })` continues to work exactly
376
+ * as before and is equivalent to `sparse: true`.
367
377
  */
368
378
  dontFragment?: boolean;
369
379
  /**
@@ -453,6 +463,53 @@ declare function getComponentIdByName(name: string): ComponentId<any> | undefine
453
463
  * @returns The component name if found, undefined otherwise
454
464
  */
455
465
  declare function getComponentNameById(id: ComponentId<any>): string | undefined;
466
+ /**
467
+ * Check if a component is marked as `sparse` (sparse storage for relations).
468
+ *
469
+ * When a component has `sparse: true`, relations using it do not cause archetype
470
+ * fragmentation — entities with different relation targets can share the same
471
+ * archetype. This is a fast O(1) bitset lookup. The legacy `dontFragment` key
472
+ * is still accepted and sets the same internal flag.
473
+ *
474
+ * @param id - The component ID to check.
475
+ * @returns `true` if the component was created with `sparse: true` (or the
476
+ * legacy `dontFragment: true`).
477
+ *
478
+ * @see {@link ComponentOptions.sparse} for the full explanation of sparse storage.
479
+ */
480
+ declare function isSparseComponent(id: ComponentId<any>): boolean;
481
+ /**
482
+ * Check if an ID is a specific (non-wildcard) relation backed by a `sparse`
483
+ * component (i.e. stored in the side sparse store rather than the archetype).
484
+ *
485
+ * This is used in hot paths (archetype resolution, command processing) to determine
486
+ * whether a relation should be excluded from the archetype signature.
487
+ *
488
+ * @param id - The entity/relation ID to check (must be a relation ID, not a plain
489
+ * component ID).
490
+ * @returns `true` if this is a specific-target relation (not wildcard) whose base
491
+ * component was created with `sparse: true` (or legacy `dontFragment: true`).
492
+ *
493
+ * @see {@link isSparseWildcard} for the wildcard variant.
494
+ * @see {@link ComponentOptions.sparse} for the full explanation.
495
+ */
496
+ declare function isSparseRelation(id: EntityId<any>): boolean;
497
+ /**
498
+ * Check if an ID is a wildcard relation (`relation(Comp, "*")`) backed by a
499
+ * `sparse` component.
500
+ *
501
+ * Wildcard markers for sparse components are placed in the archetype component
502
+ * list so that queries can discover archetypes containing entities with that
503
+ * relation type.
504
+ *
505
+ * @param id - The entity/relation ID to check.
506
+ * @returns `true` if this is a wildcard relation (`"*"` target) whose base
507
+ * component was created with `sparse: true` (or legacy `dontFragment: true`).
508
+ *
509
+ * @see {@link isSparseRelation} for the specific-target variant.
510
+ * @see {@link ComponentOptions.sparse} for the full explanation.
511
+ */
512
+ declare function isSparseWildcard(id: EntityId<any>): boolean;
456
513
  //#endregion
457
514
  //#region src/storage/serialization.d.ts
458
515
  type SerializedEntityId = number | string | {
@@ -558,24 +615,78 @@ interface LifecycleHookEntry {
558
615
  /** Archetypes that match this hook, used for precise cleanup on unsubscription */
559
616
  matchedArchetypes?: Set<any>;
560
617
  }
618
+ /**
619
+ * Statistics payload delivered to callbacks registered via `World.createDebugStatsCollector`.
620
+ *
621
+ * All structural counts are snapshots taken after the sync that triggered delivery.
622
+ * `activity` always reflects work performed during that specific sync.
623
+ *
624
+ * Timestamps are raw `performance.now()` values suitable for `performance.measure`.
625
+ */
626
+ interface SyncDebugStats {
627
+ readonly timestamps: {
628
+ readonly syncStart: number;
629
+ readonly syncEnd: number;
630
+ readonly commandBufferStart: number;
631
+ readonly commandBufferEnd: number;
632
+ };
633
+ /** Number of iterations the internal command buffer loop performed during this sync. */
634
+ readonly commandIterations: number;
635
+ readonly entities: {
636
+ readonly total: number;
637
+ readonly freelistSize: number;
638
+ readonly nextId: number;
639
+ };
640
+ readonly archetypes: {
641
+ readonly total: number;
642
+ readonly empty: number;
643
+ };
644
+ readonly queries: {
645
+ readonly cached: number;
646
+ readonly registered: number;
647
+ };
648
+ readonly hooks: {
649
+ readonly total: number;
650
+ };
651
+ /** Sizes of stable internal reverse indices (conservative set). */
652
+ readonly indices: {
653
+ readonly entityReferences: number;
654
+ readonly entityToReferencingArchetypes: number;
655
+ readonly archetypesByComponent: number;
656
+ };
657
+ /**
658
+ * Activity that occurred as a direct result of this sync.
659
+ * All fields are always present (never optional).
660
+ */
661
+ readonly activity: {
662
+ /** Number of entities that performed an archetype migration (hasArchetypeStructuralChange was true). */readonly migrations: number; /** Total number of individual hook callback invocations (invokeHook calls). */
663
+ readonly hooksExecuted: number; /** Number of new archetypes created during this sync. */
664
+ readonly archetypesCreated: number; /** Number of archetypes removed during this sync. */
665
+ readonly archetypesRemoved: number;
666
+ };
667
+ }
668
+ /**
669
+ * Handle returned by `World.createDebugStatsCollector`.
670
+ * The object itself carries no data — its only responsibility is lifetime management.
671
+ * Use with `using` or call `[Symbol.dispose]()` when you no longer need collection.
672
+ */
673
+ interface DebugStatsCollector {
674
+ [Symbol.dispose](): void;
675
+ }
561
676
  //#endregion
562
677
  //#region src/archetype/store.d.ts
563
678
  /**
564
- * Interface for storing dontFragment relation data.
565
- *
566
- * Storage is now primarily keyed by relation ComponentId (the "kind" of relation)
567
- * rather than by entity. This provides O(1) or near-O(1) answers for the hot
568
- * wildcard-related paths (hasRelationWithComponentId, wildcard materialization
569
- * during iteration, hook matching, etc.).
679
+ * Interface for the sparse side store used by components declared with `sparse: true`
680
+ * (or the legacy `dontFragment: true` alias).
570
681
  *
571
- * A lightweight reverse index (entity -> Set of base ComponentIds) is maintained
572
- * to efficiently support the infrequent "get all dontFragment data for this entity"
573
- * operations (removeEntity, dump, getEntity, serialization).
682
+ * Relation data for these components lives here instead of in archetype columns,
683
+ * preventing fragmentation for high-cardinality or frequently-changing relations.
574
684
  *
575
- * The interface no longer leaks internal Map structures. Callers work with
576
- * semantic operations only.
685
+ * Storage is primarily keyed by base relation ComponentId. This enables efficient
686
+ * per-component lookups required by wildcard queries (relation(Comp, "*")) and
687
+ * archetype filtering, while still supporting full-entity enumeration when needed.
577
688
  */
578
- interface DontFragmentStore {
689
+ interface SparseStore {
579
690
  getValue(entityId: EntityId, relationType: EntityId<any>): any | undefined;
580
691
  setValue(entityId: EntityId, relationType: EntityId<any>, data: any): void;
581
692
  deleteValue(entityId: EntityId, relationType: EntityId<any>): boolean;
@@ -583,6 +694,12 @@ interface DontFragmentStore {
583
694
  getRelationsForComponent(entityId: EntityId, componentId: EntityId<any>): [target: EntityId, data: any][];
584
695
  getAllForEntity(entityId: EntityId): Array<[relationType: EntityId<any>, data: any]>;
585
696
  deleteEntity(entityId: EntityId): void;
697
+ /**
698
+ * @internal Bulk helper for serialization of many entities.
699
+ * Default implementation simply loops getAllForEntity; subclasses / future
700
+ * implementations can provide a more efficient fused walk.
701
+ */
702
+ getAllForEntities(entityIds: readonly EntityId[]): Map<EntityId, Array<[EntityId<any>, any]>>;
586
703
  }
587
704
  //#endregion
588
705
  //#region src/archetype/archetype.d.ts
@@ -614,11 +731,10 @@ declare class Archetype {
614
731
  */
615
732
  private entityToIndex;
616
733
  /**
617
- * DontFragmentStore (keyed primarily by relation ComponentId).
618
- * Uses optimized RelationEntry (single/multi) for the common exclusive case.
734
+ * SparseStore used for relations declared with `sparse: true`.
619
735
  * See store.ts for implementation details.
620
736
  */
621
- private dontFragmentRelations;
737
+ private sparseRelations;
622
738
  /**
623
739
  * Multi-hooks that match this archetype
624
740
  */
@@ -627,7 +743,7 @@ declare class Archetype {
627
743
  * Cache for pre-computed component data sources to avoid repeated calculations
628
744
  */
629
745
  private componentDataSourcesCache;
630
- constructor(componentTypes: EntityId<any>[], dontFragmentRelations: DontFragmentStore);
746
+ constructor(componentTypes: EntityId<any>[], sparseStore: SparseStore);
631
747
  get size(): number;
632
748
  /**
633
749
  * Check if the given component types match this archetype
@@ -637,19 +753,34 @@ declare class Archetype {
637
753
  */
638
754
  matches(componentTypes: EntityId<any>[]): boolean;
639
755
  addEntity(entityId: EntityId, componentData: Map<EntityId<any>, any>): void;
640
- private addDontFragmentRelations;
756
+ private addSparseRelations;
641
757
  getEntity(entityId: EntityId): Map<EntityId<any>, any> | undefined;
642
758
  /**
643
- * Returns all dontFragment relations for the given entity as an array of tuples.
644
- * This is a compatibility adapter during the store refactor.
645
- *
646
- * Prefer the new DontFragmentStore methods when possible.
759
+ * Returns all sparse-stored relations for the given entity.
760
+ * Internal helper used by command processing and tests.
647
761
  */
648
- getEntityDontFragmentRelations(entityId: EntityId): Map<EntityId<any>, any> | undefined;
762
+ getEntitySparseRelations(entityId: EntityId): Map<EntityId<any>, any> | undefined;
649
763
  dump(): Array<{
650
764
  entity: EntityId;
651
765
  components: Map<EntityId<any>, any>;
652
766
  }>;
767
+ /**
768
+ * @internal Serialization fast-path.
769
+ *
770
+ * Appends SerializedEntity records directly from the archetype's column storage
771
+ * (componentData arrays) plus sparse relations, avoiding per-entity Map
772
+ * allocation and repeated Array.from(entries()).
773
+ *
774
+ * Component type IDs should be pre-encoded by the caller (once per archetype)
775
+ * and passed in `encodedComponentTypes` (same order and length as this.componentTypes).
776
+ *
777
+ * The provided `encode` function should be the cached variant for best performance
778
+ * on entity IDs and any sparse relation type IDs.
779
+ *
780
+ * `sparseByEntity` is an optional pre-fetched map from a bulk
781
+ * `SparseStore.getAllForEntities` call (further reduces per-entity calls).
782
+ */
783
+ appendSerializedEntities(out: SerializedEntity[], encode: (id: EntityId<any>) => SerializedEntityId, encodedComponentTypes: SerializedEntityId[], sparseByEntity?: Map<EntityId, Array<[EntityId<any>, any]>>): void;
653
784
  removeEntity(entityId: EntityId): Map<EntityId<any>, any> | undefined;
654
785
  exists(entityId: EntityId): boolean;
655
786
  get<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, any][];
@@ -764,8 +895,8 @@ declare class Query {
764
895
  _cacheKey: string | undefined;
765
896
  /** Cached wildcard component types for faster entity filtering */
766
897
  private wildcardTypes;
767
- /** Cached specific dontFragment relation types that need entity-level filtering */
768
- private specificDontFragmentTypes;
898
+ /** Cached specific sparse relation types that need entity-level filtering */
899
+ private specificSparseRelationTypes;
769
900
  /**
770
901
  * @internal Queries should be created via {@link World.createQuery}, not instantiated directly.
771
902
  */
@@ -787,7 +918,7 @@ declare class Query {
787
918
  */
788
919
  getEntities(): EntityId[];
789
920
  /**
790
- * Check if entity matches all query requirements (wildcards and specific dontFragment relations)
921
+ * Check if entity matches all query requirements (wildcards and specific sparse relations)
791
922
  */
792
923
  private entityMatchesQuery;
793
924
  /**
@@ -878,6 +1009,41 @@ declare class Query {
878
1009
  get disposed(): boolean;
879
1010
  }
880
1011
  //#endregion
1012
+ //#region src/world/singleton.d.ts
1013
+ interface SingletonHandleOps<T> {
1014
+ has(): boolean;
1015
+ get(): T;
1016
+ getOptional(): {
1017
+ value: T;
1018
+ } | undefined;
1019
+ remove(): void;
1020
+ set(value: T | undefined): void;
1021
+ }
1022
+ /**
1023
+ * Explicit handle for a singleton component (component-as-entity).
1024
+ *
1025
+ * This provides an explicit and concise API for singleton components without
1026
+ * overloading `world.set()` semantics.
1027
+ *
1028
+ * @example
1029
+ * const config = world.singleton(Config);
1030
+ * config.set({ debug: true });
1031
+ * world.sync();
1032
+ * console.log(config.get());
1033
+ */
1034
+ declare class SingletonHandle<T = void> {
1035
+ readonly componentId: ComponentId<T>;
1036
+ private readonly ops;
1037
+ constructor(componentId: ComponentId<T>, ops: SingletonHandleOps<T>);
1038
+ has(): boolean;
1039
+ get(): T;
1040
+ getOptional(): {
1041
+ value: T;
1042
+ } | undefined;
1043
+ remove(): void;
1044
+ set(...args: T extends void ? [] : [value: NoInfer<T>]): void;
1045
+ }
1046
+ //#endregion
881
1047
  //#region src/world/world.d.ts
882
1048
  /**
883
1049
  * World class for ECS architecture
@@ -885,28 +1051,22 @@ declare class Query {
885
1051
  */
886
1052
  declare class World {
887
1053
  private entityIdManager;
888
- private archetypes;
889
- private archetypeBySignature;
890
- private entityToArchetype;
891
- private archetypesByComponent;
892
1054
  private entityReferences;
893
- /** Reverse index: entity ID set of archetypes whose componentTypes include that entity ID */
894
- private entityToReferencingArchetypes;
895
- /** DontFragment relation storage, shared with all Archetype instances */
896
- private readonly dontFragmentStore;
1055
+ /** Sparse relation storage (for components created with `sparse: true`), shared with all Archetype instances */
1056
+ private readonly sparseStore;
897
1057
  /** Component entity (singleton) storage */
898
1058
  private readonly componentEntities;
1059
+ private archetypeManager;
1060
+ private get archetypes();
1061
+ private get entityToArchetype();
1062
+ private get archetypesByComponent();
1063
+ private get entityToReferencingArchetypes();
899
1064
  private readonly queryRegistry;
900
1065
  private hooks;
1066
+ private readonly debugStats;
901
1067
  private commandBuffer;
902
- private readonly _changeset;
903
- private readonly _removeChangeset;
904
- /** Cached command processor context to avoid per-entity object allocation */
905
- private readonly _commandCtx;
906
- /** Cached hooks context to avoid per-entity object allocation */
907
- private readonly _hooksCtx;
1068
+ private commandExecutor;
908
1069
  constructor(snapshot?: SerializedWorld);
909
- private createArchetypeSignature;
910
1070
  /**
911
1071
  * Creates a new entity.
912
1072
  * The entity is created with an empty component set and can be configured using `set()`.
@@ -951,11 +1111,6 @@ declare class World {
951
1111
  * if (world.has(entity, Position)) { ... }
952
1112
  */
953
1113
  exists(entityId: EntityId): boolean;
954
- private assertEntityExists;
955
- private assertComponentTypeValid;
956
- private assertSetComponentTypeValid;
957
- private resolveSetOperation;
958
- private resolveRemoveOperation;
959
1114
  /**
960
1115
  * Adds or updates a component on an entity (or marks void component as present).
961
1116
  * The change is buffered and takes effect after calling `world.sync()`.
@@ -967,21 +1122,17 @@ declare class World {
967
1122
  * @overload set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void
968
1123
  * Adds or updates a component with data on the entity
969
1124
  *
970
- * @overload set<T>(componentId: ComponentId<T>, component: NoInfer<T>): void
971
- * Adds or updates a singleton component (shorthand for set(componentId, componentId, component))
972
- *
973
1125
  * @throws {Error} If the entity does not exist
974
1126
  * @throws {Error} If the component type is invalid or is a wildcard relation
975
1127
  *
976
1128
  * @example
977
1129
  * world.set(entity, Position, { x: 10, y: 20 });
978
1130
  * world.set(entity, Marker); // void component
979
- * world.set(GlobalConfig, { debug: true }); // singleton component
1131
+ * world.singleton(GlobalConfig).set({ debug: true }); // singleton component
980
1132
  * world.sync(); // Apply changes
981
1133
  */
982
1134
  set(entityId: EntityId, componentType: EntityId<void>): void;
983
1135
  set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void;
984
- set<T>(componentId: ComponentId<T>, component: NoInfer<T>): void;
985
1136
  /**
986
1137
  * Removes a component from an entity.
987
1138
  * The change is buffered and takes effect after calling `world.sync()`.
@@ -1019,6 +1170,18 @@ declare class World {
1019
1170
  * world.sync(); // Apply changes
1020
1171
  */
1021
1172
  delete(entityId: EntityId): void;
1173
+ /**
1174
+ * Returns an explicit handle for a singleton component (component-as-entity).
1175
+ *
1176
+ * This is the preferred API for singleton components.
1177
+ *
1178
+ * @example
1179
+ * const config = world.singleton(GlobalConfig);
1180
+ * config.set({ debug: true });
1181
+ * world.sync();
1182
+ * console.log(config.get());
1183
+ */
1184
+ singleton<T>(componentId: ComponentId<T>): SingletonHandle<T>;
1022
1185
  /**
1023
1186
  * Checks if a specific **component** is present on an entity.
1024
1187
  *
@@ -1112,6 +1275,106 @@ declare class World {
1112
1275
  getOptional<T>(entityId: EntityId, componentType: EntityId<T>): {
1113
1276
  value: T;
1114
1277
  } | undefined;
1278
+ /**
1279
+ * Retrieves all targets (and their associated data) for relations of a given
1280
+ * base component on an entity.
1281
+ *
1282
+ * This is the ergonomic replacement for the common pattern:
1283
+ * world.get(entity, relation(Comp, "*"))
1284
+ *
1285
+ * @example
1286
+ * const ChildOf = component({ exclusive: true, sparse: true });
1287
+ * const children = world.getRelationTargets(parent, ChildOf); // usually []
1288
+ * const items = world.getRelationTargets(player, InInventory);
1289
+ *
1290
+ * // For common hierarchy use cases, prefer the higher-level helpers:
1291
+ * // world.getChildren(parent, ChildOf), world.getParent(child, ChildOf)
1292
+ */
1293
+ getRelationTargets<T = void>(entityId: EntityId, relationComp: ComponentId<T>): [target: EntityId<unknown>, data: T | undefined][];
1294
+ /**
1295
+ * Returns every entity that currently holds a relation of the given base
1296
+ * component pointing at `targetId`.
1297
+ *
1298
+ * This is the efficient **reverse** lookup. For common hierarchy cases,
1299
+ * prefer the higher-level `world.getChildren(parent, ChildOf)` instead.
1300
+ *
1301
+ * @example
1302
+ * const ChildOf = component({ exclusive: true, sparse: true });
1303
+ * const directChildren = world.getRelationSources(ship, ChildOf);
1304
+ */
1305
+ getRelationSources(targetId: EntityId, relationComp: ComponentId<any>): EntityId[];
1306
+ /**
1307
+ * Returns true if the entity has any (or a specific-target) relation of the
1308
+ * given base component.
1309
+ */
1310
+ hasRelation(entityId: EntityId, relationComp: ComponentId<any>, targetId?: EntityId): boolean;
1311
+ /**
1312
+ * Returns the number of relations of the given base component held by the entity.
1313
+ */
1314
+ countRelations(entityId: EntityId, relationComp: ComponentId<any>): number;
1315
+ /**
1316
+ * For an *exclusive* relation (e.g. ChildOf, Owner), returns the single
1317
+ * target entity (or undefined if none).
1318
+ *
1319
+ * When the component was declared `exclusive: true`, this is the preferred
1320
+ * accessor (clearer intent than array destructuring).
1321
+ */
1322
+ getSingleRelationTarget<T = void>(entityId: EntityId, relationComp: ComponentId<T>): EntityId | undefined;
1323
+ /**
1324
+ * Returns the direct children of `parent` for the given relationship component
1325
+ * (typically a `ChildOf` or similar exclusive `sparse` relation).
1326
+ *
1327
+ * This is the recommended high-level API for hierarchy traversal.
1328
+ * It uses the internal reverse reference index for efficiency.
1329
+ *
1330
+ * @example
1331
+ * const ChildOf = component({ exclusive: true, sparse: true });
1332
+ * const kids = world.getChildren(ship, ChildOf);
1333
+ */
1334
+ getChildren(parent: EntityId, childOf: ComponentId<any>): EntityId[];
1335
+ /**
1336
+ * Returns the parent of `child` for the given relationship component
1337
+ * (typically an exclusive `ChildOf` relation).
1338
+ *
1339
+ * @example
1340
+ * const ChildOf = component({ exclusive: true, sparse: true });
1341
+ * const parent = world.getParent(turret, ChildOf);
1342
+ */
1343
+ getParent(child: EntityId, childOf: ComponentId<any>): EntityId | undefined;
1344
+ /**
1345
+ * Returns the ancestor chain from the immediate parent up to (but not
1346
+ * including) the root for the given relationship component.
1347
+ *
1348
+ * @example
1349
+ * const ChildOf = component({ exclusive: true, sparse: true });
1350
+ * const ancestors = world.getAncestors(muzzle, ChildOf); // [turret, ship]
1351
+ */
1352
+ getAncestors(entity: EntityId, childOf: ComponentId<any>): EntityId[];
1353
+ /**
1354
+ * Iteratively traverses all descendants of `root` in DFS pre-order.
1355
+ * This is a generator and is safe for very deep hierarchies.
1356
+ *
1357
+ * @example
1358
+ * for (const { entity, depth, parent } of world.iterateDescendants(root, ChildOf)) {
1359
+ * console.log(depth, entity);
1360
+ * }
1361
+ */
1362
+ iterateDescendants(root: EntityId, childOf: ComponentId<any>, opts?: {
1363
+ includeSelf?: boolean;
1364
+ maxDepth?: number;
1365
+ }): IterableIterator<{
1366
+ entity: EntityId;
1367
+ depth: number;
1368
+ parent: EntityId | null;
1369
+ }>;
1370
+ /**
1371
+ * Callback-based descendant traversal (hot path friendly).
1372
+ * Return `false` from the visitor to stop early.
1373
+ */
1374
+ traverseDescendants(root: EntityId, childOf: ComponentId<any>, visitor: (entity: EntityId, depth: number, parent: EntityId | null) => void | boolean, opts?: {
1375
+ includeSelf?: boolean;
1376
+ maxDepth?: number;
1377
+ }): void;
1115
1378
  /**
1116
1379
  * Registers a lifecycle hook that responds to component changes.
1117
1380
  * The hook callback is invoked when components matching the specified types are added, updated, or removed.
@@ -1153,6 +1416,19 @@ declare class World {
1153
1416
  * );
1154
1417
  */
1155
1418
  hook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T> | LifecycleCallback<T>, filter?: QueryFilter): () => void;
1419
+ /**
1420
+ * Creates a debug stats collector that will receive a `SyncDebugStats` payload
1421
+ * after every subsequent `sync()`.
1422
+ *
1423
+ * The returned object is a pure lifecycle handle. It does not store data.
1424
+ * Collection stops when you call `[Symbol.dispose]()` (or use a `using` declaration).
1425
+ *
1426
+ * All active collectors receive the exact same stats object for a given sync.
1427
+ * Exceptions thrown by callbacks are ignored.
1428
+ *
1429
+ * This is intended for development/debugging and leak detection.
1430
+ */
1431
+ createDebugStatsCollector(callback: (stats: SyncDebugStats) => void): DebugStatsCollector;
1156
1432
  /**
1157
1433
  * Synchronizes all buffered commands (set/remove/delete) to the world.
1158
1434
  * This method must be called after making changes via `set()`, `remove()`, or `delete()` for them to take effect.
@@ -1248,7 +1524,6 @@ declare class World {
1248
1524
  * @internal
1249
1525
  */
1250
1526
  getMatchingArchetypes(componentTypes: EntityId<any>[]): Archetype[];
1251
- private getArchetypesWithComponents;
1252
1527
  /**
1253
1528
  * Queries entities with specific components.
1254
1529
  * For simpler use cases, prefer using `createQuery()` with `forEach()` which is cached and more efficient.
@@ -1281,21 +1556,9 @@ declare class World {
1281
1556
  entity: EntityId;
1282
1557
  components: ComponentTuple<T>;
1283
1558
  }>;
1284
- private executeEntityCommands;
1285
- private applyEntityCommands;
1286
- private createHooksContext;
1287
- private removeComponentImmediate;
1288
- private updateEntityReferences;
1289
1559
  private ensureArchetype;
1290
- /** Add componentType to the reverse index if it contains an entity ID */
1291
- private addToReferencingIndex;
1292
- /** Remove componentType from the reverse index */
1293
- private removeFromReferencingIndex;
1294
- private createNewArchetype;
1295
- private updateArchetypeHookMatches;
1296
- private archetypeMatchesHook;
1297
1560
  private cleanupArchetypesReferencingEntity;
1298
- private removeArchetype;
1561
+ private archetypeMatchesHook;
1299
1562
  /**
1300
1563
  * Serializes the entire world state to a plain JavaScript object.
1301
1564
  * This creates a "memory snapshot" that can be stored or transmitted.
@@ -1317,6 +1580,8 @@ declare class World {
1317
1580
  * const savedData = JSON.parse(localStorage.getItem('save'));
1318
1581
  * const newWorld = new World(savedData);
1319
1582
  */
1583
+ private removeComponentImmediate;
1584
+ private createHooksContext;
1320
1585
  serialize(): SerializedWorld;
1321
1586
  }
1322
1587
  //#endregion
@@ -1393,5 +1658,5 @@ declare class EntityBuilder {
1393
1658
  build(): EntityId;
1394
1659
  }
1395
1660
  //#endregion
1396
- export { EntityRelationId as C, isEntityId as D, isComponentId as E, isRelationId as O, EntityId as S, WildcardRelationId as T, decodeRelationId as _, ComponentTuple as a, ComponentId as b, LifecycleHook as c, SerializedEntityId as d, SerializedWorld as f, getComponentNameById as g, getComponentIdByName as h, Query as i, SerializedComponent as l, component as m, EntityBuilder as n, ComponentType as o, ComponentOptions as p, World as r, LifecycleCallback as s, ComponentDef as t, SerializedEntity as u, isWildcardRelationId as v, RelationId as w, ComponentRelationId as x, relation as y };
1661
+ export { EntityRelationId as A, isSparseWildcard as C, ComponentId as D, relation as E, isRelationId as F, WildcardRelationId as M, isComponentId as N, ComponentRelationId as O, isEntityId as P, isSparseRelation as S, isWildcardRelationId as T, ComponentOptions as _, SingletonHandleOps as a, getComponentNameById as b, ComponentType as c, LifecycleHook as d, SyncDebugStats as f, SerializedWorld as g, SerializedEntityId as h, SingletonHandle as i, RelationId as j, EntityId as k, DebugStatsCollector as l, SerializedEntity as m, EntityBuilder as n, Query as o, SerializedComponent as p, World as r, ComponentTuple as s, ComponentDef as t, LifecycleCallback as u, component as v, decodeRelationId as w, isSparseComponent as x, getComponentIdByName as y };
1397
1662
  //# sourceMappingURL=builder.d.mts.map
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { C as EntityRelationId, D as isEntityId, E as isComponentId, O as isRelationId, S as EntityId, T as WildcardRelationId, _ as decodeRelationId, a as ComponentTuple, b as ComponentId, c as LifecycleHook, d as SerializedEntityId, f as SerializedWorld, g as getComponentNameById, h as getComponentIdByName, i as Query, l as SerializedComponent, m as component, n as EntityBuilder, o as ComponentType, p as ComponentOptions, r as World, s as LifecycleCallback, t as ComponentDef, u as SerializedEntity, v as isWildcardRelationId, w as RelationId, x as ComponentRelationId, y as relation } from "./builder.mjs";
2
- export { type ComponentDef, type ComponentId, type ComponentOptions, type ComponentRelationId, type ComponentTuple, type ComponentType, EntityBuilder, type EntityId, type EntityRelationId, type LifecycleCallback, type LifecycleHook, Query, type RelationId, type SerializedComponent, type SerializedEntity, type SerializedEntityId, type SerializedWorld, type WildcardRelationId, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isEntityId, isRelationId, isWildcardRelationId, relation };
1
+ import { A as EntityRelationId, C as isSparseWildcard, D as ComponentId, E as relation, F as isRelationId, M as WildcardRelationId, N as isComponentId, O as ComponentRelationId, P as isEntityId, S as isSparseRelation, T as isWildcardRelationId, _ as ComponentOptions, a as SingletonHandleOps, b as getComponentNameById, c as ComponentType, d as LifecycleHook, f as SyncDebugStats, g as SerializedWorld, h as SerializedEntityId, i as SingletonHandle, j as RelationId, k as EntityId, l as DebugStatsCollector, m as SerializedEntity, n as EntityBuilder, o as Query, p as SerializedComponent, r as World, s as ComponentTuple, t as ComponentDef, u as LifecycleCallback, v as component, w as decodeRelationId, x as isSparseComponent, y as getComponentIdByName } from "./builder.mjs";
2
+ export { type ComponentDef, type ComponentId, type ComponentOptions, type ComponentRelationId, type ComponentTuple, type ComponentType, type DebugStatsCollector, EntityBuilder, type EntityId, type EntityRelationId, type LifecycleCallback, type LifecycleHook, Query, type RelationId, type SerializedComponent, type SerializedEntity, type SerializedEntityId, type SerializedWorld, SingletonHandle, type SingletonHandleOps, type SyncDebugStats, type WildcardRelationId, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isSparseComponent as isDontFragmentComponent, isSparseComponent, isSparseRelation as isDontFragmentRelation, isSparseRelation, isSparseWildcard as isDontFragmentWildcard, isSparseWildcard, isEntityId, isRelationId, isWildcardRelationId, relation };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as getComponentIdByName, c as isWildcardRelationId, d as isEntityId, f as isRelationId, i as component, l as relation, n as Query, o as getComponentNameById, r as EntityBuilder, s as decodeRelationId, t as World, u as isComponentId } from "./world.mjs";
2
- export { EntityBuilder, Query, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isEntityId, isRelationId, isWildcardRelationId, relation };
1
+ import { a as component, c as isSparseComponent, d as decodeRelationId, f as isWildcardRelationId, g as isRelationId, h as isEntityId, i as EntityBuilder, l as isSparseRelation, m as isComponentId, n as Query, o as getComponentIdByName, p as relation, r as SingletonHandle, s as getComponentNameById, t as World, u as isSparseWildcard } from "./world.mjs";
2
+ export { EntityBuilder, Query, SingletonHandle, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isSparseComponent as isDontFragmentComponent, isSparseComponent, isSparseRelation as isDontFragmentRelation, isSparseRelation, isSparseWildcard as isDontFragmentWildcard, isSparseWildcard, isEntityId, isRelationId, isWildcardRelationId, relation };
@@ -1,4 +1,4 @@
1
- import { S as EntityId, T as WildcardRelationId, b as ComponentId, c as LifecycleHook, i as Query, m as component, n as EntityBuilder, r as World, s as LifecycleCallback, t as ComponentDef, w as RelationId, y as relation } from "./builder.mjs";
1
+ import { D as ComponentId, E as relation, M as WildcardRelationId, d as LifecycleHook, j as RelationId, k as EntityId, n as EntityBuilder, o as Query, r as World, t as ComponentDef, u as LifecycleCallback, v as component } from "./builder.mjs";
2
2
 
3
3
  //#region src/testing/index.d.ts
4
4
  /**