@codehz/ecs 0.8.0 → 0.8.1

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.
@@ -9,13 +9,7 @@ import {
9
9
  } from "../entity";
10
10
  import { isOptionalEntityId, type ComponentTuple, type ComponentType, type LifecycleHookEntry } from "../types";
11
11
  import { getOrCompute } from "../utils/utils";
12
- import {
13
- buildCacheKey,
14
- buildSingleComponent,
15
- findMatchingDontFragmentRelations,
16
- getWildcardRelationDataSource,
17
- isRelationType,
18
- } from "./helpers";
12
+ import { buildCacheKey, buildSingleComponent, getWildcardRelationDataSource, isRelationType } from "./helpers";
19
13
  import type { DontFragmentStore } from "./store";
20
14
 
21
15
  /**
@@ -56,9 +50,9 @@ export class Archetype {
56
50
  private entityToIndex: Map<EntityId, number> = new Map();
57
51
 
58
52
  /**
59
- * DontFragmentStore for relation data keyed by entity ID.
60
- * This allows entities with different relation targets to share the same archetype
61
- * without migration overhead when entities change archetypes.
53
+ * DontFragmentStore (keyed primarily by relation ComponentId).
54
+ * Uses optimized RelationEntry (single/multi) for the common exclusive case.
55
+ * See store.ts for implementation details.
62
56
  */
63
57
  private dontFragmentRelations: DontFragmentStore;
64
58
 
@@ -118,20 +112,14 @@ export class Archetype {
118
112
  }
119
113
 
120
114
  private addDontFragmentRelations(entityId: EntityId, componentData: Map<EntityId<any>, any>): void {
121
- const dontFragmentData = new Map<EntityId<any>, any>();
122
-
123
115
  for (const [componentType, data] of componentData) {
124
116
  if (this.componentTypeSet.has(componentType)) continue;
125
117
 
126
118
  const detailedType = getDetailedIdType(componentType);
127
119
  if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId!)) {
128
- dontFragmentData.set(componentType, data);
120
+ this.dontFragmentRelations.setValue(entityId, componentType, data);
129
121
  }
130
122
  }
131
-
132
- if (dontFragmentData.size > 0) {
133
- this.dontFragmentRelations.set(entityId, dontFragmentData);
134
- }
135
123
  }
136
124
 
137
125
  getEntity(entityId: EntityId): Map<EntityId<any>, any> | undefined {
@@ -147,18 +135,29 @@ export class Archetype {
147
135
  }
148
136
 
149
137
  // Add dontFragment relations
150
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
151
- if (dontFragmentData) {
152
- for (const [componentType, data] of dontFragmentData) {
153
- entityData.set(componentType, data);
154
- }
138
+ const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entityId);
139
+ for (const [componentType, data] of dontFragmentTuples) {
140
+ entityData.set(componentType, data);
155
141
  }
156
142
 
157
143
  return entityData;
158
144
  }
159
145
 
146
+ /**
147
+ * Returns all dontFragment relations for the given entity as an array of tuples.
148
+ * This is a compatibility adapter during the store refactor.
149
+ *
150
+ * Prefer the new DontFragmentStore methods when possible.
151
+ */
160
152
  getEntityDontFragmentRelations(entityId: EntityId): Map<EntityId<any>, any> | undefined {
161
- return this.dontFragmentRelations.get(entityId);
153
+ const tuples = this.dontFragmentRelations.getAllForEntity(entityId);
154
+ if (tuples.length === 0) return undefined;
155
+
156
+ const map = new Map<EntityId<any>, any>();
157
+ for (const [relType, data] of tuples) {
158
+ map.set(relType, data);
159
+ }
160
+ return map;
162
161
  }
163
162
 
