• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

excaliburjs / Excalibur / 17382328181

01 Sep 2025 03:52PM UTC coverage: 87.365% (+0.006%) from 87.359%
17382328181

Pull #3511

github

web-flow
Merge e5f6ec63d into e8be5e077
Pull Request #3511: fix: Clean up memory leaks from Entities in Maps [#3510]

5180 of 7245 branches covered (71.5%)

14029 of 16058 relevant lines covered (87.36%)

24482.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.79
/src/engine/EntityComponentSystem/QueryManager.ts
1
import type { Entity } from './Entity';
2
import type { QueryParams } from './Query';
3
import { Query } from './Query';
4
import type { Component, ComponentCtor } from './Component';
5
import type { World } from './World';
6
import { TagQuery } from './TagQuery';
7

8
/**
9
 * The query manager is responsible for updating all queries when entities/components change
10
 */
11
export class QueryManager {
12
  private _queries = new Map<string, Query<any, any>>();
1,313✔
13
  private _addComponentHandlers = new Map<Entity, (c: Component) => any>();
1,313✔
14
  private _removeComponentHandlers = new Map<Entity, (c: Component) => any>();
1,313✔
15
  private _componentToQueriesIndex = new Map<ComponentCtor<any>, Query<any, any>[]>();
1,313✔
16

17
  private _tagQueries = new Map<string, TagQuery<any>>();
1,313✔
18
  private _addTagHandlers = new Map<Entity, (tag: string) => any>();
1,313✔
19
  private _removeTagHandlers = new Map<Entity, (tag: string) => any>();
1,313✔
20
  private _tagToQueriesIndex = new Map<string, TagQuery<any>[]>();
1,313✔
21

22
  constructor(private _world: World) {}
1,313✔
23

24
  public createQuery<
25
    TKnownComponentCtors extends ComponentCtor<Component> = never,
26
    TAnyComponentCtors extends ComponentCtor<Component> = never
27
  >(
28
    params: TKnownComponentCtors[] | QueryParams<TKnownComponentCtors, TAnyComponentCtors>
29
  ): Query<TKnownComponentCtors, TAnyComponentCtors> {
30
    const id = Query.createId(params);
10,534✔
31
    if (this._queries.has(id)) {
10,534✔
32
      // short circuit if query is already created
33
      return this._queries.get(id) as Query<TKnownComponentCtors>;
1,541✔
34
    }
35

36
    const query = new Query<TKnownComponentCtors, TAnyComponentCtors>(params);
8,993✔
37

38
    this._queries.set(query.id, query);
8,993✔
39

40
    // index maintenance
41
    for (const component of [...query.filter.components.all, ...query.filter.components.any, ...query.filter.components.not]) {
8,993✔
42
      const queries = this._componentToQueriesIndex.get(component);
15,412✔
43
      if (!queries) {
15,412✔
44
        this._componentToQueriesIndex.set(component, [query]);
8,999✔
45
      } else {
46
        queries.push(query);
6,413✔
47
      }
48
    }
49

50
    for (const entity of this._world.entities) {
8,993✔
51
      this.addEntity(entity);
6✔
52
    }
53

54
    return query;
8,993✔
55
  }
56

57
  public createTagQuery<TKnownTags extends string>(requiredTags: TKnownTags[]): TagQuery<TKnownTags> {
58
    const id = TagQuery.createId(requiredTags);
3✔
59
    if (this._tagQueries.has(id)) {
3!
60
      // short circuit if query is already created
61
      return this._tagQueries.get(id) as TagQuery<TKnownTags>;
×
62
    }
63

64
    const query = new TagQuery(requiredTags);
3✔
65

66
    this._tagQueries.set(query.id, query);
3✔
67

68
    // index maintenance
69
    for (const tag of requiredTags) {
3✔
70
      const queries = this._tagToQueriesIndex.get(tag);
6✔
71
      if (!queries) {
6!
72
        this._tagToQueriesIndex.set(tag, [query]);
6✔
73
      } else {
74
        queries.push(query);
×
75
      }
76
    }
77

78
    for (const entity of this._world.entities) {
3✔
79
      this.addEntity(entity);
×
80
    }
81

82
    return query;
3✔
83
  }
84

85
  private _createAddComponentHandler = (entity: Entity) => (c: Component) => {
1,967✔
86
    this.addComponent(entity, c);
6✔
87
  };
88

89
  private _createRemoveComponentHandler = (entity: Entity) => (c: Component) => {
1,967✔
90
    this.removeComponent(entity, c);
8✔
91
  };
92

93
  private _createAddTagHandler = (entity: Entity) => (tag: string) => {
1,967✔
94
    this.addTag(entity, tag);
24✔
95
  };
96

97
  private _createRemoveTagHandler = (entity: Entity) => (tag: string) => {
1,967✔
98
    this.removeTag(entity, tag);
1✔
99
  };
100

101
  /**
102
   * Scans queries and locates any that need this entity added
103
   * @param entity
104
   */
105
  addEntity(entity: Entity) {
106
    const maybeAddComponent = this._addComponentHandlers.get(entity);
1,973✔
107
    const maybeRemoveComponent = this._removeComponentHandlers.get(entity);
1,973✔
108
    const addComponent = maybeAddComponent ?? this._createAddComponentHandler(entity);
1,973✔
109
    const removeComponent = maybeRemoveComponent ?? this._createRemoveComponentHandler(entity);
1,973✔
110
    this._addComponentHandlers.set(entity, addComponent);
1,973✔
111
    this._removeComponentHandlers.set(entity, removeComponent);
1,973✔
112

113
    const maybeAddTag = this._addTagHandlers.get(entity);
1,973✔
114
    const maybeRemoveTag = this._removeTagHandlers.get(entity);
1,973✔
115
    const addTag = maybeAddTag ?? this._createAddTagHandler(entity);
1,973✔
116
    const removeTag = maybeRemoveTag ?? this._createRemoveTagHandler(entity);
1,973✔
117
    this._addTagHandlers.set(entity, addTag);
1,973✔
118
    this._removeTagHandlers.set(entity, removeTag);
1,973✔
119

120
    for (const query of this._queries.values()) {
1,973✔
121
      query.checkAndAdd(entity);
13,508✔
122
    }
123
    for (const tagQuery of this._tagQueries.values()) {
1,973✔
124
      tagQuery.checkAndAdd(entity);
6✔
125
    }
126
    entity.componentAdded$.subscribe(addComponent);
1,973✔
127
    entity.componentRemoved$.subscribe(removeComponent);
1,973✔
128
    entity.tagAdded$.subscribe(addTag);
1,973✔
129
    entity.tagRemoved$.subscribe(removeTag);
1,973✔
130
  }
131

132
  /**
133
   * Scans queries and locates any that need this entity removed
134
   * @param entity
135
   */
136
  removeEntity(entity: Entity) {
137
    // Handle components
138
    const addComponent = this._addComponentHandlers.get(entity);
54✔
139
    const removeComponent = this._removeComponentHandlers.get(entity);
54✔
140
    for (const query of this._queries.values()) {
54✔
141
      query.removeEntity(entity);
275✔
142
    }
143
    if (addComponent) {
54!
144
      entity.componentAdded$.unsubscribe(addComponent);
54✔
145
      this._addComponentHandlers.delete(entity);
54✔
146
    }
147
    if (removeComponent) {
54!
148
      entity.componentRemoved$.unsubscribe(removeComponent);
54✔
149
      this._removeComponentHandlers.delete(entity);
54✔
150
    }
151

152
    // Handle tags
153
    const addTag = this._addTagHandlers.get(entity);
54✔
154
    const removeTag = this._removeTagHandlers.get(entity);
54✔
155
    for (const tagQuery of this._tagQueries.values()) {
54✔
156
      tagQuery.removeEntity(entity);
2✔
157
    }
158

159
    if (addTag) {
54!
160
      entity.tagAdded$.unsubscribe(addTag);
54✔
161
      this._addTagHandlers.delete(entity);
54✔
162
    }
163
    if (removeTag) {
54!
164
      entity.tagRemoved$.unsubscribe(removeTag);
54✔
165
      this._removeTagHandlers.delete(entity);
54✔
166
    }
167
  }
168

169
  /**
170
   * Updates any queries when a component is added to an entity
171
   * @param entity
172
   * @param component
173
   */
174
  addComponent(entity: Entity, component: Component) {
175
    const queries = this._componentToQueriesIndex.get(component.constructor as ComponentCtor<any>) ?? [];
6✔
176
    for (const query of queries) {
6✔
177
      query.checkAndAdd(entity);
5✔
178
    }
179
  }
180

181
  /**
182
   * Updates any queries when a component is removed from an entity
183
   * @param entity
184
   * @param component
185
   */
186
  removeComponent(entity: Entity, component: Component) {
187
    const queries = this._componentToQueriesIndex.get(component.constructor as ComponentCtor<any>) ?? [];
10✔
188
    for (const query of queries) {
10✔
189
      query.removeEntity(entity);
9✔
190
    }
191
  }
192

193
  /**
194
   * Updates any queries when a tag is added to an entity
195
   * @param entity
196
   * @param tag
197
   */
198
  addTag(entity: Entity, tag: string) {
199
    const queries = this._tagToQueriesIndex.get(tag) ?? [];
24!
200
    for (const query of queries) {
24✔
201
      query.checkAndAdd(entity);
×
202
    }
203
  }
204

205
  /**
206
   * Updates any queries when a component is removed from an entity
207
   * @param entity
208
   * @param tag
209
   */
210
  removeTag(entity: Entity, tag: string) {
211
    const queries = this._tagToQueriesIndex.get(tag) ?? [];
2!
212
    for (const query of queries) {
2✔
213
      query.removeEntity(entity);
2✔
214
    }
215
  }
216
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc