@codehz/ecs 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.en.md +26 -3
  2. package/README.md +28 -3
  3. package/dist/builder.d.mts +296 -46
  4. package/dist/index.d.mts +2 -2
  5. package/dist/index.mjs +2 -2
  6. package/dist/testing.d.mts +1 -1
  7. package/dist/testing.mjs +1 -1
  8. package/dist/world.mjs +452 -179
  9. package/dist/world.mjs.map +1 -1
  10. package/examples/debug-observability.ts +92 -0
  11. package/examples/inventory-system-relations.ts +1 -1
  12. package/examples/parent-child-hierarchy.ts +18 -38
  13. package/package.json +1 -1
  14. package/skills/ecs/SKILL.md +4 -4
  15. package/src/__tests__/component/singleton.test.ts +40 -1
  16. package/src/__tests__/core/archetype.test.ts +155 -13
  17. package/src/__tests__/core/bitset.test.ts +12 -0
  18. package/src/__tests__/entity/entity.test.ts +33 -0
  19. package/src/__tests__/entity/id-system.test.ts +40 -0
  20. package/src/__tests__/perf/comprehensive.perf.test.ts +6 -9
  21. package/src/__tests__/perf/serialization.perf.test.ts +242 -0
  22. package/src/__tests__/perf/{dontfragment-wildcard.perf.test.ts → sparse-wildcard.perf.test.ts} +13 -16
  23. package/src/__tests__/query/caching.test.ts +62 -0
  24. package/src/__tests__/query/filter.test.ts +16 -22
  25. package/src/__tests__/query/perf.test.ts +3 -5
  26. package/src/__tests__/relations/hierarchy.test.ts +208 -0
  27. package/src/__tests__/relations/{dont-fragment → sparse}/basic.test.ts +64 -69
  28. package/src/__tests__/relations/{dont-fragment → sparse}/query-notification.test.ts +17 -9
  29. package/src/__tests__/serialization/bounds.test.ts +134 -1
  30. package/src/__tests__/world/commands.test.ts +337 -0
  31. package/src/__tests__/world/debug-stats.test.ts +206 -0
  32. package/src/__tests__/world/multi-component-hooks.test.ts +44 -0
  33. package/src/__tests__/world/serialize.test.ts +17 -0
  34. package/src/__tests__/world/wildcard-relation-hooks.test.ts +127 -0
  35. package/src/archetype/archetype.ts +96 -46
  36. package/src/archetype/helpers.ts +7 -29
  37. package/src/archetype/store.ts +35 -20
  38. package/src/commands/buffer.ts +5 -2
  39. package/src/commands/changeset.ts +0 -31
  40. package/src/component/registry.ts +64 -63
  41. package/src/entity/index.ts +6 -3
  42. package/src/index.ts +13 -0
  43. package/src/query/filter.ts +4 -10
  44. package/src/query/query.ts +12 -12
  45. package/src/storage/serialization.ts +29 -2
  46. package/src/types/index.ts +71 -0
  47. package/src/world/commands.ts +44 -56
  48. package/src/world/hooks.ts +8 -0
  49. package/src/world/serialization.ts +32 -18
  50. package/src/world/world.ts +387 -20
package/README.en.md CHANGED
@@ -78,7 +78,7 @@ const ChildOf = component({ exclusive: true, name: "ChildOf" });
78
78
  | `name` | `string` | Component name, used for serialization/debugging |
79
79
  | `exclusive` | `boolean` | Relation components only: an entity can have at most one relation of the same base component |
80
80
  | `cascadeDelete` | `boolean` | Entity relations only: when the target entity is deleted, the **entire referencing entity** is deleted. Differs from default behavior (default only cleans up the relation component, the entity survives). Supports transitive cascading. |
81
- | `dontFragment` | `boolean` | Relation components only: relations with different target entities are stored in the same Archetype, preventing excessive fragmentation |
81
+ | `sparse` | `boolean` | Relation components only: relations with different target entities are stored in the same Archetype, preventing excessive fragmentation (legacy `dontFragment` alias remains fully supported) |
82
82
  | `merge` | `(prev, next) => T` | Merge strategy when `set()` is called multiple times on the same component within a single sync batch |
83
83
 
84
84
  ### Lifecycle Hooks
@@ -274,7 +274,7 @@ component<T>();
274
274
  // With a name