164
163
  dump(): Array<{ entity: EntityId; components: Map<EntityId<any>, any> }> {
@@ -170,11 +169,9 @@ export class Archetype {
170
169
  components.set(componentType, data === MISSING_COMPONENT ? undefined : data);
171
170
  }
172
171
 
173
- const dontFragmentData = this.dontFragmentRelations.get(entity);
174
- if (dontFragmentData) {
175
- for (const [componentType, data] of dontFragmentData) {
176
- components.set(componentType, data);
177
- }
172
+ const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entity);
173
+ for (const [componentType, data] of dontFragmentTuples) {
174
+ components.set(componentType, data);
178
175
  }
179
176
 
180
177
  return { entity, components };
@@ -192,13 +189,11 @@ export class Archetype {
192
189
  }
193
190
 
194
191
  // Include dontFragment relations
195
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
196
- if (dontFragmentData) {
197
- for (const [componentType, data] of dontFragmentData) {
198
- removedData.set(componentType, data);
199
- }
200
- this.dontFragmentRelations.delete(entityId);
192
+ const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entityId);
193
+ for (const [componentType, data] of dontFragmentTuples) {
194
+ removedData.set(componentType, data);
201
195
  }
196
+ this.dontFragmentRelations.deleteEntity(entityId);
202
197
 
203
198
  this.entityToIndex.delete(entityId);
204
199
 
@@ -262,9 +257,10 @@ export class Archetype {
262
257
  }
263
258
  }
264
259
 
265
- // Check dontFragment relations
260
+ // Check dontFragment relations (now uses the efficient per-component path)
266
261
  if (componentId !== undefined) {
267
- findMatchingDontFragmentRelations(this.dontFragmentRelations.get(entityId), componentId, relations);
262
+ const matches = this.dontFragmentRelations.getRelationsForComponent(entityId, componentId);
263
+ for (const m of matches) relations.push(m);
268
264
  }
269
265
 
270
266
  return relations;
@@ -279,9 +275,14 @@ export class Archetype {
279
275
  return data as T;
280
276
  }
281
277
 
282
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
283
- if (dontFragmentData?.has(componentType)) {
284
- return dontFragmentData.get(componentType);
278
+ const value = this.dontFragmentRelations.getValue(entityId, componentType);
279
+ if (
280
+ value !== undefined ||
281
+ this.dontFragmentRelations.getAllForEntity(entityId).some(([t]) => t === componentType)
282
+ ) {
283
+ // Note: the extra check above handles the (rare) case where `undefined` is a legitimate stored value.
284
+ // For the common case we just return whatever getValue gave us.
285
+ return this.dontFragmentRelations.getValue(entityId, componentType);
285
286
  }
286
287
 
287
288
  throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
@@ -299,11 +300,15 @@ export class Archetype {
299
300
  return { value: data as T };
300
301
  }
301
302
 
302
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
303
- if (dontFragmentData?.has(componentType)) {
304
- return { value: dontFragmentData.get(componentType) };
303
+ const value = this.dontFragmentRelations.getValue(entityId, componentType);
304
+ // We use getAllForEntity only as a presence check when the value itself might be undefined.
305
+ if (value !== undefined) {
306
+ return { value };
307
+ }
308
+ const all = this.dontFragmentRelations.getAllForEntity(entityId);
309
+ if (all.some(([t]) => t === componentType)) {
310
+ return { value: this.dontFragmentRelations.getValue(entityId, componentType) };
305
311
  }
306
-
307
312
  return undefined;
308
313
  }
309
314
 
@@ -320,12 +325,7 @@ export class Archetype {
320
325
 
321
326
  const detailedType = getDetailedIdType(componentType);
322
327
  if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId!)) {
323
- let dontFragmentData = this.dontFragmentRelations.get(entityId);
324
- if (!dontFragmentData) {
325
- dontFragmentData = new Map();
326
- this.dontFragmentRelations.set(entityId, dontFragmentData);
327
- }
328
- dontFragmentData.set(componentType, data);
328
+ this.dontFragmentRelations.setValue(entityId, componentType, data);
329
329
  return;
330
330
  }
331
331
 
