@codehz/ecs 0.6.10 → 0.7.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.
package/builder.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/core/entity-types.d.ts
1
+ //#region src/entity/types.d.ts
2
2
  /**
3
3
  * Unique symbol brand for associating component type information with EntityId
4
4
  */
@@ -14,14 +14,53 @@ declare const __entityIdTypeTag: unique symbol;
14
14
  * - Entity IDs: 1024+
15
15
  * - Relation IDs: negative numbers encoding component and entity associations
16
16
  */
17
+ /**
18
+ * Branded numeric type representing an ECS identifier.
19
+ *
20
+ * - {@link ComponentId}: positive values in range `1–1023`
21
+ * - Entity IDs: values `1024+`
22
+ * - {@link RelationId}: negative values encoding `(componentId, targetId)`
23
+ *
24
+ * @template T - The data type associated with this ID
25
+ * @template U - Discriminant for the ID kind (e.g. `"component"`, `"entity-relation"`)
26
+ */
17
27
  type EntityId<T = unknown, U$1 = unknown> = number & {
18
28
  readonly [__componentTypeMarker]: T;
19
29
  readonly [__entityIdTypeTag]: U$1;
20
30
  };
31
+ /**
32
+ * Component identifier. Valid values are `1` through `1023`.
33
+ * Created with {@link component}.
34
+ *
35
+ * @template T - The data type stored by this component (`void` for tag components)
36
+ */
21
37
  type ComponentId<T = void> = EntityId<T, "component">;
38
+ /**
39
+ * Relation identifier targeting an entity.
40
+ * Created with {@link relation}.
41
+ *
42
+ * @template T - The data type stored by this relation
43
+ */
22
44
  type EntityRelationId<T = void> = EntityId<T, "entity-relation">;
45
+ /**
46
+ * Relation identifier targeting another component (singleton relation).
47
+ * Created with {@link relation}.
48
+ *
49
+ * @template T - The data type stored by this relation
50
+ */
23
51
  type ComponentRelationId<T = void> = EntityId<T, "component-relation">;
52
+ /**
53
+ * Wildcard relation identifier used to query all targets of a given relation component.
54
+ * Created with `relation(componentId, "*")`.
55
+ *
56
+ * @template T - The data type stored by the relation
57
+ */
24
58
  type WildcardRelationId<T = void> = EntityId<T, "wildcard-relation">;
59
+ /**
60
+ * Union of all relation identifier kinds.
61
+ *
62
+ * @template T - The data type stored by the relation
63
+ */
25
64
  type RelationId<T = void> = EntityRelationId<T> | ComponentRelationId<T> | WildcardRelationId<T>;
26
65
  /**
27
66
  * Check if an ID is a component ID
@@ -36,20 +75,42 @@ declare function isEntityId<T>(id: EntityId<T>): id is EntityId<T>;
36
75
  */
37
76
  declare function isRelationId<T>(id: EntityId<T>): id is RelationId<T>;
38
77
  //#endregion
39
- //#region src/core/entity-relation.d.ts
78
+ //#region src/entity/relation.d.ts
40
79
  /**
41
80
  * Type for relation ID based on component and target types
42
81
  */
43
82
  type RelationIdType<T, R> = R extends ComponentId<infer U> ? U extends void ? ComponentRelationId<T> : ComponentRelationId<T extends void ? U : T> : R extends EntityId<any> ? EntityRelationId<T> : never;
44
83
  /**
45
- * Create a relation ID by associating a component with another ID (entity or component)
46
- * @param componentId The component ID (0-1023)
47
- * @param targetId The target ID (entity, component, or '*' for wildcard)
84
+ * Create a relation ID by associating a component with a target entity, component, or wildcard.
85
+ *
86
+ * Relations are encoded as negative numbers and can be used anywhere a regular component ID is accepted.
87
+ * Use `"*"` as the target to create a wildcard relation for querying all targets of a given relation type.
88
+ *
89
+ * @param componentId - The base component ID (must be a valid component)
90
+ * @param targetId - The target entity ID, component ID, or `"*"` for wildcard
91
+ * @returns A relation ID that encodes both the component and target
92
+ *
93
+ * @throws {Error} If `componentId` is not a valid component ID
94
+ * @throws {Error} If `targetId` is not a valid entity, component, or `"*"`
95
+ *
96
+ * @example
97
+ * const ChildOf = component();
98
+ * const parent = world.new();
99
+ *
100
+ * // Entity relation
101
+ * const childRelation = relation(ChildOf, parent);
102
+ * world.set(child, childRelation);
103
+ *
104
+ * // Wildcard relation (queries all targets)
105
+ * const allChildren = world.createQuery([relation(ChildOf, "*")]);
48
106
  */
49
107
  declare function relation<T>(componentId: ComponentId<T>, targetId: "*"): WildcardRelationId<T>;
50
108
  declare function relation<T, R extends EntityId<any>>(componentId: ComponentId<T>, targetId: R): RelationIdType<T, R>;
51
109
  /**
52
- * Check if an ID is a wildcard relation id
110
+ * Check if an ID is a wildcard relation (created with `relation(componentId, "*")`).
111
+ *
112
+ * @param id - The ID to check
113
+ * @returns `true` if the ID is a wildcard relation, `false` otherwise
53
114
  */
54
115
  declare function isWildcardRelationId<T>(id: EntityId<T>): id is WildcardRelationId<T>;
55
116
  /**
@@ -63,39 +124,306 @@ declare function decodeRelationId(relationId: RelationId<any>): {
63
124
  type: "entity" | "component" | "wildcard";
64
125
  };
65
126
  //#endregion
66
- //#region src/core/component-registry.d.ts
127
+ //#region src/component/registry.d.ts
128
+ /**
129
+ * Merge function type for combining repeated `set()` values within a single sync batch.
130
+ *
131
+ * When `world.set(entity, componentType, value)` is called **multiple times** for the
132
+ * same entity and same component type **before** the next `world.sync()`, the merge
133
+ * callback is invoked to combine the values instead of simply overwriting. This allows
134
+ * additive or custom composition of component data in a single frame.
135
+ *
136
+ * @typeParam T - The component's value type.
137
+ *
138
+ * @param prev - The value from the **previous** `set()` call (or the merged result of
139
+ * earlier calls) for this entity/componentType pair within the current sync batch.
140
+ * @param next - The value from the **current** `set()` call being processed.
141
+ *
142
+ * @returns The merged value to be stored. This becomes `prev` if another `set()` for
143
+ * the same entity and componentType is encountered later in the same batch.
144
+ *
145
+ * @remarks
146
+ * **Idempotency**: Merge functions **must be idempotent**. The ECS does not guarantee
147
+ * that `world.sync()` won't be called multiple times in edge cases (e.g., intermediate
148
+ * syncs during pipeline execution), so the merge result should not depend on call
149
+ * count or non-deterministic state.
150
+ *
151
+ * **Single-batch scope**: Merging only applies to `set()` calls within the **same sync
152
+ * batch** (i.e., between two `world.sync()` calls). After `world.sync()`, the component
153
+ * value is committed to storage, and the next `set()` starts with a fresh `prev` value.
154
+ *
155
+ * @example
156
+ * ```ts
157
+ * // Accumulate damage events in a single frame
158
+ * const DamageEvents = component<DamageEvent[]>({
159
+ * merge: (prev, next) => [...prev, ...next],
160
+ * });
161
+ *
162
+ * world.set(player, DamageEvents, [{ source: "fire", amount: 10 }]);
163
+ * world.set(player, DamageEvents, [{ source: "ice", amount: 5 }]);
164
+ * // After sync: player has [{ source: "fire", amount: 10 }, { source: "ice", amount: 5 }]
165
+ * ```
166
+ */
67
167
  type ComponentMerge<T = any> = (prev: T, next: T) => T;
68
168
  /**
69
169
  * Component options that define intrinsic properties
70
170
  */
71
171
  interface ComponentOptions<T = any> {
72
172
  /**
73
- * Optional name for the component (for serialization/debugging)
173
+ * An optional human-readable name for the component, used for debugging and
174
+ * serialization.
175
+ *
176
+ * While `name` is **optional** at registration time, omitting it can cause
177
+ * problems when serializing and later deserializing the world:
178
+ *
179
+ * 1. **Cross-session portability**: Without a name, the component is
180
+ * serialized as a raw numeric ID. Component IDs are allocated sequentially
181
+ * at registration time, so if the order of `component()` calls changes
182
+ * between sessions (e.g. due to code refactoring, lazy-loading, or
183
+ * tree-shaking), those numeric IDs will no longer point to the same
184
+ * component type, leading to **silent data corruption** on restore.
185
+ *
186
+ * 2. **Runtime warnings**: `encodeEntityId` logs a `console.warn` for every
187
+ * unnamed component it encounters during `world.serialize()`, which can be
188
+ * noisy in production when serialization is used for save-games or
189
+ * snapshots.
190
+ *
191
+ * 3. **Debugging ergonomics**: Named components make serialized snapshots
192
+ * human-readable (e.g. `"Position"` instead of `42`), which is invaluable
193
+ * when inspecting save files or network dumps.
194
+ *
195
+ * **Recommendation**: Always provide a `name` for any component that may
196
+ * appear in a serialized world — even if it's just the same string as the
197
+ * variable name.
198
+ *
199
+ * @example
200
+ * ```ts
201
+ * // ✅ Good: explicit name ensures stable serialization
202
+ * const Position = component<{ x: number; y: number }>({ name: "Position" });
203
+ *
204
+ * // ⚠️ Risky: no name — serialization falls back to numeric ID
205
+ * const Velocity = component<{ dx: number; dy: number }>();
206
+ * ```
74
207
  */
75
208
  name?: string;
76
209
  /**
77
- * If true, an entity can have at most one relation per base component.
78
- * When adding a new relation with the same base component, any existing relations
79
- * with that base component are automatically removed.
80
- * Only applicable to relation components.
210
+ * If `true`, an entity can have **at most one** relation per base component type.
211
+ * When a new relation with the same base component is added, any existing relations
212
+ * with that base component are **automatically removed** before the new one is applied.
213
+ *
214
+ * **Only applicable to relation components** — components used via
215
+ * `relation(componentId, target)`. Regular (non-relation) components ignore this flag.
216
+ *
217
+ * ## Behavior
218
+ *
219
+ * Exclusive relations enforce a **one-to-one** constraint at the entity level:
220
+ * each entity can hold at most one relation of a given exclusive component type.
221
+ *
222
+ * - **Same base component, different targets**: `set(entity, relation(Comp, A))`
223
+ * followed by `set(entity, relation(Comp, B))` results in only `(Comp, B)` —
224
+ * the `(Comp, A)` relation is automatically removed.
225
+ * - **Same base component, same target**: Re-setting the same relation target
226
+ * simply updates the component value (no extra removal overhead).
227
+ * - **Different exclusive components**: Independent — `exclusive` on `CompA` does
228
+ * not affect relations using `CompB`.
229
+ *
230
+ * The removal happens **during `world.sync()`**, as part of the command buffer
231
+ * processing, so it respects the same deferred execution model as other structural
232
+ * changes.
233
+ *
234
+ * ## Use cases
235
+ *
236
+ * - **Ownership**: An entity can only be owned by one parent at a time
237
+ * (`ChildOf` with `exclusive: true`).
238
+ * - **Equipment slots**: An item can only be in one slot at a time
239
+ * (`EquippedBy` with `exclusive: true`).
240
+ * - **Targeting**: An AI agent can only track one target at a time
241
+ * (`Targeting` with `exclusive: true`).
242
+ * - **State machines**: An entity can only have one active state from a set
243
+ * (`ActiveState` with `exclusive: true`).
244
+ *
245
+ * ## Interaction with other options
246
+ *
247
+ * - **`cascadeDelete`**: Compatible. When an exclusive relation uses
248
+ * `cascadeDelete`, deleting the target entity will both (a) delete the
249
+ * referencing entity, and (b) the exclusivity constraint prevents the
250
+ * entity from having multiple cascade-delete relations of the same type.
251
+ * - **`dontFragment`**: Compatible. Exclusivity is enforced at the data level
252
+ * regardless of whether the archetype is fragmented.
253
+ *
254
+ * @example
255
+ * ```ts
256
+ * // Without exclusive: entity can have multiple ChildOf relations
257
+ * const ChildOf = component();
258
+ * world.set(child, relation(ChildOf, parentA));
259
+ * world.set(child, relation(ChildOf, parentB));
260
+ * world.sync();
261
+ * // child now has TWO ChildOf relations (parentA and parentB)
262
+ * ```
263
+ *
264
+ * @example
265
+ * ```ts
266
+ * // With exclusive: only the last relation survives
267
+ * const ChildOf = component({ exclusive: true });
268
+ * world.set(child, relation(ChildOf, parentA));
269
+ * world.set(child, relation(ChildOf, parentB));
270
+ * world.sync();
271
+ * // child has only (ChildOf, parentB); (ChildOf, parentA) was auto-removed
272
+ * ```
81
273
  */
82
274
  exclusive?: boolean;
83
275
  /**
84
276
  * If true, when a relation target entity is deleted, all entities that reference
85
277
  * it through this component will also be deleted (cascade delete).
278
+ *
86
279
  * Only applicable to entity-relation components.
280
+ *
281
+ * **Important distinction from default cleanup**:
282
+ * By default, the ECS library **always** cleans up relation components that point
283
+ * to a deleted entity — the relation component is removed from the referencing
284
+ * entity, but the referencing entity itself **survives**. When `cascadeDelete` is
285
+ * enabled, the **entire referencing entity** is deleted, not just the relation
286
+ * component. This deletion is transitive: if entity C references entity B (which
287
+ * is cascade-deleted), entity C will also be deleted, and so on.
288
+ *
289
+ * @example
290
+ * // Without cascadeDelete (default behavior):
291
+ * const ChildOf = component(); // no cascadeDelete
292
+ * world.set(child, relation(ChildOf, parent));
293
+ * world.sync();
294
+ * world.delete(parent);
295
+ * world.sync();
296
+ * // child still exists, but the ChildOf relation is cleaned up
297
+ *
298
+ * @example
299
+ * // With cascadeDelete:
300
+ * const ChildOf = component({ cascadeDelete: true });
301
+ * world.set(child, relation(ChildOf, parent));
302
+ * world.sync();
303
+ * world.delete(parent);
304
+ * world.sync();
305
+ * // child is also deleted (entity deleted, not just relation cleaned up)
87
306
  */
88
307
  cascadeDelete?: boolean;
89
308
  /**
90
309
  * If true, relations with this component will not cause archetype fragmentation.
91
- * Entities with different target entities for this relation component will be stored
92
- * in the same archetype, preventing fragmentation when there are many different targets.
93
- * Only applicable to relation components.
94
- * Inspired by Flecs' DontFragment trait.
310
+ *
311
+ * **Problem it solves**: By default, each unique relation pair `(component, target)`
312
+ * creates a **separate archetype**. If 100 entities each have a `ChildOf` relation
313
+ * to a different parent, you get 100 archetypes — this is **archetype fragmentation**.
314
+ * Queries that iterate over all entities with a `ChildOf` relation must check all
315
+ * 100 archetypes, which degrades iteration performance and increases memory overhead.
316
+ *
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.
323
+ *
324
+ * **Use cases**:
325
+ * - **Hierarchy/ownership**: `ChildOf` relations where thousands of entities each
326
+ * point to different parent entities.
327
+ * - **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.
330
+ * - **High-cardinality relations**: Any relation where the number of unique targets
331
+ * is large compared to the number of entities.
332
+ *
333
+ * **Performance implications**:
334
+ * - **Without `dontFragment`**: Archetype count grows linearly with unique targets.
335
+ * Each archetype migration (changing a relation target) requires moving the entity's
336
+ * 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`.
339
+ * The trade-off is an extra map lookup when accessing the relation data.
340
+ *
341
+ * **Constraints**:
342
+ * - Only applicable to **relation components** (components used with `relation()`).
343
+ * - Wildcard queries (e.g., `relation(Comp, "*")`) still work correctly — the
344
+ * archetype carries a wildcard marker so queries can discover it.
345
+ * - Works with `exclusive` and `cascadeDelete` simultaneously.
346
+ *
347
+ * @example
348
+ * ```ts
349
+ * // Without dontFragment: 100 entities with different parents = 100 archetypes
350
+ * const ChildOf = component(); // default: fragmentation happens
351
+ *
352
+ * // With dontFragment: 100 entities with different parents = 1 archetype
353
+ * const ChildOf = component({ dontFragment: true });
354
+ *
355
+ * for (let i = 0; i < 100; i++) {
356
+ * const parent = world.new();
357
+ * const child = world.new();
358
+ * world.set(child, Position);
359
+ * world.set(child, relation(ChildOf, parent));
360
+ * }
361
+ * world.sync();
362
+ * // dontFragment: 1 archetype for all 100 entities
363
+ * // without: 100 archetypes, one per unique parent
364
+ * ```
365
+ *
366
+ * Inspired by Flecs' `DontFragment` trait.
95
367
  */
96
368
  dontFragment?: boolean;
97
369
  /**
98
- * Custom merge behavior for repeated set() of the same componentType in a single sync batch.
370
+ * Custom merge behavior for repeated `set()` of the same component type on the
371
+ * same entity within a single sync batch.
372
+ *
373
+ * By default, calling `world.set(entity, comp, value)` multiple times for the same
374
+ * entity and component before `world.sync()` simply overwrites the previous value —
375
+ * the last `set()` wins. When `merge` is provided, the values are combined using
376
+ * your function instead.
377
+ *
378
+ * @remarks
379
+ * **Use cases**:
380
+ * - **Accumulation**: Collecting events, tags, or modifiers that multiple systems
381
+ * contribute to within the same frame.
382
+ * - **Composition**: Merging partial updates into a single component value (e.g.,
383
+ * applying multiple `Vec3` deltas to a position).
384
+ * - **Conflict resolution**: Choosing the max/min/latest value when multiple
385
+ * systems want to set the same component.
386
+ *
387
+ * **Scope**: This only affects `set()` calls on the **same entity** with the **same
388
+ * component type** within **one sync batch** (i.e., between `world.sync()` calls).
389
+ * It does NOT merge values across different entities or across sync boundaries.
390
+ *
391
+ * **Relation support**: If the component is used as a relation (via
392
+ * `relation(componentId, target)`), the merge function also applies per-target.
393
+ * `set(entity, relation(Comp, A), v1)` and `set(entity, relation(Comp, A), v2)`
394
+ * will be merged, but `set(entity, relation(Comp, B), v)` is independent.
395
+ *
396
+ * **Idempotency required**: Your merge function should be idempotent — calling it
397
+ * multiple times with the same inputs must produce the same result. The ECS
398
+ * runtime does not guarantee exactly-once `sync()` execution in all scenarios.
399
+ *
400
+ * **Return value**: The function **must return** the merged value. It should not
401
+ * mutate `prev` or `next` in place unless you intentionally want shared mutable
402
+ * state (which is discouraged).
403
+ *
404
+ * @example
405
+ * ```ts
406
+ * // Collect tags from multiple systems in one frame
407
+ * const Tags = component<string[]>({
408
+ * merge: (prev, next) => [...prev, ...next],
409
+ * });
410
+ * ```
411
+ *
412
+ * @example
413
+ * ```ts
414
+ * // Only keep the highest priority value
415
+ * const Alert = component<{ level: number; msg: string }>({
416
+ * merge: (prev, next) => prev.level >= next.level ? prev : next,
417
+ * });
418
+ * ```
419
+ *
420
+ * @example
421
+ * ```ts
422
+ * // Accumulate numeric deltas (e.g., for movement)
423
+ * const Velocity = component<{ x: number; y: number }>({
424
+ * merge: (prev, next) => ({ x: prev.x + next.x, y: prev.y + next.y }),
425
+ * });
426
+ * ```
99
427
  */
100
428
  merge?: ComponentMerge<T>;
101
429
  }