275
275
  component<T>("Name");
276
276
  // With options
277
- component<T>({ name?: string, exclusive?: boolean, cascadeDelete?: boolean, dontFragment?: boolean, merge?: (prev, next) => T });
277
+ component<T>({ name?: string, exclusive?: boolean, cascadeDelete?: boolean, sparse?: boolean, dontFragment?: boolean /* legacy alias, fully compatible */, merge?: (prev, next) => T });
278
278
  ```
279
279
 
280
280
  ### relation()
@@ -288,6 +288,29 @@ relation(componentId, "*");
288
288
  relation(componentId, otherComponentId);
289
289
  ```
290
290
 
291
+ ### Relation & Hierarchy Companion Tools (New)
292
+
293
+ To stop users from repeatedly hand-writing `buildChildrenByParent` + recursive descent for parent-child hierarchies and inventory systems, we now ship first-class helpers:
294
+
295
+ ```typescript
296
+ const ChildOf = component<void>({ exclusive: true, sparse: true });
297
+ const world = new World();
298
+ // ... build hierarchy ...
299
+
300
+ // Recommended usage (standalone functions removed to simplify API surface)
301
+ const kids = world.getChildren(parent, ChildOf);
302
+ const p = world.getParent(child, ChildOf);
303
+
304
+ for (const { entity, depth } of world.iterateDescendants(root, ChildOf)) { ... }
305
+
306
+ const items = world.getRelationTargets(player, InInventory);
307
+ const owners = world.getRelationSources(sword, InInventory);
308
+ ```
309
+
310
+ The same functionality is also available as methods on `World` instances. All helpers are fully compatible with data-bearing relations, exclusive/non-exclusive, and post-`sync()` semantics.
311
+
312
+ See `src/relations/hierarchy.ts` and the new test suite for details.
313
+
291
314
  ### Component / Entity ID Rules
292
315
 
293
316
  - Component ID: `1` – `1023`
@@ -391,7 +414,7 @@ src/
391
414
  │ ├── component-registry.ts # Component registry
392
415
  │ ├── component-entity-store.ts # Singleton component storage
393
416
  │ ├── component-type-utils.ts # Component type utilities
394
- │ ├── dont-fragment-store.ts # DontFragment storage
417
+ │ ├── store.ts # SparseStore (internal sparse storage)
395
418
  │ ├── entity.ts # Entity/component/relation type exports (aggregate)
396
419
  │ ├── entity-types.ts # Entity ID type definitions & constants
397
420
  │ ├── entity-relation.ts # Relation ID encoding/decoding
package/README.md CHANGED
@@ -78,7 +78,7 @@ const ChildOf = component({ exclusive: true, name: "ChildOf" });
78
78
  | `name` | `string` | 组件名称,用于序列化/调试 |
79
79
  | `exclusive` | `boolean` | 仅关系组件:同一实体对同一基础组件最多只能有一个关系 |
80
80
  | `cascadeDelete` | `boolean` | 仅实体关系:删除目标实体时,持有该关系的**整个实体**也会被删除。区别于默认行为(默认仅清理关系组件,实体保留)。支持传递级联。 |
81
- | `dontFragment` | `boolean` | 仅关系组件:不同目标实体的关系存放在同一 Archetype,防止因目标不同而过度碎片化 |
81
+ | `sparse` | `boolean` | 仅关系组件:不同目标实体的关系存放在同一 Archetype,防止因目标不同而过度碎片化(旧别名 `dontFragment` 仍完全兼容) |
82
82
  | `merge` | `(prev, next) => T` | 在同一 sync 批次中对同一组件反复 `set()` 时的合并策略 |
83
83
 
84
84
  ### 生命周期钩子
@@ -274,7 +274,7 @@ component<T>();
274
274
  // 指定名称
275
275
  component<T>("Name");
276
276
  // 带选项
277
- component<T>({ name?: string, exclusive?: boolean, cascadeDelete?: boolean, dontFragment?: boolean, merge?: (prev, next) => T });
277
+ component<T>({ name?: string, exclusive?: boolean, cascadeDelete?: boolean, sparse?: boolean, dontFragment?: boolean /* 旧别名,完全兼容 */, merge?: (prev, next) => T });
278
278
  ```
279
279
 
280
280
  ### relation()
@@ -288,6 +288,31 @@ relation(componentId, "*");
288
288
  relation(componentId, otherComponentId);
289
289
  ```