@@ -436,12 +436,20 @@ export class Archetype {
436
436
 
437
437
  forEach(callback: (entityId: EntityId, components: Map<EntityId<any>, any>) => void): void {
438
438
  for (let i = 0; i < this.entities.length; i++) {
439
+ const entity = this.entities[i]!;
439
440
  const components = new Map<EntityId<any>, any>();
440
441
  for (const componentType of this.componentTypes) {
441
442
  const data = this.getComponentData(componentType)[i];
442
443
  components.set(componentType, data === MISSING_COMPONENT ? undefined : data);
443
444
  }
444
- callback(this.entities[i]!, components);
445
+
446
+ // Append dontFragment relations (Y-class path, acceptable cost)
447
+ const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entity);
448
+ for (const [componentType, data] of dontFragmentTuples) {
449
+ components.set(componentType, data);
450
+ }
451
+
452
+ callback(entity, components);
445
453
  }
446
454
  }
447
455
 
@@ -454,19 +462,15 @@ export class Archetype {
454
462
  }
455
463
  }
456
464
 
457
- // Check dontFragment relations
465
+ // Check dontFragment relations only for entities that actually belong to *this* archetype.
466
+ // We must not use the global hasAnyForComponent here, otherwise unrelated archetypes
467
+ // can be incorrectly pulled into wildcard queries when any entity in the world has the relation.
458
468
  for (const entityId of this.entities) {
459
- const entityDontFragmentRelations = this.dontFragmentRelations.get(entityId);
460
- if (entityDontFragmentRelations) {
461
- for (const relationType of entityDontFragmentRelations.keys()) {
462
- const detailedType = getDetailedIdType(relationType);
463
- if (isRelationType(detailedType) && detailedType.componentId === componentId) {
464
- return true;
465
- }
466
- }
469
+ const rels = this.dontFragmentRelations.getRelationsForComponent(entityId, componentId);
470
+ if (rels.length > 0) {
471
+ return true;
467
472
  }
468
473
  }
469
-
470
474
  return false;
471
475
  }
472
476
  }
@@ -67,7 +67,10 @@ export function matchesRelationComponentId(componentType: EntityId<any>, compone
67
67
  }
68
68
 
69
69
  /**
70
- * Find all relations in dontFragment data that match a component ID
70
+ * Find all relations in dontFragment data that match a component ID.
71
+ *
72
+ * @deprecated Prefer calling `DontFragmentStore.getRelationsForComponent` directly.
73
+ * This helper is kept temporarily for any remaining call sites during the refactor.
71
74
  */