@@ -126,98 +454,86 @@ declare function getComponentIdByName(name: string): ComponentId<any> | undefine
126
454
  */
127
455
  declare function getComponentNameById(id: ComponentId<any>): string | undefined;
128
456
  //#endregion
129
- //#region src/commands/changeset.d.ts
457
+ //#region src/storage/serialization.d.ts
458
+ type SerializedEntityId = number | string | {
459
+ component: string;
460
+ target: number | string | "*";
461
+ };
130
462
  /**
131
- * @internal Represents a set of component changes to be applied to an entity
463
+ * Serialized state of EntityIdManager
132
464
  */
133
- declare class ComponentChangeset {
134
- readonly adds: Map<EntityId<any>, any>;
135
- readonly removes: Set<EntityId<any>>;
136
- /**
137
- * Add a component to the changeset
138
- */
139
- set<T>(componentType: EntityId<T>, component: T): void;
140
- /**
141
- * Remove a component from the changeset
142
- */
143
- delete<T>(componentType: EntityId<T>): void;
144
- /**
145
- * Check if the changeset has any changes
146
- */
147
- hasChanges(): boolean;
148
- /**
149
- * Clear all changes
150
- */
151
- clear(): void;
152
- /**
153
- * Merge another changeset into this one
154
- */
155
- merge(other: ComponentChangeset): void;
156
- /**
157
- * Apply the changeset to existing components and return the final state
158
- */
159
- applyTo(existingComponents: Map<EntityId<any>, any>): Map<EntityId<any>, any>;
160
- /**
161
- * Get the final component types after applying the changeset
162
- * @param existingComponentTypes - The current component types on the entity
163
- * @returns The final component types or undefined if no changes
164
- */
165
- getFinalComponentTypes(existingComponentTypes: EntityId<any>[]): EntityId<any>[] | undefined;
465
+ interface SerializedEntityIdManager {
466
+ nextId: number;
467
+ freelist?: number[];
166
468
  }
469
+ type SerializedWorld = {
470
+ version: number;
471
+ entityManager: SerializedEntityIdManager;
472
+ entities: SerializedEntity[];
473
+ componentEntities?: SerializedEntity[];
474
+ };
475
+ type SerializedEntity = {
476
+ id: SerializedEntityId;
477
+ components: SerializedComponent[];
478
+ };
479
+ type SerializedComponent = {
480
+ type: SerializedEntityId;
481
+ value: any;
482
+ };
167
483
  //#endregion
168
- //#region src/commands/command-buffer.d.ts
484
+ //#region src/query/filter.d.ts
169
485
  /**
170
- * Command for deferred execution
171
- * Uses discriminated union for type safety
486
+ * Filter options for queries
172
487
  */
173
- type Command = {
174
- type: "set";
175
- entityId: EntityId;
176
- componentType: EntityId<any>;
177
- component: any;
178
- } | {
179
- type: "delete";
180
- entityId: EntityId;
181
- componentType: EntityId<any>;
182
- } | {
183
- type: "destroy";
184
- entityId: EntityId;
185
- };
488
+ interface QueryFilter {
489
+ negativeComponentTypes?: EntityId<any>[];
490
+ }
186
491
  //#endregion
187
- //#region src/core/types.d.ts
492
+ //#region src/types/index.d.ts
188
493
  /**
189
- * Hook types for component lifecycle events
494
+ * Lifecycle hook definition for reacting to component additions, updates, and removals.
495
+ * Register hooks with {@link World.hook}.
190
496
  */
191
- interface LegacyLifecycleHook<T = unknown> {
497
+ interface LifecycleHook<T extends readonly ComponentType<any>[]> {
192
498
  /**
193
- * Called when a component is added to an entity
499
+ * Called once for each entity that already matches the hook's component types
500
+ * when the hook is first registered, and then for every new matching entity.
194
501
  */
195
- on_init?: (entityId: EntityId, componentType: EntityId<T>, component: T) => void;
502
+ on_init?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
196
503
  /**
197
- * Called when a component is updated on an entity
504
+ * Called whenever a matching entity's component data is updated via `set()`.
198
505
  */
199
- on_set?: (entityId: EntityId, componentType: EntityId<T>, component: T) => void;
506
+ on_set?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
200
507
  /**
201
- * Called when a component is deleted from an entity
508
+ * Called whenever a matching entity loses one of the required components
509
+ * or is deleted.
202
510
  */
203
- on_remove?: (entityId: EntityId, componentType: EntityId<T>, component: T) => void;
204
- }
205
- interface LifecycleHook<T extends readonly ComponentType<any>[]> {
206
- on_init?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
207
- on_set?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
208
511
  on_remove?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
209
512
  }
210
513
  /**
211
- * Convenience function type for single component lifecycle events
212
- * Combines on_init, on_set, and on_remove into a single callback
514
+ * Shorthand callback style for multi-component lifecycle hooks.
515
+ * The same function receives all three events distinguished by the `type` parameter.
516
+ *
517
+ * @example
518
+ * world.hook([Position, Velocity], (type, entityId, position, velocity) => {
519
+ * if (type === "init") console.log("spawned");
520
+ * if (type === "set") console.log("updated");
521
+ * if (type === "remove") console.log("despawned");
522
+ * });
213
523
  */
214
- type LegacyLifecycleCallback<T = unknown> = (type: "init" | "set" | "remove", entityId: EntityId, componentType: EntityId<T>, component: T) => void;
524
+ type LifecycleCallback<T extends readonly ComponentType<any>[]> = (type: "init" | "set" | "remove", entityId: EntityId, ...components: ComponentTuple<T>) => void;
215
525
  /**
216
- * Convenience function type for multi-component lifecycle events
217
- * Combines on_init, on_set, and on_remove into a single callback
526
+ * A component type used in queries and hooks.
527
+ * Can be a plain {@link EntityId} or an {@link OptionalEntityId} wrapped with `.optional`.
218
528
  */
219
- type LifecycleCallback<T extends readonly ComponentType<any>[]> = (type: "init" | "set" | "remove", entityId: EntityId, ...components: ComponentTuple<T>) => void;
220
529
  type ComponentType<T> = EntityId<T> | OptionalEntityId<T>;
530
+ /**
531
+ * Wrapper that marks a component as optional in queries and hooks.
532
+ * When a component is optional, entities missing it are still included in results.
533
+ *
534
+ * @example
535
+ * world.createQuery([Position, { optional: Velocity }]);
536
+ */
221
537
  type OptionalEntityId<T> = {
222
538
  optional: EntityId<T>;
223
539
  };
@@ -227,17 +543,36 @@ type ComponentTypeToData<T> = T extends {
227
543
  value: ComponentTypeToData<U>;
228
544
  } | undefined : T extends WildcardRelationId<infer U> ? [EntityId<unknown>, U][] : T extends EntityId<infer U> ? U : never;
229
545
  /**
230
- * Type helper for component tuples extracted from EntityId array
546
+ * Maps an array of {@link ComponentType} to their corresponding data tuples.
547
+ * Used by {@link World.query} and {@link Query.forEach} to type component results.
231
548
  */
232
549
  type ComponentTuple<T extends readonly ComponentType<any>[]> = { readonly [K in keyof T]: ComponentTypeToData<T[K]> };
233
550
  interface LifecycleHookEntry {
234
551
  componentTypes: readonly ComponentType<any>[];
235
552
  requiredComponents: EntityId<any>[];
236
553
  optionalComponents: EntityId<any>[];
554
+ filter: QueryFilter;
237
555
  hook: LifecycleHook<any>;
556
+ /** Raw callback function; takes precedence over hook.on_* when present */
557
+ callback?: LifecycleCallback<any>;
558
+ /** Archetypes that match this hook, used for precise cleanup on unsubscription */
559
+ matchedArchetypes?: Set<any>;
560
+ }
561
+ //#endregion
562
+ //#region src/archetype/store.d.ts
563
+ /**
564
+ * Minimal interface for storing dontFragment relation data keyed by entity ID.
565
+ *
566
+ * Using an interface here decouples `Archetype` (and `world-commands.ts`) from
567
+ * the concrete `Map` used by `World`, making archetypes independently testable.
568
+ */
569
+ interface DontFragmentStore {
570
+ get(entityId: EntityId): Map<EntityId<any>, any> | undefined;
571
+ set(entityId: EntityId, data: Map<EntityId<any>, any>): void;
572
+ delete(entityId: EntityId): void;
238
573
  }
239
574
  //#endregion
240
- //#region src/core/archetype.d.ts
575
+ //#region src/archetype/archetype.d.ts
241
576
  /**
242
577
  * Archetype class for ECS architecture
243
578
  * Represents a group of entities that share the same set of components
@@ -266,9 +601,9 @@ declare class Archetype {
266
601
  */
267
602
  private entityToIndex;
268
603
  /**
269
- * Reference to dontFragment relations storage from World
604
+ * DontFragmentStore for relation data keyed by entity ID.
270
605
  * This allows entities with different relation targets to share the same archetype
271
- * Stored in World to avoid migration overhead when entities change archetypes
606
+ * without migration overhead when entities change archetypes.
272
607
  */
273
608
  private dontFragmentRelations;
274
609
  /**
@@ -279,7 +614,7 @@ declare class Archetype {
279
614
  * Cache for pre-computed component data sources to avoid repeated calculations
280
615
  */
281
616
  private componentDataSourcesCache;
282
- constructor(componentTypes: EntityId<any>[], dontFragmentRelations: Map<EntityId, Map<EntityId<any>, any>>);
617
+ constructor(componentTypes: EntityId<any>[], dontFragmentRelations: DontFragmentStore);
283
618
  get size(): number;
284
619
  /**
285
620
  * Check if the given component types match this archetype
@@ -317,23 +652,88 @@ declare class Archetype {
317
652
  entity: EntityId;
318
653
  components: ComponentTuple<T>;
319
654
  }>;
655
+ appendEntitiesWithComponents<const T extends readonly ComponentType<any>[]>(componentTypes: T, result: Array<{
656
+ entity: EntityId;
657
+ components: ComponentTuple<T>;
658
+ }>): void;
320
659
  iterateWithComponents<const T extends readonly ComponentType<any>[]>(componentTypes: T): IterableIterator<[EntityId, ...ComponentTuple<T>]>;
321
660
  forEachWithComponents<const T extends readonly ComponentType<any>[]>(componentTypes: T, callback: (entity: EntityId, ...components: ComponentTuple<T>) => void): void;
322
661
  forEach(callback: (entityId: EntityId, components: Map<EntityId<any>, any>) => void): void;
323
662
  hasRelationWithComponentId(componentId: EntityId<any>): boolean;
324
663
  }
325
664
  //#endregion
326
- //#region src/query/filter.d.ts
665
+ //#region src/query/registry.d.ts
327
666
  /**
328
- * Filter options for queries
667
+ * Manages the lifecycle and caching of `Query` instances.
668
+ *
669
+ * Responsibilities:
670
+ * - Create / reuse cached queries keyed by component-type + filter signature.
671
+ * - Track reference counts so queries are only disposed when truly unused.
672
+ * - Notify registered queries when new archetypes are created or destroyed.
673
+ *
674
+ * The `_cacheKey` string that was previously attached directly to `Query` is now
675
+ * kept in a private `WeakMap` so the `Query` class doesn't need to expose it.
329
676
  */
330
- interface QueryFilter {
331
- negativeComponentTypes?: EntityId<any>[];
677
+ declare class QueryRegistry {
678
+ /** All live queries that should receive archetype notifications. */
679
+ private readonly queries;
680
+ /** Cache of reusable queries keyed by a deterministic signature string. */
681
+ private readonly cache;
682
+ /** Maps each query to its cache key without polluting the Query public API. */
683
+ private readonly cacheKeys;
684
+ /**
685
+ * Returns (or creates) a cached query for the given component types and filter.
686
+ * Increments the reference count on cache hits.
687
+ *
688
+ * @param world The world that owns this registry.
689
+ * @param sortedTypes Normalized (sorted) component types.
690
+ * @param key Combined cache key (`types|filter`).
691
+ * @param filter The raw query filter (used when creating a new Query).
692
+ */
693
+ getOrCreate(world: World, sortedTypes: EntityId<any>[], key: string, filter: QueryFilter): Query;
694
+ /**
695
+ * Decrements the reference count for the given query.
696
+ * When the count reaches zero the query is fully disposed.
697
+ */
698
+ release(query: Query): void;
699
+ /**
700
+ * Registers a query so it receives future archetype notifications.
701
+ * Called automatically by the `Query` constructor via `world._registerQuery`.
702
+ */
703
+ register(query: Query): void;
704
+ /**
705
+ * Removes a query from the notification list.
706
+ * Called by `Query._disposeInternal` via `world._unregisterQuery`.
707
+ */
708
+ unregister(query: Query): void;
709
+ /**
710
+ * Notifies all live queries that a new archetype has been created.
711
+ * Queries will add the archetype to their cache if it matches.
712
+ */
713
+ onNewArchetype(archetype: Archetype): void;
714
+ /**
715
+ * Notifies all live queries that an archetype has been destroyed.
716
+ * Queries will remove the archetype from their internal cache.
717
+ */
718
+ onArchetypeRemoved(archetype: Archetype): void;
332
719
  }
333
720
  //#endregion
334
721
  //#region src/query/query.d.ts
335
722
  /**
336
- * Query class for efficient entity queries with cached archetypes
723
+ * Cached query for efficiently iterating entities with specific components.
724
+ *
725
+ * Queries are created via {@link World.createQuery} and should be **reused across frames**
726
+ * for optimal performance. The world automatically keeps the query's internal archetype cache
727
+ * up to date as entities are created and destroyed.
728
+ *
729
+ * @example
730
+ * const movementQuery = world.createQuery([Position, Velocity]);
731
+ *
732
+ * // In the game loop
733
+ * movementQuery.forEach([Position, Velocity], (entity, pos, vel) => {
734
+ * pos.x += vel.x;
735
+ * pos.y += vel.y;
736
+ * });
337
737
  */
338
738
  declare class Query {
339
739
  private world;
@@ -347,13 +747,24 @@ declare class Query {
347
747
  private wildcardTypes;
348
748
  /** Cached specific dontFragment relation types that need entity-level filtering */
349
749
  private specificDontFragmentTypes;
350
- constructor(world: World, componentTypes: EntityId<any>[], filter?: QueryFilter);
750
+ /**
751
+ * @internal Queries should be created via {@link World.createQuery}, not instantiated directly.
752
+ */
753
+ constructor(world: World, componentTypes: EntityId<any>[], filter?: QueryFilter, registry?: QueryRegistry);
351
754
  /**
352
755
  * Check if query is disposed and throw error if so
353
756
  */
354
757
  private ensureNotDisposed;
355
758
  /**
356
- * Get all entities matching the query
759
+ * Returns all entity IDs that match this query.
760
+ *
761
+ * @returns Array of matching entity IDs
762
+ *
763
+ * @example
764
+ * const entities = query.getEntities();
765
+ * for (const entity of entities) {
766
+ * const pos = world.get(entity, Position);
767
+ * }
357
768
  */
358
769
  getEntities(): EntityId[];
359
770
  /**
@@ -361,42 +772,67 @@ declare class Query {
361
772
  */
362
773
  private entityMatchesQuery;
363
774
  /**
364
- * Get entities with their component data
365
- * @param componentTypes Array of component types to retrieve
366
- * @returns Array of objects with entity and component data
775
+ * Returns all matching entities along with their component data.
776
+ *
777
+ * @param componentTypes - Array of component types to retrieve
778
+ * @returns Array of objects containing the entity ID and its component tuple
779
+ *
780
+ * @example
781
+ * const results = query.getEntitiesWithComponents([Position, Velocity]);
782
+ * results.forEach(({ entity, components: [pos, vel] }) => {
783
+ * pos.x += vel.x;
784
+ * });
367
785
  */
368
786
  getEntitiesWithComponents<const T extends readonly ComponentType<any>[]>(componentTypes: T): Array<{
369
787
  entity: EntityId;
370
788
  components: ComponentTuple<T>;
371
789
  }>;
372
790
  /**
373
- * Iterate over entities with their component data
374
- * @param componentTypes Array of component types to retrieve
375
- * @param callback Function called for each entity with its components
791
+ * Iterates over all matching entities and invokes the callback with their component data.
792
+ * This is the preferred way to read and mutate components in a hot loop.
793
+ *
794
+ * @param componentTypes - Array of component types to retrieve
795
+ * @param callback - Function called for each matching entity with its components
796
+ *
797
+ * @example
798
+ * query.forEach([Position, Velocity], (entity, pos, vel) => {
799
+ * pos.x += vel.x;
800
+ * pos.y += vel.y;
801
+ * });
376
802
  */
377
803
  forEach<const T extends readonly ComponentType<any>[]>(componentTypes: T, callback: (entity: EntityId, ...components: ComponentTuple<T>) => void): void;
378
804
  /**
379
- * Iterate over entities with their component data (generator)
380
- * @param componentTypes Array of component types to retrieve
805
+ * Generator that yields each matching entity together with its component data.
806
+ *
807
+ * @param componentTypes - Array of component types to retrieve
808
+ * @yields Tuples of `[entityId, ...components]`
809
+ *
810
+ * @example
811
+ * for (const [entity, pos, vel] of query.iterate([Position, Velocity])) {
812
+ * pos.x += vel.x;
813
+ * }
381
814
  */
382
815
  iterate<const T extends readonly ComponentType<any>[]>(componentTypes: T): IterableIterator<[EntityId, ...ComponentTuple<T>]>;
383
816
  /**
384
- * Get component data arrays for all matching entities
385
- * @param componentType The component type to retrieve
386
- * @returns Array of component data for all matching entities
817
+ * Returns an array containing the data of a single component for every matching entity.
818
+ *
819
+ * @param componentType - The component type to retrieve
820
+ * @returns Array of component data (one entry per matching entity)
821
+ *
822
+ * @example
823
+ * const positions = query.getComponentData(Position);
387
824
  */
388
825
  getComponentData<T>(componentType: EntityId<T>): T[];
389
826
  /**
390
- * Update the cached archetypes
391
- * Called when new archetypes are created
827
+ * @internal Rebuilds the cached archetype list. Called automatically by the world.
392
828
  */
393
829
  updateCache(): void;
394
830
  /**
395
- * Check if a new archetype matches this query and add to cache if it does
831
+ * @internal Called by the world when a new archetype is created.
396
832
  */
397
833
  checkNewArchetype(archetype: Archetype): void;
398
834
  /**
399
- * Remove an archetype from the cached archetypes
835
+ * @internal Called by the world when an archetype is destroyed.
400
836
  */
401
837
  removeArchetype(archetype: Archetype): void;
402
838
  /**
@@ -406,47 +842,24 @@ declare class Query {
406
842
  */
407
843
  dispose(): void;
408
844
  /**
409
- * Internal full dispose called by World when refCount reaches zero.
845
+ * @internal Fully disposes the query when the world's refCount reaches zero.
410
846
  */
411
- _disposeInternal(): void;
847
+ _disposeInternal(registry?: QueryRegistry): void;
412
848
  /**
413
- * Symbol.dispose implementation for automatic resource management
849
+ * Using-with-disposals support. Calls {@link dispose} automatically.
850
+ *
851
+ * @example
852
+ * using query = world.createQuery([Position]);
853
+ * // query is released automatically when the block exits
414
854
  */
415
855
  [Symbol.dispose](): void;
416
856
  /**
417
- * Check if the query has been disposed
857
+ * Whether the query has been disposed and can no longer be used.
418
858
  */
419
859
  get disposed(): boolean;
420
860
  }
421
861
  //#endregion
422
- //#region src/core/serialization.d.ts
423
- type SerializedEntityId = number | string | {
424
- component: string;
425
- target: number | string | "*";
426
- };
427
- /**
428
- * Serialized state of EntityIdManager
429
- */
430
- interface SerializedEntityIdManager {
431
- nextId: number;
432
- freelist?: number[];
433
- }
434
- type SerializedWorld = {
435
- version: number;
436
- entityManager: SerializedEntityIdManager;
437
- entities: SerializedEntity[];
438
- componentEntities?: SerializedEntity[];
439
- };
440
- type SerializedEntity = {
441
- id: SerializedEntityId;
442
- components: SerializedComponent[];
443
- };
444
- type SerializedComponent = {
445
- type: SerializedEntityId;
446
- value: any;
447
- };
448
- //#endregion
449
- //#region src/core/world.d.ts
862
+ //#region src/world/world.d.ts
450
863
  /**
451
864
  * World class for ECS architecture
452
865
  * Manages entities and components
@@ -458,21 +871,22 @@ declare class World {
458
871
  private entityToArchetype;
459
872
  private archetypesByComponent;
460
873
  private entityReferences;
461
- private dontFragmentRelations;
462
- private componentEntityComponents;
463
- private relationEntityIdsByTarget;
464
- private queries;
465
- private queryCache;
466
- private legacyHooks;
874
+ /** Reverse index: entity ID → set of archetypes whose componentTypes include that entity ID */
875
+ private entityToReferencingArchetypes;
876
+ /** DontFragment relation storage, shared with all Archetype instances */
877
+ private readonly dontFragmentStore;
878
+ /** Component entity (singleton) storage */
879
+ private readonly componentEntities;
880
+ private readonly queryRegistry;
467
881
  private hooks;
468
882
  private commandBuffer;
469
883
  private readonly _changeset;
884
+ private readonly _removeChangeset;
470
885
  /** Cached command processor context to avoid per-entity object allocation */
471
886
  private readonly _commandCtx;
472
887
  /** Cached hooks context to avoid per-entity object allocation */
473
888
  private readonly _hooksCtx;
474
889
  constructor(snapshot?: SerializedWorld);
475
- private deserializeSnapshot;
476
890
  private createArchetypeSignature;
477
891
  /**
478
892
  * Creates a new entity.
@@ -487,23 +901,35 @@ declare class World {
487
901
  * world.sync();
488
902
  */
489
903
  new<T = void>(): EntityId<T>;
490
- private isComponentEntityId;
491
- private registerRelationEntityId;
492
- private unregisterRelationEntityId;
493
- private getComponentEntityComponents;
494
- private clearComponentEntityComponents;
495
- private cleanupComponentEntitiesReferencingEntity;
904
+ /**
905
+ * Semantic alias for `new()` to avoid confusion with the `new` keyword.
906
+ * Creates a new entity with an empty component set.
907
+ *
908
+ * @example
909
+ * const entity = world.create<MyComponent>();
910
+ */
911
+ create<T = void>(): EntityId<T>;
912
+ /** Fast path: destroy an entity that is not referenced by any other entity, skipping BFS */
913
+ private destroySingleEntity;
496
914
  private destroyEntityImmediate;
497
915
  /**
498
- * Checks if an entity exists in the world.
916
+ * Checks if an **entity** (not a component) exists in the world.
917
+ *
918
+ * This is specifically for checking entity liveness — whether the given entity ID
919
+ * is currently alive in the world. For checking if a component is present on an
920
+ * entity, use {@link has} instead.
499
921
  *
500
922
  * @param entityId - The entity identifier to check
501
923
  * @returns `true` if the entity exists, `false` otherwise
502
924
  *
503
925
  * @example
926
+ * // Check if an entity is alive
504
927
  * if (world.exists(entityId)) {
505
928
  * console.log("Entity exists");
506
929
  * }
930
+ *
931
+ * // To check for a component, use has() instead:
932
+ * if (world.has(entity, Position)) { ... }
507
933
  */
508
934
  exists(entityId: EntityId): boolean;
509
935
  private assertEntityExists;
@@ -511,7 +937,6 @@ declare class World {
511
937
  private assertSetComponentTypeValid;
512
938
  private resolveSetOperation;
513
939
  private resolveRemoveOperation;
514
- private getComponentEntityWildcardRelations;
515
940
  /**
516
941
  * Adds or updates a component on an entity (or marks void component as present).
517
942
  * The change is buffered and takes effect after calling `world.sync()`.
@@ -576,27 +1001,39 @@ declare class World {
576
1001
  */
577
1002
  delete(entityId: EntityId): void;
578
1003
  /**
579
- * Checks if an entity has a specific component.
1004
+ * Checks if a specific **component** is present on an entity.
1005
+ *
1006
+ * This is for component membership checks — does the given entity have this
1007
+ * component type? For checking whether an entity itself is alive, use
1008
+ * {@link exists} instead.
1009
+ *
580
1010
  * Immediately reflects the current state without waiting for `sync()`.
581
1011
  *
582
1012
  * @overload has<T>(entityId: EntityId, componentType: EntityId<T>): boolean
583
1013
  * Checks if a specific component type is present on the entity.
584
1014
  *
585
1015
  * @overload has<T>(componentId: ComponentId<T>): boolean
586
- * Checks if a singleton component has data (shorthand for has(componentId, componentId)).
1016
+ * Shorthand for checking a **singleton component** a component that is its own
1017
+ * entity (component-as-entity pattern). Equivalent to `has(componentId, componentId)`.
587
1018
  *
588
1019
  * @template T - The component data type
589
- * @param entityId - The entity identifier
1020
+ * @param entityId - The entity identifier, or a singleton component ID
590
1021
  * @param componentType - The component type to check
591
1022
  * @returns `true` if the entity has the component, `false` otherwise
592
1023
  *
593
1024
  * @example
1025
+ * // Check if an entity has a component
594
1026
  * if (world.has(entity, Position)) {
595
1027
  * const pos = world.get(entity, Position);
596
1028
  * }
1029
+ *
1030
+ * // Check a singleton component (component-as-entity)
597
1031
  * if (world.has(GlobalConfig)) {
598
1032
  * const config = world.get(GlobalConfig);
599
1033
  * }
1034
+ *
1035
+ * // Use exists() for entity liveness checks
1036
+ * if (world.exists(entity)) { ... }
600
1037
  */
601
1038
  has<T>(componentId: ComponentId<T>): boolean;
602
1039
  has<T>(entityId: EntityId, componentType: EntityId<T>): boolean;
@@ -653,27 +1090,22 @@ declare class World {
653
1090
  /**
654
1091
  * Registers a lifecycle hook that responds to component changes.
655
1092
  * The hook callback is invoked when components matching the specified types are added, updated, or removed.
656
- *
657
- * @deprecated For single components, use the array overload with LifecycleCallback for better multi-component support
658
- *
659
- * @overload hook<T>(componentType: EntityId<T>, hook: LegacyLifecycleHook<T> | LegacyLifecycleCallback<T>): () => void
660
- * Registers a hook for a single component type (legacy API).
661
- *
662
1093
  * @overload hook<const T extends readonly ComponentType<any>[]>(
663
1094
  * componentTypes: T,
664
1095
  * hook: LifecycleHook<T> | LifecycleCallback<T>,
1096
+ * filter?: QueryFilter,
665
1097
  * ): () => void
666
1098
  * Registers a hook for multiple component types.
667
- * The hook is triggered when all required components change together.
1099
+ * The hook is triggered when entities enter/exit the matching set.
668
1100
  *
669
- * @param componentTypesOrSingle - A single component type or an array of component types
1101
+ * @param componentTypes - Component types that define the matching entity set
670
1102
  * @param hook - Either a hook object with on_init/on_set/on_remove handlers, or a callback function
1103
+ * @param filter - Optional query-style filter applied to the hook match set
671
1104
  * @returns A function that unsubscribes the hook when called
672
1105
  *
673
1106
  * @throws {Error} If no required components are specified in array overload
674
1107
  *
675
1108
  * @example
676
- * // Array overload (recommended)
677
1109
  * const unsubscribe = world.hook([Position, Velocity], {
678
1110
  * on_init: (entityId, position, velocity) => console.log("Initialized"),
679
1111
  * on_set: (entityId, position, velocity) => console.log("Updated"),
@@ -685,13 +1117,17 @@ declare class World {
685
1117
  * const unsubscribe = world.hook([Position], (event, entityId, position) => {
686
1118
  * if (event === "init") console.log("Initialized");
687
1119
  * });
1120
+ *
1121
+ * // With filter
1122
+ * const unsubscribe2 = world.hook(
1123
+ * [Position, Velocity],
1124
+ * {
1125
+ * on_set: (entityId, position, velocity) => console.log(entityId, position, velocity),
1126
+ * },
1127
+ * { negativeComponentTypes: [Disabled] },
1128
+ * );
688
1129
  */
689
- hook<T>(componentType: EntityId<T>, hook: LegacyLifecycleHook<T> | LegacyLifecycleCallback<T>): () => void;
690
- hook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T> | LifecycleCallback<T>): () => void;
691
- /** @deprecated use the unsubscribe function returned by hook() instead */
692
- unhook<T>(componentType: EntityId<T>, hook: LegacyLifecycleHook<T>): void;
693
- /** @deprecated use the unsubscribe function returned by hook() instead */
694
- unhook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T>): void;
1130
+ hook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T> | LifecycleCallback<T>, filter?: QueryFilter): () => void;
695
1131
  /**
696
1132
  * Synchronizes all buffered commands (set/remove/delete) to the world.
697
1133
  * This method must be called after making changes via `set()`, `remove()`, or `delete()` for them to take effect.
@@ -763,8 +1199,6 @@ declare class World {
763
1199
  * world.sync();
764
1200
  */
765
1201
  spawnMany(count: number, configure: (builder: EntityBuilder, index: number) => EntityBuilder): EntityId[];
766
- _registerQuery(query: Query): void;
767
- _unregisterQuery(query: Query): void;
768
1202
  /**
769
1203
  * Releases a cached query and frees its resources if no longer needed.
770
1204
  * Call this when you're done using a query to allow the world to clean up its cache entry.
@@ -819,16 +1253,19 @@ declare class World {
819
1253
  entity: EntityId;
820
1254
  components: ComponentTuple<T>;
821
1255
  }>;
822
- executeEntityCommands(entityId: EntityId, commands: Command[]): ComponentChangeset;
823
- private executeComponentEntityCommands;
1256
+ private executeEntityCommands;
1257
+ private applyEntityCommands;
824
1258
  private createHooksContext;
825
1259
  private removeComponentImmediate;
826
1260
  private updateEntityReferences;
827
1261
  private ensureArchetype;
1262
+ /** Add componentType to the reverse index if it contains an entity ID */
1263
+ private addToReferencingIndex;
1264
+ /** Remove componentType from the reverse index */
1265
+ private removeFromReferencingIndex;
828
1266
  private createNewArchetype;
829
1267
  private updateArchetypeHookMatches;
830
1268
  private archetypeMatchesHook;
831
- private archetypeReferencesEntity;
832
1269
  private cleanupArchetypesReferencingEntity;
833
1270
  private removeArchetype;
834
1271
  /**
@@ -855,7 +1292,7 @@ declare class World {
855
1292
  serialize(): SerializedWorld;
856
1293
  }
857
1294
  //#endregion
858
- //#region src/core/builder.d.ts
1295
+ //#region src/world/builder.d.ts
859
1296
  /**
860
1297
  * A component definition for entity building, supporting both regular components and relations
861
1298
  */
@@ -869,28 +1306,62 @@ type ComponentDef<T = unknown> = {
869
1306
  targetId: EntityId<any>;
870
1307
  value: T;
871
1308
  };
1309
+ /**
1310
+ * Fluent API for constructing entities with multiple components.
1311
+ * Create instances via {@link World.spawn}.
1312
+ *
1313
+ * @example
1314
+ * const entity = world.spawn()
1315
+ * .with(Position, { x: 0, y: 0 })
1316
+ * .withRelation(Parent, parentEntity)
1317
+ * .build();
1318
+ * world.sync();
1319
+ */
872
1320
  declare class EntityBuilder {
873
1321
  private world;
874
1322
  private components;
875
1323
  constructor(world: World);
876
- with<T>(componentId: EntityId<T>, ...args: T extends void ? [] | [void] : [T]): this;
877
1324
  /**
878
- * @deprecated Use `with(componentId)` instead for void components
1325
+ * Add a regular component to the entity under construction.
1326
+ *
1327
+ * @template T - The component data type
1328
+ * @param componentId - The component type to add
1329
+ * @param args - Component data (omit for void components)
1330
+ * @returns This builder for chaining
1331
+ *
1332
+ * @example
1333
+ * builder.with(Position, { x: 10, y: 20 });
1334
+ * builder.with(Marker); // void component
879
1335
  */
880
- withTag(componentId: EntityId<void>): this;
881
- withRelation<T>(componentId: ComponentId<T>, targetEntity: EntityId<any>, ...args: T extends void ? [] | [void] : [T]): this;
1336
+ with<T>(componentId: EntityId<T>, ...args: T extends void ? [] | [void] : [T]): this;
882
1337
  /**
883
- * @deprecated Use `withRelation(componentId, targetEntity)` instead for void relations
1338
+ * Add a relation component to the entity under construction.
1339
+ *
1340
+ * @template T - The relation data type
1341
+ * @param componentId - The base component type for the relation
1342
+ * @param targetEntity - The target entity or component for the relation
1343
+ * @param args - Relation data (omit for void relations)
1344
+ * @returns This builder for chaining
1345
+ *
1346
+ * @example
1347
+ * builder.withRelation(Parent, parentEntity);
1348
+ * builder.withRelation(ChildOf, childEntity, { order: 1 });
884
1349
  */
885
- withRelationTag(componentId: ComponentId<void>, targetEntity: EntityId<any>): this;
1350
+ withRelation<T>(componentId: ComponentId<T>, targetEntity: EntityId<any>, ...args: T extends void ? [] | [void] : [T]): this;
886
1351
  /**
887
- * Create an entity and enqueue components to be applied. This method
888
- * does NOT call `world.sync()` automatically; callers must invoke
889
- * `world.sync()` to apply deferred commands.
890
- * (Previously auto-synced; now a breaking change — buildDeferred() removed.)
1352
+ * Create the entity and enqueue all configured components.
1353
+ * The entity and components are only materialised after {@link World.sync} is called.
1354
+ *
1355
+ * @returns The newly created entity ID
1356
+ *
1357
+ * @example
1358
+ * const entity = world.spawn()
1359
+ * .with(Position, { x: 0, y: 0 })
1360
+ * .build();
1361
+ * world.sync(); // Apply changes
891
1362
  */
892
1363
  build(): EntityId;
893
1364
  }
894
1365
  //#endregion
895
- export { isRelationId as A, ComponentRelationId as C, WildcardRelationId as D, RelationId as E, isComponentId as O, ComponentId as S, EntityRelationId as T, getComponentIdByName as _, SerializedEntity as a, isWildcardRelationId as b, Query as c, LegacyLifecycleCallback as d, LegacyLifecycleHook as f, component as g, ComponentOptions as h, SerializedComponent as i, isEntityId as k, ComponentTuple as l, LifecycleHook as m, EntityBuilder as n, SerializedEntityId as o, LifecycleCallback as p, World as r, SerializedWorld as s, ComponentDef as t, ComponentType as u, getComponentNameById as v, EntityId as w, relation as x, decodeRelationId as y };
1366
+ 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 };
896
1367
  //# sourceMappingURL=builder.d.mts.map