290
290
 
291
+ ### 关系/层级配套工具(新)
292
+
293
+ 为避免用户在父子层级(`ChildOf`)和库存系统(`InInventory`)中反复手写 `buildChildrenByParent` + 递归遍历逻辑,我们提供了配套工具:
294
+
295
+ ```typescript
296
+ const ChildOf = component<void>({ exclusive: true, sparse: true });
297
+ const world = new World();
298
+ // ... 创建层级 ...
299
+
300
+ // 推荐直接在 World 实例上使用(API 表面已简化)
301
+ const kids = world.getChildren(parent, ChildOf);
302
+ const p = world.getParent(child, ChildOf);
303
+
304
+ for (const { entity, depth, parent } of world.iterateDescendants(root, ChildOf)) {
305
+ // ...
306
+ }
307
+
308
+ const items = world.getRelationTargets(player, InInventory);
309
+ const owners = world.getRelationSources(sword, InInventory);
310
+ ```
311
+
312
+ 这些工具全部在 `world` 实例方法上也有对应(`world.getChildren(...)` 等),并完整支持数据负载关系、独占/非独占、删除后一致性。
313
+
314
+ 详见 `src/relations/hierarchy.ts` 和新增的测试。
315
+
291
316
  ### 组件 / 实体 ID 规则
292
317
 
293
318
  - 组件 ID:`1` ~ `1023`
@@ -391,7 +416,7 @@ src/
391
416
  │ ├── component-registry.ts # 组件注册表
392
417
  │ ├── component-entity-store.ts # 单例组件存储
393
418
  │ ├── component-type-utils.ts # 组件类型工具
394
- │ ├── dont-fragment-store.ts # DontFragment 存储
419
+ │ ├── store.ts # SparseStore (内部稀疏存储)
395
420
  │ ├── entity.ts # 实体/组件/关系类型导出(聚合)
396
421
  │ ├── entity-types.ts # 实体 ID 类型定义与常量
397
422
  │ ├── entity-relation.ts # 关系 ID 编码/解码
@@ -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
  /**
@@ -892,12 +1023,16 @@ declare class World {
892
1023
  private entityReferences;
893
1024
  /** Reverse index: entity ID → set of archetypes whose componentTypes include that entity ID */
894
1025
  private entityToReferencingArchetypes;
895
- /** DontFragment relation storage, shared with all Archetype instances */
896
- private readonly dontFragmentStore;
1026
+ /** Sparse relation storage (for components created with `sparse: true`), shared with all Archetype instances */
1027
+ private readonly sparseStore;
897
1028
  /** Component entity (singleton) storage */
898
1029
  private readonly componentEntities;
899
1030
  private readonly queryRegistry;
900
1031
  private hooks;
1032
+ private readonly _debugCollectors;
1033
+ private _debugMigrations;
1034
+ private _debugArchetypesCreated;
1035
+ private _debugArchetypesRemoved;
901
1036
  private commandBuffer;
902
1037
  private readonly _changeset;
903
1038
  private readonly _removeChangeset;
@@ -1112,6 +1247,106 @@ declare class World {
1112
1247
  getOptional<T>(entityId: EntityId, componentType: EntityId<T>): {
1113
1248
  value: T;
1114
1249
  } | undefined;
1250
+ /**
1251
+ * Retrieves all targets (and their associated data) for relations of a given
1252
+ * base component on an entity.
1253
+ *
1254
+ * This is the ergonomic replacement for the common pattern:
1255
+ * world.get(entity, relation(Comp, "*"))
1256
+ *
1257
+ * @example
1258
+ * const ChildOf = component({ exclusive: true, sparse: true });
1259
+ * const children = world.getRelationTargets(parent, ChildOf); // usually []
1260
+ * const items = world.getRelationTargets(player, InInventory);
1261
+ *
1262
+ * // For common hierarchy use cases, prefer the higher-level helpers:
1263
+ * // world.getChildren(parent, ChildOf), world.getParent(child, ChildOf)
1264
+ */
1265
+ getRelationTargets<T = void>(entityId: EntityId, relationComp: ComponentId<T>): [target: EntityId<unknown>, data: T | undefined][];
1266
+ /**
1267
+ * Returns every entity that currently holds a relation of the given base
1268
+ * component pointing at `targetId`.
1269
+ *
1270
+ * This is the efficient **reverse** lookup. For common hierarchy cases,
1271
+ * prefer the higher-level `world.getChildren(parent, ChildOf)` instead.
1272
+ *
1273
+ * @example
1274
+ * const ChildOf = component({ exclusive: true, sparse: true });
1275
+ * const directChildren = world.getRelationSources(ship, ChildOf);
1276
+ */
1277
+ getRelationSources(targetId: EntityId, relationComp: ComponentId<any>): EntityId[];
1278
+ /**
1279
+ * Returns true if the entity has any (or a specific-target) relation of the
1280
+ * given base component.
1281
+ */
1282
+ hasRelation(entityId: EntityId, relationComp: ComponentId<any>, targetId?: EntityId): boolean;
1283
+ /**
1284
+ * Returns the number of relations of the given base component held by the entity.
1285
+ */
1286
+ countRelations(entityId: EntityId, relationComp: ComponentId<any>): number;
1287
+ /**
1288
+ * For an *exclusive* relation (e.g. ChildOf, Owner), returns the single
1289
+ * target entity (or undefined if none).
1290
+ *
1291
+ * When the component was declared `exclusive: true`, this is the preferred
1292
+ * accessor (clearer intent than array destructuring).
1293
+ */
1294
+ getSingleRelationTarget<T = void>(entityId: EntityId, relationComp: ComponentId<T>): EntityId | undefined;
1295
+ /**
1296
+ * Returns the direct children of `parent` for the given relationship component
1297
+ * (typically a `ChildOf` or similar exclusive `sparse` relation).
1298
+ *
1299
+ * This is the recommended high-level API for hierarchy traversal.
1300
+ * It uses the internal reverse reference index for efficiency.
1301
+ *
1302
+ * @example
1303
+ * const ChildOf = component({ exclusive: true, sparse: true });
1304
+ * const kids = world.getChildren(ship, ChildOf);
1305
+ */
1306
+ getChildren(parent: EntityId, childOf: ComponentId<any>): EntityId[];
1307
+ /**
1308
+ * Returns the parent of `child` for the given relationship component
1309
+ * (typically an exclusive `ChildOf` relation).
1310
+ *
1311
+ * @example
1312
+ * const ChildOf = component({ exclusive: true, sparse: true });
1313
+ * const parent = world.getParent(turret, ChildOf);
1314
+ */
1315
+ getParent(child: EntityId, childOf: ComponentId<any>): EntityId | undefined;
1316
+ /**
1317
+ * Returns the ancestor chain from the immediate parent up to (but not
1318
+ * including) the root for the given relationship component.
1319
+ *
1320
+ * @example
1321
+ * const ChildOf = component({ exclusive: true, sparse: true });
1322
+ * const ancestors = world.getAncestors(muzzle, ChildOf); // [turret, ship]
1323
+ */
1324
+ getAncestors(entity: EntityId, childOf: ComponentId<any>): EntityId[];
1325
+ /**
1326
+ * Iteratively traverses all descendants of `root` in DFS pre-order.
1327
+ * This is a generator and is safe for very deep hierarchies.
1328
+ *
1329
+ * @example
1330
+ * for (const { entity, depth, parent } of world.iterateDescendants(root, ChildOf)) {
1331
+ * console.log(depth, entity);
1332
+ * }
1333
+ */
1334
+ iterateDescendants(root: EntityId, childOf: ComponentId<any>, opts?: {
1335
+ includeSelf?: boolean;
1336
+ maxDepth?: number;
1337
+ }): IterableIterator<{
1338
+ entity: EntityId;
1339
+ depth: number;
1340
+ parent: EntityId | null;
1341
+ }>;
1342
+ /**
1343
+ * Callback-based descendant traversal (hot path friendly).
1344
+ * Return `false` from the visitor to stop early.
1345
+ */
1346
+ traverseDescendants(root: EntityId, childOf: ComponentId<any>, visitor: (entity: EntityId, depth: number, parent: EntityId | null) => void | boolean, opts?: {
1347
+ includeSelf?: boolean;
1348
+ maxDepth?: number;
1349
+ }): void;
1115
1350
  /**
1116
1351
  * Registers a lifecycle hook that responds to component changes.
1117
1352
  * The hook callback is invoked when components matching the specified types are added, updated, or removed.
@@ -1153,6 +1388,21 @@ declare class World {
1153
1388
  * );
1154
1389
  */
1155
1390
  hook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T> | LifecycleCallback<T>, filter?: QueryFilter): () => void;