72
75
  export function findMatchingDontFragmentRelations(
73
76
  dontFragmentData: Map<EntityId<any>, any> | undefined,
@@ -105,13 +108,14 @@ export function getWildcardRelationDataSource(
105
108
  }
106
109
 
107
110
  /**
108
- * Build wildcard relation value from matching relations
111
+ * Build wildcard relation value from matching relations.
112
+ * Now receives the DontFragmentStore directly for efficient per-component lookups.
109
113
  */
110
114
  export function buildWildcardRelationValue(
111
115
  wildcardRelationType: WildcardRelationId<any>,
112
116
  matchingRelations: EntityId<any>[] | undefined,
113
117
  getDataAtIndex: (relType: EntityId<any>) => any,
114
- dontFragmentData: Map<EntityId<any>, any> | undefined,
118
+ dontFragmentStore: DontFragmentStore,
115
119
  entityId: EntityId,
116
120
  optional: boolean,
117
121
  ): any {
@@ -125,9 +129,12 @@ export function buildWildcardRelationValue(
125
129
  relations.push([targetId, data === MISSING_COMPONENT ? undefined : data]);
126
130
  }
127
131
 
128
- // Add dontFragment relations
132
+ // Add dontFragment relations using the efficient store API (key win for X-class workload)
129
133
  if (targetComponentId !== undefined) {
130
- findMatchingDontFragmentRelations(dontFragmentData, targetComponentId, relations);
134
+ const dfMatches = dontFragmentStore.getRelationsForComponent(entityId, targetComponentId);
135
+ for (const m of dfMatches) {
136
+ relations.push(m);
137
+ }
131
138
  }
132
139
 
133
140
  if (relations.length === 0) {
@@ -176,7 +183,7 @@ export function buildSingleComponent(
176
183
  actualType as WildcardRelationId<any>,
177
184
  dataSource as EntityId<any>[] | undefined,
178
185
  (relType) => getComponentData(relType)[entityIndex],
179
- dontFragmentRelations.get(entityId),
186
+ dontFragmentRelations,
180
187
  entityId,
181
188
  optional,
182
189
  );
@@ -1,33 +1,240 @@
1
1
  import type { EntityId } from "../entity";
2
+ import { getComponentIdFromRelationId, getTargetIdFromRelationId } from "../entity";
2
3
 
3
4
  /**
4
- * Minimal interface for storing dontFragment relation data keyed by entity ID.
5
+ * Internal representation for the relations an entity has under one component kind.
6
+ * 'single' is the optimized form for exclusive relations (vast majority of cases).
7
+ */
8
+ type RelationEntry =
9
+ | { type: "single"; relationType: EntityId<any>; target: EntityId; data: any }
10
+ | { type: "multi"; targets: Map<EntityId, { relationType: EntityId<any>; data: any }> };
11
+
12
+ /**
13
+ * Interface for storing dontFragment relation data.
5
14
  *
6
- * Using an interface here decouples `Archetype` (and `world-commands.ts`) from
7
- * the concrete `Map` used by `World`, making archetypes independently testable.
15
+ * Storage is now primarily keyed by relation ComponentId (the "kind" of relation)
16
+ * rather than by entity. This provides O(1) or near-O(1) answers for the hot
17
+ * wildcard-related paths (hasRelationWithComponentId, wildcard materialization
18
+ * during iteration, hook matching, etc.).
19
+ *
20
+ * A lightweight reverse index (entity -> Set of base ComponentIds) is maintained
21
+ * to efficiently support the infrequent "get all dontFragment data for this entity"
22
+ * operations (removeEntity, dump, getEntity, serialization).
23
+ *
24
+ * The interface no longer leaks internal Map structures. Callers work with
25
+ * semantic operations only.
8
26
  */
9
27
  export interface DontFragmentStore {
10
- get(entityId: EntityId): Map<EntityId<any>, any> | undefined;
11
- set(entityId: EntityId, data: Map<EntityId<any>, any>): void;
12
- delete(entityId: EntityId): void;
28
+ // High-frequency operations (used by get/set/getOptional and structural changes)
29
+ getValue(entityId: EntityId, relationType: EntityId<any>): any | undefined;
30
+ setValue(entityId: EntityId, relationType: EntityId<any>, data: any): void;
31
+ deleteValue(entityId: EntityId, relationType: EntityId<any>): boolean;
32
+
33
+ // Wildcard / filtering hot paths (X-class priority)
34
+ hasAnyForComponent(componentId: EntityId<any>): boolean;
35
+ getRelationsForComponent(entityId: EntityId, componentId: EntityId<any>): [target: EntityId, data: any][];
36
+
37
+ // Low-frequency "get everything for entity" paths (Y-class, acceptable cost)
38
+ getAllForEntity(entityId: EntityId): Array<[relationType: EntityId<any>, data: any]>;
39
+ deleteEntity(entityId: EntityId): void;
13
40
  }
14
41
 
15
42
  /**
16
- * Default implementation backed by a plain `Map`.
17
- * Created once by `World` and shared with every `Archetype`.
43
+ * Production implementation of DontFragmentStore.
44
+ *
45
+ * Internal layout (optimized):
46
+ * - byComponent: baseComponentId → (entityId → RelationEntry)
47
+ * RelationEntry uses a single-value form for the common exclusive case (1 target),
48
+ * avoiding Map allocation entirely for the vast majority of dontFragment usage.
49
+ * - entityIndex: entityId → Set<baseComponentId>
50
+ * Lightweight reverse index.
18
51
  */
19
52
  export class DontFragmentStoreImpl implements DontFragmentStore {
20
- private readonly data: Map<EntityId, Map<EntityId<any>, any>> = new Map();
53
+ /**
54
+ * Primary storage, keyed by the base relation component ID.
55
+ */
56
+ private byComponent = new Map<
57
+ EntityId<any>, // base componentId
58
+ Map<EntityId, RelationEntry>
59
+ >();
60
+
61
+ /**
62
+ * Reverse index: which base component kinds an entity participates in.
63
+ * Used only by the infrequent getAllForEntity / deleteEntity paths.
64
+ */
65
+ private entityIndex = new Map<EntityId, Set<EntityId<any>>>();
66
+
67
+ getValue(entityId: EntityId, relationType: EntityId<any>): any | undefined {
68
+ const componentId = getComponentIdFromRelationId(relationType);
69
+ if (componentId === undefined) return undefined;
70
+
71
+ const entities = this.byComponent.get(componentId);
72
+ if (!entities) return undefined;
21
73
 
22
- get(entityId: EntityId): Map<EntityId<any>, any> | undefined {
23
- return this.data.get(entityId);
74
+ const entry = entities.get(entityId);
75
+ if (!entry) return undefined;
76
+
77
+ const targetId = getTargetIdFromRelationId(relationType)!;
78
+
79
+ if (entry.type === "single") {
80
+ return entry.target === targetId ? entry.data : undefined;
81
+ } else {
82
+ const item = entry.targets.get(targetId);
83
+ return item ? item.data : undefined;
84
+ }
24
85
  }
25
86
 
26
- set(entityId: EntityId, data: Map<EntityId<any>, any>): void {
27
- this.data.set(entityId, data);
87
+ setValue(entityId: EntityId, relationType: EntityId<any>, data: any): void {
88
+ const componentId = getComponentIdFromRelationId(relationType);
89
+ if (componentId === undefined) {
90
+ throw new Error("setValue called with a non-relation type on DontFragmentStore");
91
+ }
92
+
93
+ let entities = this.byComponent.get(componentId);
94
+ if (!entities) {
95
+ entities = new Map();
96
+ this.byComponent.set(componentId, entities);
97
+ }
98
+
99
+ const targetId = getTargetIdFromRelationId(relationType)!;
100
+ let entry = entities.get(entityId);
101
+
102
+ if (!entry) {
103
+ // First relation for this (entity, component) — use single form (big win for exclusive)
104
+ entry = { type: "single", relationType, target: targetId, data };
105
+ entities.set(entityId, entry);
106
+ } else if (entry.type === "single") {
107
+ if (entry.target === targetId) {
108
+ entry.data = data;
109
+ entry.relationType = relationType; // update in case it changed
110
+ } else {
111
+ // Promote to multi
112
+ const targets = new Map();
113
+ targets.set(entry.target, { relationType: entry.relationType, data: entry.data });
114
+ targets.set(targetId, { relationType, data });
115
+ entities.set(entityId, { type: "multi", targets });
116
+ }
117
+ } else {
118
+ entry.targets.set(targetId, { relationType, data });
119
+ }
120
+
121
+ // Maintain reverse index
122
+ let components = this.entityIndex.get(entityId);
123
+ if (!components) {
124
+ components = new Set();
125
+ this.entityIndex.set(entityId, components);
126
+ }
127
+ components.add(componentId);
28
128
  }
29
129
 
30
- delete(entityId: EntityId): void {
31
- this.data.delete(entityId);
130
+ deleteValue(entityId: EntityId, relationType: EntityId<any>): boolean {
131
+ const componentId = getComponentIdFromRelationId(relationType);
132
+ if (componentId === undefined) return false;
133
+
134
+ const entities = this.byComponent.get(componentId);
135
+ if (!entities) return false;
136
+
137
+ const entry = entities.get(entityId);
138
+ if (!entry) return false;
139
+
140
+ const targetId = getTargetIdFromRelationId(relationType)!;
141
+ let existed = false;
142
+
143
+ if (entry.type === "single") {
144
+ if (entry.target === targetId) {
145
+ existed = true;
146
+ entities.delete(entityId);
147
+ }
148
+ } else {
149
+ existed = entry.targets.delete(targetId);
150
+ if (entry.targets.size === 0) {
151
+ entities.delete(entityId);
152
+ } else if (entry.targets.size === 1) {
153
+ // Demote to single
154
+ const [first] = entry.targets.entries();
155
+ const [t, item] = first!;
156
+ entities.set(entityId, { type: "single", relationType: item.relationType, target: t, data: item.data });
157
+ }
158
+ }
159
+
160
+ if (!entities.has(entityId) && entities.size === 0) {
161
+ this.byComponent.delete(componentId);
162
+ }
163
+
164
+ // Update reverse index
165
+ const components = this.entityIndex.get(entityId);
166
+ if (components && !entities.has(entityId)) {
167
+ components.delete(componentId);
168
+ if (components.size === 0) {
169
+ this.entityIndex.delete(entityId);
170
+ }
171
+ }
172
+
173
+ return existed;
174
+ }
175
+
176
+ hasAnyForComponent(componentId: EntityId<any>): boolean {
177
+ const entities = this.byComponent.get(componentId);
178
+ return entities !== undefined && entities.size > 0;
179
+ }
180
+
181
+ getRelationsForComponent(entityId: EntityId, componentId: EntityId<any>): [EntityId, any][] {
182
+ const result: [EntityId, any][] = [];
183
+
184
+ const entities = this.byComponent.get(componentId);
185
+ if (!entities) return result;
186
+
187
+ const entry = entities.get(entityId);
188
+ if (!entry) return result;
189
+
190
+ if (entry.type === "single") {
191
+ result.push([entry.target, entry.data]);
192
+ } else {
193
+ for (const [target, item] of entry.targets) {
194
+ result.push([target, item.data]);
195
+ }
196
+ }
197
+
198
+ return result;
199
+ }
200
+
201
+ getAllForEntity(entityId: EntityId): Array<[relationType: EntityId<any>, data: any]> {
202
+ const components = this.entityIndex.get(entityId);
203
+ if (!components || components.size === 0) return [];
204
+
205
+ const result: Array<[EntityId<any>, any]> = [];
206
+
207
+ for (const componentId of components) {
208
+ const entities = this.byComponent.get(componentId);
209
+ const entry = entities?.get(entityId);
210
+ if (entry) {
211
+ if (entry.type === "single") {
212
+ result.push([entry.relationType, entry.data]);
213
+ } else {
214
+ for (const item of entry.targets.values()) {
215
+ result.push([item.relationType, item.data]);
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ return result;
222
+ }
223
+
224
+ deleteEntity(entityId: EntityId): void {
225
+ const components = this.entityIndex.get(entityId);
226
+ if (!components) return;
227
+
228
+ for (const componentId of components) {
229
+ const entities = this.byComponent.get(componentId);
230
+ if (entities) {
231
+ entities.delete(entityId);
232
+ if (entities.size === 0) {
233
+ this.byComponent.delete(componentId);
234
+ }
235
+ }
236
+ }
237
+
238
+ this.entityIndex.delete(entityId);
32
239
  }
33
240
  }
@@ -172,6 +172,15 @@ export function maybeRemoveWildcardMarker(
172
172
  }
173
173
  }
174
174
 
175
+ // Also check if this changeset itself is adding another relation of the same kind
176
+ // (common in exclusive dontFragment flips: remove old target + add new target in one batch)
177
+ for (const addedType of changeset.adds.keys()) {
178
+ if (addedType === removedComponentType) continue;
179
+ if (getComponentIdFromRelationId(addedType) === componentId) {
180
+ return; // Replacement is being added in the same changeset, keep the marker
181
+ }
182
+ }
183
+
175
184
  changeset.delete(wildcardMarker);
176
185
  }
177
186
 
@@ -280,6 +289,8 @@ export function applyChangeset(
280
289
 
281
290
  /**
282
291
  * No-hooks variant of applyDontFragmentChanges that skips tracking removed component data.
292
+ *
293
+ * Rewritten for the new DontFragmentStore interface (ComponentId-primary storage).
283
294
  */
284
295
  function applyDontFragmentChanges(
285
296
  dontFragmentRelations: DontFragmentStore,
@@ -287,35 +298,25 @@ function applyDontFragmentChanges(
287
298
  changeset: ComponentChangeset,
288
299
  removedComponents: Map<EntityId<any>, any>,
289
300
  ): void {
290
- // Get or create the entity's dontFragment relations map
291
- let entityRelations = dontFragmentRelations.get(entityId);
292
-
293
301
  for (const componentType of changeset.removes) {
294
302
  if (isDontFragmentRelation(componentType)) {
295
- if (entityRelations) {
296
- const removedValue = entityRelations.get(componentType);
297
- if (removedValue !== undefined || entityRelations.has(componentType)) {
298
- removedComponents.set(componentType, removedValue);
299
- entityRelations.delete(componentType);
300
- }
303
+ const removedValue = dontFragmentRelations.getValue(entityId, componentType);
304
+ // Record for hooks if we are actually removing something
305
+ if (
306
+ removedValue !== undefined ||
307
+ dontFragmentRelations.getAllForEntity(entityId).some(([t]) => t === componentType)
308
+ ) {
309
+ removedComponents.set(componentType, removedValue);
301
310
  }
311
+ dontFragmentRelations.deleteValue(entityId, componentType);
302
312
  }
303
313
  }
304
314
 
305
315
  for (const [componentType, component] of changeset.adds) {
306
316
  if (isDontFragmentRelation(componentType)) {
307
- if (!entityRelations) {
308
- entityRelations = new Map();
309
- dontFragmentRelations.set(entityId, entityRelations);
310
- }
311
- entityRelations.set(componentType, component);
317
+ dontFragmentRelations.setValue(entityId, componentType, component);
312
318
  }
313
319
  }
314
-
315
- // Clean up empty map
316
- if (entityRelations && entityRelations.size === 0) {
317
- dontFragmentRelations.delete(entityId);
318
- }
319
320
  }
320
321
 
321
322
  function applyDontFragmentChangesNoHooks(
@@ -323,30 +324,17 @@ function applyDontFragmentChangesNoHooks(
323
324
  entityId: EntityId,
324
325
  changeset: ComponentChangeset,
325
326
  ): void {
326
- let entityRelations = dontFragmentRelations.get(entityId);
327
-
328
327
  for (const componentType of changeset.removes) {
329
328
  if (isDontFragmentRelation(componentType)) {
330
- if (entityRelations) {
331
- entityRelations.delete(componentType);
332
- }
329
+ dontFragmentRelations.deleteValue(entityId, componentType);
333
330
  }
334
331
  }
335
332
 
336
333
  for (const [componentType, component] of changeset.adds) {
337
334
  if (isDontFragmentRelation(componentType)) {
338
- if (!entityRelations) {
339
- entityRelations = new Map();
340
- dontFragmentRelations.set(entityId, entityRelations);
341
- }
342
- entityRelations.set(componentType, component);
335
+ dontFragmentRelations.setValue(entityId, componentType, component);
343
336
  }
344
337
  }
345
-
346
- // Clean up empty map
347
- if (entityRelations && entityRelations.size === 0) {
348
- dontFragmentRelations.delete(entityId);
349
- }
350
338
  }
351
339
 
352
340
  export function filterRegularComponentTypes(componentTypes: Iterable<EntityId<any>>): EntityId<any>[] {