@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
@@ -138,7 +138,7 @@ export interface ComponentOptions<T = any> {
138
138
  * `cascadeDelete`, deleting the target entity will both (a) delete the
139
139
  * referencing entity, and (b) the exclusivity constraint prevents the
140
140
  * entity from having multiple cascade-delete relations of the same type.
141
- * - **`dontFragment`**: Compatible. Exclusivity is enforced at the data level
141
+ * - **`sparse`**: Compatible. Exclusivity is enforced at the data level
142
142
  * regardless of whether the archetype is fragmented.
143
143
  *
144
144
  * @example
@@ -196,7 +196,8 @@ export interface ComponentOptions<T = any> {
196
196
  */
197
197
  cascadeDelete?: boolean;
198
198
  /**
199
- * If true, relations with this component will not cause archetype fragmentation.
199
+ * If true, relations with this component use sparse storage and will not cause
200
+ * archetype fragmentation.
200
201
  *
201
202
  * **Problem it solves**: By default, each unique relation pair `(component, target)`
202
203
  * creates a **separate archetype**. If 100 entities each have a `ChildOf` relation
@@ -204,28 +205,28 @@ export interface ComponentOptions<T = any> {
204
205
  * Queries that iterate over all entities with a `ChildOf` relation must check all
205
206
  * 100 archetypes, which degrades iteration performance and increases memory overhead.
206
207
  *
207
- * **How it works**: When `dontFragment` is enabled, the relation's target does **not**
208
- * contribute to the archetype signature. Entities with different targets for the same
209
- * relation component share a **single archetype**, and the per-entity target data is
210
- * stored in a separate `DontFragmentStore` (a `Map<EntityId, Map<EntityId, any>>`).
211
- * A wildcard relation marker (`relation(Comp, "*")`) is placed in the archetype
212
- * component list so queries can still discover matching archetypes.
208
+ * **How it works (sparse storage)**: When `sparse` is enabled, the relation's target
209
+ * does **not** contribute to the archetype signature. Entities with different targets
210
+ * for the same relation component share a **single archetype**, and the per-entity
211
+ * target data is stored in a separate side store (historically called
212
+ * `DontFragmentStore`, now `SparseStore`). A wildcard relation marker (`relation(Comp, "*")`) is placed
213
+ * in the archetype component list so queries can still discover matching archetypes.
213
214
  *
214
215
  * **Use cases**:
215
216
  * - **Hierarchy/ownership**: `ChildOf` relations where thousands of entities each
216
217
  * point to different parent entities.
217
218
  * - **Dynamic targeting**: Relations where targets change frequently (e.g., AI
218
- * targeting, inventory slots) — without `dontFragment`, each target change would
219
- * cause an archetype migration, which is expensive.
219
+ * targeting, inventory slots) — without `sparse`, each target change would cause
220
+ * an archetype migration, which is expensive.
220
221
  * - **High-cardinality relations**: Any relation where the number of unique targets
221
222
  * is large compared to the number of entities.
222
223
  *
223
224
  * **Performance implications**:
224
- * - **Without `dontFragment`**: Archetype count grows linearly with unique targets.
225
+ * - **Without `sparse`**: Archetype count grows linearly with unique targets.
225
226
  * Each archetype migration (changing a relation target) requires moving the entity's
226
227
  * data between component arrays.
227
- * - **With `dontFragment`**: Archetype count stays constant regardless of target
228
- * diversity. Changing a relation target is an O(1) update in the `DontFragmentStore`.
228
+ * - **With `sparse`**: Archetype count stays constant regardless of target diversity.
229
+ * Changing a relation target is an O(1) update in the sparse side store.
229
230
  * The trade-off is an extra map lookup when accessing the relation data.
230
231
  *
231
232
  * **Constraints**:
@@ -234,13 +235,16 @@ export interface ComponentOptions<T = any> {
234
235
  * archetype carries a wildcard marker so queries can discover it.
235
236
  * - Works with `exclusive` and `cascadeDelete` simultaneously.
236
237
  *
238
+ * **Backward compatibility**: The legacy key `dontFragment` is still accepted and
239
+ * behaves identically. Prefer `sparse` in new code.
240
+ *
237
241
  * @example
238
242
  * ```ts
239
- * // Without dontFragment: 100 entities with different parents = 100 archetypes
243
+ * // Without sparse: 100 entities with different parents = 100 archetypes
240
244
  * const ChildOf = component(); // default: fragmentation happens
241
245
  *
242
- * // With dontFragment: 100 entities with different parents = 1 archetype
243
- * const ChildOf = component({ dontFragment: true });
246
+ * // With sparse: 100 entities with different parents = 1 archetype
247
+ * const ChildOf = component({ sparse: true });
244
248
  *
245
249
  * for (let i = 0; i < 100; i++) {
246
250
  * const parent = world.new();
@@ -249,11 +253,17 @@ export interface ComponentOptions<T = any> {
249
253
  * world.set(child, relation(ChildOf, parent));
250
254
  * }
251
255
  * world.sync();
252
- * // dontFragment: 1 archetype for all 100 entities
256
+ * // sparse: 1 archetype for all 100 entities
253
257
  * // without: 100 archetypes, one per unique parent
254
258
  * ```
255
259
  *
256
- * Inspired by Flecs' `DontFragment` trait.
260
+ * Inspired by Flecs' `DontFragment` trait (now exposed as the clearer `sparse` option).
261
+ */
262
+ sparse?: boolean;
263
+ /**
264
+ * @deprecated Use `sparse: true` instead. This key is kept solely for backward
265
+ * compatibility; `component({ dontFragment: true })` continues to work exactly
266
+ * as before and is equivalent to `sparse: true`.
257
267
  */
258
268
  dontFragment?: boolean;
259
269
  /**
@@ -324,7 +334,7 @@ const componentNames: (string | undefined)[] = new Array(COMPONENT_ID_MAX + 1);
324
334
  // BitSets for fast component option checks (Component ID range: 1-1023)
325
335
  const exclusiveFlags = new BitSet(COMPONENT_ID_MAX + 1);
326
336
  const cascadeDeleteFlags = new BitSet(COMPONENT_ID_MAX + 1);
327
- const dontFragmentFlags = new BitSet(COMPONENT_ID_MAX + 1);
337
+ const sparseFlags = new BitSet(COMPONENT_ID_MAX + 1);
328
338
  const componentMerges: (ComponentMerge<any> | undefined)[] = new Array(COMPONENT_ID_MAX + 1);
329
339
 
330
340
  /**
@@ -370,7 +380,8 @@ export function component<T = void>(nameOrOptions?: string | ComponentOptions<T>
370
380
  // Set bitset flags for fast lookup
371
381
  if (options.exclusive) exclusiveFlags.set(id);
372
382
  if (options.cascadeDelete) cascadeDeleteFlags.set(id);
373
- if (options.dontFragment) dontFragmentFlags.set(id);
383
+ // Support both `sparse` (preferred) and the legacy `dontFragment` alias for BC
384
+ if (options.sparse || options.dontFragment) sparseFlags.set(id);
374
385
  if (options.merge) componentMerges[id] = options.merge;
375
386
  }
376
387
 
@@ -406,12 +417,14 @@ export function getComponentOptions<T = any>(id: ComponentId<T>): ComponentOptio
406
417
  const hasName = componentNames[id] !== undefined;
407
418
  const hasExclusive = exclusiveFlags.has(id);
408
419
  const hasCascadeDelete = cascadeDeleteFlags.has(id);
409
- const hasDontFragment = dontFragmentFlags.has(id);
420
+ const hasSparse = sparseFlags.has(id);
410
421
  return {
411
422
  name: hasName ? componentNames[id] : undefined,
412
423
  exclusive: hasExclusive ? true : undefined,
413
424
  cascadeDelete: hasCascadeDelete ? true : undefined,
414
- dontFragment: hasDontFragment ? true : undefined,
425
+ sparse: hasSparse ? true : undefined,
426
+ // For full backward compatibility with code that inspects options.dontFragment
427
+ dontFragment: hasSparse ? true : undefined,
415
428
  merge: componentMerges[id] as ComponentMerge<T> | undefined,
416
429
  };
417
430
  }
@@ -487,20 +500,21 @@ export function isCascadeDeleteComponent(id: ComponentId<any>): boolean {
487
500
  }
488
501
 
489
502
  /**
490
- * Check if a component is marked as `dontFragment`.
503
+ * Check if a component is marked as `sparse` (sparse storage for relations).
491
504
  *
492
- * When a component has `dontFragment: true`, relations using it do not cause
493
- * archetype fragmentation — entities with different relation targets can share
494
- * the same archetype. This is a fast O(1) bitset lookup.
505
+ * When a component has `sparse: true`, relations using it do not cause archetype
506
+ * fragmentation — entities with different relation targets can share the same
507
+ * archetype. This is a fast O(1) bitset lookup. The legacy `dontFragment` key
508
+ * is still accepted and sets the same internal flag.
495
509
  *
496
510
  * @param id - The component ID to check.
497
- * @returns `true` if the component was created with `dontFragment: true`.
511
+ * @returns `true` if the component was created with `sparse: true` (or the
512
+ * legacy `dontFragment: true`).
498
513
  *
499
- * @see {@link ComponentOptions.dontFragment} for the full explanation of how
500
- * `dontFragment` prevents archetype fragmentation.
514
+ * @see {@link ComponentOptions.sparse} for the full explanation of sparse storage.
501
515
  */
502
- export function isDontFragmentComponent(id: ComponentId<any>): boolean {
503
- return dontFragmentFlags.has(id);
516
+ export function isSparseComponent(id: ComponentId<any>): boolean {
517
+ return sparseFlags.has(id);
504
518
  }
505
519
 
506
520
  /**
@@ -511,9 +525,8 @@ export function isDontFragmentComponent(id: ComponentId<any>): boolean {
511
525
  * ID and checking: (1) the ID is a valid relation, (2) the component ID is in the
512
526
  * valid range, (3) the target satisfies the condition, and (4) the flag bit is set.
513
527
  *
514
- * Used as the fast-path implementation for `isDontFragmentRelation`,
515
- * `isDontFragmentWildcard`, `isExclusiveRelation`, `isExclusiveWildcard`,
516
- * and `isCascadeDeleteRelation`.
528
+ * Used as the fast-path implementation for `isSparseRelation`, `isSparseWildcard`,
529
+ * `isExclusiveRelation`, `isExclusiveWildcard`, and `isCascadeDeleteRelation`.
517
530
  *
518
531
  * @param id - The entity/relation ID to check.
519
532
  * @param flagBitSet - The bitset tracking which component IDs have the flag.
@@ -534,53 +547,41 @@ function checkRelationFlag(
534
547
  }
535
548
 
536
549
  /**
537
- * Check if an ID is a specific (non-wildcard) relation backed by a `dontFragment`
538
- * component.
550
+ * Check if an ID is a specific (non-wildcard) relation backed by a `sparse`
551
+ * component (i.e. stored in the side sparse store rather than the archetype).
539
552
  *
540
553
  * This is used in hot paths (archetype resolution, command processing) to determine
541
- * whether a relation should be excluded from the archetype signature. Relations with
542
- * `dontFragment` components are stored in the shared {@link DontFragmentStore} instead
543
- * of being part of the archetype's component type list.
544
- *
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.
554
+ * whether a relation should be excluded from the archetype signature.
548
555
  *
549
556
  * @param id - The entity/relation ID to check (must be a relation ID, not a plain
550
557
  * component ID).
551
558
  * @returns `true` if this is a specific-target relation (not wildcard) whose base
552
- * component was created with `dontFragment: true`.
559
+ * component was created with `sparse: true` (or legacy `dontFragment: true`).
553
560
  *
554
- * @see {@link isDontFragmentWildcard} for the wildcard variant.
555
- * @see {@link ComponentOptions.dontFragment} for the full explanation.
561
+ * @see {@link isSparseWildcard} for the wildcard variant.
562
+ * @see {@link ComponentOptions.sparse} for the full explanation.
556
563
  */
557
- export function isDontFragmentRelation(id: EntityId<any>): boolean {
558
- return checkRelationFlag(id, dontFragmentFlags, (targetId) => targetId !== WILDCARD_TARGET_ID);
564
+ export function isSparseRelation(id: EntityId<any>): boolean {
565
+ return checkRelationFlag(id, sparseFlags, (targetId) => targetId !== WILDCARD_TARGET_ID);
559
566
  }
560
567
 
561
568
  /**
562
569
  * Check if an ID is a wildcard relation (`relation(Comp, "*")`) backed by a
563
- * `dontFragment` component.
564
- *
565
- * Wildcard markers for `dontFragment` components are placed in the archetype
566
- * component list so that queries can discover archetypes containing entities
567
- * with that relation type. This function is used in `filterRegularComponentTypes`
568
- * to **keep** these wildcard markers in the archetype signature while stripping
569
- * out specific-target `dontFragment` relations.
570
+ * `sparse` component.
570
571
  *
571
- * This is an optimized function that avoids the overhead of `getDetailedIdType`
572
- * by directly decoding and checking the relation's component ID against the
573
- * `dontFragment` bitset.
572
+ * Wildcard markers for sparse components are placed in the archetype component
573
+ * list so that queries can discover archetypes containing entities with that
574
+ * relation type.
574
575
  *
575
576
  * @param id - The entity/relation ID to check.
576
577
  * @returns `true` if this is a wildcard relation (`"*"` target) whose base
577
- * component was created with `dontFragment: true`.
578
+ * component was created with `sparse: true` (or legacy `dontFragment: true`).
578
579
  *
579
- * @see {@link isDontFragmentRelation} for the specific-target variant.
580
- * @see {@link ComponentOptions.dontFragment} for the full explanation.
580
+ * @see {@link isSparseRelation} for the specific-target variant.
581
+ * @see {@link ComponentOptions.sparse} for the full explanation.
581
582
  */
582
- export function isDontFragmentWildcard(id: EntityId<any>): boolean {
583
- return checkRelationFlag(id, dontFragmentFlags, (targetId) => targetId === WILDCARD_TARGET_ID);
583
+ export function isSparseWildcard(id: EntityId<any>): boolean {
584
+ return checkRelationFlag(id, sparseFlags, (targetId) => targetId === WILDCARD_TARGET_ID);
584
585
  }
585
586
 
586
587
  /**
@@ -54,10 +54,13 @@ export {
54
54
  getComponentOptions,
55
55
  isCascadeDeleteComponent,
56
56
  isCascadeDeleteRelation,
57
- isDontFragmentComponent,
58
- isDontFragmentRelation,
59
- isDontFragmentWildcard,
57
+ isSparseComponent as isDontFragmentComponent,
58
+ isSparseRelation as isDontFragmentRelation,
59
+ isSparseWildcard as isDontFragmentWildcard,
60
60
  isExclusiveComponent,
61
61
  isExclusiveRelation,
62
62
  isExclusiveWildcard,
63
+ isSparseComponent,
64
+ isSparseRelation,
65
+ isSparseWildcard,
63
66
  } from "../component/registry";
package/src/index.ts CHANGED
@@ -32,6 +32,8 @@ export type {
32
32
  } from "./storage/serialization";
33
33
  export { EntityBuilder } from "./world/builder";
34
34
  export type { ComponentDef } from "./world/builder";
35
+ export { SingletonHandle } from "./world/singleton";
36
+ export type { SingletonHandleOps } from "./world/singleton";
35
37
  export { World } from "./world/world";
36
38
 
37
39
  // Query class
@@ -39,3 +41,16 @@ export { Query } from "./query/query";
39
41
 
40
42
  // Type utilities
41
43
  export type { ComponentTuple, ComponentType, LifecycleCallback, LifecycleHook } from "./types";
44
+
45
+ // Debug / observability types
46
+ export type { DebugStatsCollector, SyncDebugStats } from "./types";
47
+
48
+ // Sparse / dontFragment flag checks (preferred + legacy aliases for BC)
49
+ export {
50
+ isSparseComponent as isDontFragmentComponent,
51
+ isSparseRelation as isDontFragmentRelation,
52
+ isSparseWildcard as isDontFragmentWildcard,
53
+ isSparseComponent,
54
+ isSparseRelation,
55
+ isSparseWildcard,
56
+ } from "./component/registry";
@@ -1,12 +1,6 @@
1
1
  import type { Archetype } from "../archetype/archetype";
2
2
  import type { EntityId } from "../entity";
3
- import {
4
- getComponentIdFromRelationId,
5
- getDetailedIdType,
6
- isDontFragmentComponent,
7
- isRelationId,
8
- relation,
9
- } from "../entity";
3
+ import { getComponentIdFromRelationId, getDetailedIdType, isRelationId, isSparseComponent, relation } from "../entity";
10
4
 
11
5
  /**
12
6
  * Filter options for queries
@@ -41,13 +35,13 @@ export function matchesComponentTypes(archetype: Archetype, componentTypes: Enti
41
35
  } else if (
42
36
  (detailedType.type === "entity-relation" || detailedType.type === "component-relation") &&
43
37
  detailedType.componentId !== undefined &&
44
- isDontFragmentComponent(detailedType.componentId)
38
+ isSparseComponent(detailedType.componentId)
45
39
  ) {
46
- // For specific dontFragment relations, check if archetype has the wildcard marker
40
+ // For specific sparse relations, check if archetype has the wildcard marker
47
41
  const wildcardMarker = relation(detailedType.componentId, "*");
48
42
  return archetype.componentTypeSet.has(wildcardMarker);
49
43
  } else {
50
- // For regular components and non-dontFragment relations, check direct inclusion
44
+ // For regular components and non-sparse relations, check direct inclusion
51
45
  return archetype.componentTypeSet.has(type);
52
46
  }
53
47
  });
@@ -1,7 +1,7 @@
1
1
  import type { Archetype } from "../archetype/archetype";
2
2
  import { normalizeComponentTypes } from "../component/type-utils";
3
3
  import type { EntityId, WildcardRelationId } from "../entity";
4
- import { getDetailedIdType, isDontFragmentComponent } from "../entity";
4
+ import { getDetailedIdType, isSparseComponent } from "../entity";
5
5
  import type { ComponentTuple, ComponentType } from "../types";
6
6
  import type { World } from "../world/world";
7
7
  import { matchesComponentTypes, matchesFilter, type QueryFilter } from "./filter";
@@ -33,8 +33,8 @@ export class Query {
33
33
  _cacheKey: string | undefined;
34
34
  /** Cached wildcard component types for faster entity filtering */
35
35
  private wildcardTypes: WildcardRelationId<any>[];
36
- /** Cached specific dontFragment relation types that need entity-level filtering */
37
- private specificDontFragmentTypes: EntityId<any>[];
36
+ /** Cached specific sparse relation types that need entity-level filtering */
37
+ private specificSparseRelationTypes: EntityId<any>[];
38
38
 
39
39
  /**
40
40
  * @internal Queries should be created via {@link World.createQuery}, not instantiated directly.
@@ -47,13 +47,13 @@ export class Query {
47
47
  this.wildcardTypes = this.componentTypes.filter(
48
48
  (ct) => getDetailedIdType(ct).type === "wildcard-relation",
49
49
  ) as WildcardRelationId<any>[];
50
- // Pre-compute specific dontFragment relation types that need entity-level filtering
51
- this.specificDontFragmentTypes = this.componentTypes.filter((ct) => {
50
+ // Pre-compute specific sparse relation types that need entity-level filtering
51
+ this.specificSparseRelationTypes = this.componentTypes.filter((ct) => {
52
52
  const detailedType = getDetailedIdType(ct);
53
53
  return (
54
54
  (detailedType.type === "entity-relation" || detailedType.type === "component-relation") &&
55
55
  detailedType.componentId !== undefined &&
56
- isDontFragmentComponent(detailedType.componentId)
56
+ isSparseComponent(detailedType.componentId)
57
57
  );
58
58
  });
59
59
  this.updateCache();
@@ -86,8 +86,8 @@ export class Query {
86
86
  getEntities(): EntityId[] {
87
87
  this.ensureNotDisposed();
88
88
 
89
- // Fast path: no wildcard relations and no specific dontFragment relations
90
- if (this.wildcardTypes.length === 0 && this.specificDontFragmentTypes.length === 0) {
89
+ // Fast path: no wildcard relations and no specific sparse relations
90
+ if (this.wildcardTypes.length === 0 && this.specificSparseRelationTypes.length === 0) {
91
91
  const result: EntityId[] = [];
92
92
  for (const archetype of this.cachedArchetypes) {
93
93
  for (const entity of archetype.getEntities()) {
@@ -100,7 +100,7 @@ export class Query {
100
100
  // Slow path: need to filter entities that actually have the required relations
101
101
  // This is necessary for:
102
102
  // 1. Wildcard relations where an archetype can contain entities with/without the relation
103
- // 2. Specific dontFragment relations where the archetype only has the wildcard marker
103
+ // 2. Specific sparse relations where the archetype only has the wildcard marker
104
104
  const result: EntityId[] = [];
105
105
  for (const archetype of this.cachedArchetypes) {
106
106
  for (const entity of archetype.getEntities()) {
@@ -113,7 +113,7 @@ export class Query {
113
113
  }
114
114
 
115
115
  /**
116
- * Check if entity matches all query requirements (wildcards and specific dontFragment relations)
116
+ * Check if entity matches all query requirements (wildcards and specific sparse relations)
117
117
  */
118
118
  private entityMatchesQuery(archetype: Archetype, entity: EntityId): boolean {
119
119
  // Check wildcard relations
@@ -124,8 +124,8 @@ export class Query {
124
124
  }
125
125
  }
126
126
 
127
- // Check specific dontFragment relations
128
- for (const specificType of this.specificDontFragmentTypes) {
127
+ // Check specific sparse relations
128
+ for (const specificType of this.specificSparseRelationTypes) {
129
129
  const result = archetype.getOptional(entity, specificType);
130
130
  if (result === undefined) {
131
131
  return false;
@@ -33,9 +33,9 @@ export type SerializedComponent = {
33
33
  };
34
34
 
35
35
  /**
36
- * Encode an internal EntityId into a SerializedEntityId for snapshots
36
+ * Core encoding logic (no cache). Extracted so cached wrapper can reuse it without duplication.
37
37
  */
38
- export function encodeEntityId(id: EntityId<any>): SerializedEntityId {
38
+ function encodeEntityIdCore(id: EntityId<any>): SerializedEntityId {
39
39
  const detailed = getDetailedIdType(id);
40
40
  switch (detailed.type) {
41
41
  case "component": {
@@ -81,6 +81,33 @@ export function encodeEntityId(id: EntityId<any>): SerializedEntityId {
81
81
  }
82
82
  }
83
83
 
84
+ /**
85
+ * Encode an internal EntityId into a SerializedEntityId for snapshots.
86
+ * Use encodeEntityIdCached when serializing many entities to benefit from memoization
87
+ * of repeated component/relation type IDs.
88
+ */
89
+ export function encodeEntityId(id: EntityId<any>): SerializedEntityId {
90
+ return encodeEntityIdCore(id);
91
+ }
92
+
93
+ /**
94
+ * Encode an EntityId, using an optional cache Map to avoid repeated getDetailedIdType
95
+ * + name lookup work for IDs that appear many times (typical during full world snapshot).
96
+ */
97
+ export function encodeEntityIdCached(
98
+ id: EntityId<any>,
99
+ cache?: Map<EntityId<any>, SerializedEntityId>,
100
+ ): SerializedEntityId {
101
+ if (cache) {
102
+ const cached = cache.get(id);
103
+ if (cached !== undefined) return cached;
104
+ const result = encodeEntityIdCore(id);
105
+ cache.set(id, result);
106
+ return result;
107
+ }
108
+ return encodeEntityIdCore(id);
109
+ }
110
+
84
111
  /**
85
112
  * Decode a SerializedEntityId back into an internal EntityId
86
113
  */
@@ -97,3 +97,74 @@ export interface LifecycleHookEntry {
97
97
  /** Archetypes that match this hook, used for precise cleanup on unsubscription */
98
98
  matchedArchetypes?: Set<any>;
99
99
  }
100
+
101
+ /**
102
+ * Statistics payload delivered to callbacks registered via `World.createDebugStatsCollector`.
103
+ *
104
+ * All structural counts are snapshots taken after the sync that triggered delivery.
105
+ * `activity` always reflects work performed during that specific sync.
106
+ *
107
+ * Timestamps are raw `performance.now()` values suitable for `performance.measure`.
108
+ */
109
+ export interface SyncDebugStats {
110
+ readonly timestamps: {
111
+ readonly syncStart: number;
112
+ readonly syncEnd: number;
113
+ readonly commandBufferStart: number;
114
+ readonly commandBufferEnd: number;
115
+ };
116
+
117
+ /** Number of iterations the internal command buffer loop performed during this sync. */
118
+ readonly commandIterations: number;
119
+
120
+ readonly entities: {
121
+ readonly total: number;
122
+ readonly freelistSize: number;
123
+ readonly nextId: number;
124
+ };
125
+
126
+ readonly archetypes: {
127
+ readonly total: number;
128
+ readonly empty: number;
129
+ };
130
+
131
+ readonly queries: {
132
+ readonly cached: number;
133
+ readonly registered: number;
134
+ };
135
+
136
+ readonly hooks: {
137
+ readonly total: number;
138
+ };
139
+
140
+ /** Sizes of stable internal reverse indices (conservative set). */
141
+ readonly indices: {
142
+ readonly entityReferences: number;
143
+ readonly entityToReferencingArchetypes: number;
144
+ readonly archetypesByComponent: number;
145
+ };
146
+
147
+ /**
148
+ * Activity that occurred as a direct result of this sync.
149
+ * All fields are always present (never optional).
150
+ */
151
+ readonly activity: {
152
+ /** Number of entities that performed an archetype migration (hasArchetypeStructuralChange was true). */
153
+ readonly migrations: number;
154
+ /** Total number of individual hook callback invocations (invokeHook calls). */
155
+ readonly hooksExecuted: number;
156
+ /** Number of new archetypes created during this sync. */
157
+ readonly archetypesCreated: number;
158
+ /** Number of archetypes removed during this sync. */
159
+ readonly archetypesRemoved: number;
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Handle returned by `World.createDebugStatsCollector`.
165
+ * The object itself carries no data — its only responsibility is lifetime management.
166
+ * Use with `using` or call `[Symbol.dispose]()` when you no longer need collection.
167
+ */
168
+ export interface DebugStatsCollector {
169
+ [Symbol.dispose](): void;
170
+ }