1391
+ /**
1392
+ * Creates a debug stats collector that will receive a `SyncDebugStats` payload
1393
+ * after every subsequent `sync()`.
1394
+ *
1395
+ * The returned object is a pure lifecycle handle. It does not store data.
1396
+ * Collection stops when you call `[Symbol.dispose]()` (or use a `using` declaration).
1397
+ *
1398
+ * All active collectors receive the exact same stats object for a given sync.
1399
+ * Exceptions thrown by callbacks are ignored.
1400
+ *
1401
+ * This is intended for development/debugging and leak detection.
1402
+ */
1403
+ createDebugStatsCollector(callback: (stats: SyncDebugStats) => void): DebugStatsCollector;
1404
+ private _resetDebugActivityCounters;
1405
+ private _deliverDebugStats;
1156
1406
  /**
1157
1407
  * Synchronizes all buffered commands (set/remove/delete) to the world.
1158
1408
  * This method must be called after making changes via `set()`, `remove()`, or `delete()` for them to take effect.
@@ -1393,5 +1643,5 @@ declare class EntityBuilder {
1393
1643
  build(): EntityId;
1394
1644
  }
1395
1645
  //#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 };
1646
+ export { WildcardRelationId as A, isWildcardRelationId as C, EntityId as D, ComponentRelationId as E, isEntityId as M, isRelationId as N, EntityRelationId as O, decodeRelationId as S, ComponentId as T, getComponentIdByName as _, ComponentTuple as a, isSparseRelation as b, LifecycleCallback as c, SerializedComponent as d, SerializedEntity as f, component as g, ComponentOptions as h, Query as i, isComponentId as j, RelationId as k, LifecycleHook as l, SerializedWorld as m, EntityBuilder as n, ComponentType as o, SerializedEntityId as p, World as r, DebugStatsCollector as s, ComponentDef as t, SyncDebugStats as u, getComponentNameById as v, relation as w, isSparseWildcard as x, isSparseComponent as y };
1397
1647
  //# 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 WildcardRelationId, C as isWildcardRelationId, D as EntityId, E as ComponentRelationId, M as isEntityId, N as isRelationId, O as EntityRelationId, S as decodeRelationId, T as ComponentId, _ as getComponentIdByName, a as ComponentTuple, b as isSparseRelation, c as LifecycleCallback, d as SerializedComponent, f as SerializedEntity, g as component, h as ComponentOptions, i as Query, j as isComponentId, k as RelationId, l as LifecycleHook, m as SerializedWorld, n as EntityBuilder, o as ComponentType, p as SerializedEntityId, r as World, s as DebugStatsCollector, t as ComponentDef, u as SyncDebugStats, v as getComponentNameById, w as relation, x as isSparseWildcard, y as isSparseComponent } 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, 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 getComponentIdByName, c as isSparseRelation, d as isWildcardRelationId, f as relation, h as isRelationId, i as component, l as isSparseWildcard, m as isEntityId, n as Query, o as getComponentNameById, p as isComponentId, r as EntityBuilder, s as isSparseComponent, t as World, u as decodeRelationId } from "./world.mjs";
2
+ export { EntityBuilder, Query, 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 { A as WildcardRelationId, D as EntityId, T as ComponentId, c as LifecycleCallback, g as component, i as Query, k as RelationId, l as LifecycleHook, n as EntityBuilder, r as World, t as ComponentDef, w as relation } from "./builder.mjs";
2
2
 
3
3
  //#region src/testing/index.d.ts
4
4
  /**
package/dist/testing.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { c as isWildcardRelationId, i as component, l as relation, r as EntityBuilder, t as World } from "./world.mjs";
1
+ import { d as isWildcardRelationId, f as relation, i as component, r as EntityBuilder, t as World } from "./world.mjs";
2
2
  //#region src/testing/index.ts
3
3
  /**
4
4
  * A test fixture that manages a World instance and provides convenient