Skip to content

Commit c8d910b

Browse files
committed
properly handle case where a relation gets removed from an entity
1 parent 1d8e2f5 commit c8d910b

File tree

2 files changed

+65
-33
lines changed

2 files changed

+65
-33
lines changed

packages/hypergraph/src/entity/entityRelationParentsMap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ import type { DecodedEntitiesCacheEntry } from './decodedEntitiesCache.js';
22

33
export const entityRelationParentsMap: Map<
44
string, // entity ID
5-
Array<DecodedEntitiesCacheEntry>
5+
Map<DecodedEntitiesCacheEntry, number>
66
> = new Map();

packages/hypergraph/src/entity/findMany.ts

Lines changed: 64 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const subscribeToDocumentChanges = (handle: DocHandle<DocumentContent>) => {
4444
// reference to reduce the amount of O(n) operations per query to 1
4545
const touchedQueries = new Set<Array<string>>();
4646

47+
// collect all entities that used this entity as a entry in on of their relation fields
4748
const touchedRelationParents = new Set<DecodedEntitiesCacheEntry>();
4849

4950
// loop over all changed entities and update the cache
@@ -55,6 +56,7 @@ const subscribeToDocumentChanges = (handle: DocHandle<DocumentContent>) => {
5556
const cacheEntry = decodedEntitiesCache.get(typeName);
5657
if (!cacheEntry) continue;
5758

59+
const oldDecodedEntry = cacheEntry.entities.get(entityId);
5860
const relations = getEntityRelations(entity, cacheEntry.type, doc);
5961
const decoded = cacheEntry.decoder({
6062
...entity,
@@ -63,6 +65,51 @@ const subscribeToDocumentChanges = (handle: DocHandle<DocumentContent>) => {
6365
});
6466
cacheEntry.entities.set(entityId, decoded);
6567

68+
if (oldDecodedEntry) {
69+
// collect all the Ids for relation entries that don't exist in the `decoded` entry, but did in the `oldDecodedEntry`
70+
const deletedRelationIds = new Set<string>();
71+
for (const [fieldName, value] of Object.entries(oldDecodedEntry)) {
72+
if (Array.isArray(value)) {
73+
for (const relationEntity of value) {
74+
// @ts-expect-error decoded is a valid object
75+
if (!decoded[fieldName]?.includes(relationEntity.id)) {
76+
deletedRelationIds.add(relationEntity.id);
77+
}
78+
}
79+
}
80+
}
81+
82+
// it's fine to remove all of them since they are re-added below
83+
for (const deletedRelationId of deletedRelationIds) {
84+
const deletedRelationEntry = entityRelationParentsMap.get(deletedRelationId);
85+
if (deletedRelationEntry) {
86+
deletedRelationEntry.set(cacheEntry, (deletedRelationEntry.get(cacheEntry) ?? 0) - 1);
87+
if (deletedRelationEntry.get(cacheEntry) === 0) {
88+
deletedRelationEntry.delete(cacheEntry);
89+
}
90+
if (deletedRelationEntry.size === 0) {
91+
entityRelationParentsMap.delete(deletedRelationId);
92+
}
93+
}
94+
}
95+
}
96+
97+
// @ts-expect-error decoded is a valid object
98+
for (const [key, value] of Object.entries(decoded)) {
99+
if (Array.isArray(value)) {
100+
for (const relationEntity of value) {
101+
let relationParentEntry = entityRelationParentsMap.get(relationEntity.id);
102+
if (relationParentEntry) {
103+
relationParentEntry.set(cacheEntry, (relationParentEntry.get(cacheEntry) ?? 0) + 1);
104+
} else {
105+
relationParentEntry = new Map();
106+
entityRelationParentsMap.set(relationEntity.id, relationParentEntry);
107+
relationParentEntry.set(cacheEntry, 1);
108+
}
109+
}
110+
}
111+
}
112+
66113
const query = cacheEntry.queries.get('all');
67114
if (query) {
68115
const index = query.data.findIndex((entity) => entity.id === entityId);
@@ -72,31 +119,17 @@ const subscribeToDocumentChanges = (handle: DocHandle<DocumentContent>) => {
72119
query.data.push(decoded);
73120
}
74121
touchedQueries.add([typeName, 'all']);
75-
76-
// @ts-expect-error decoded is a valid object
77-
for (const [key, value] of Object.entries(decoded)) {
78-
if (Array.isArray(value)) {
79-
for (const relationEntity of value) {
80-
let relationParentEntry = entityRelationParentsMap.get(relationEntity.id);
81-
if (!relationParentEntry) {
82-
relationParentEntry = [];
83-
entityRelationParentsMap.set(relationEntity.id, relationParentEntry);
84-
}
85-
86-
relationParentEntry.push(cacheEntry);
87-
}
88-
}
89-
}
90122
}
91123

92124
entityTypes.add(typeName);
93125

94-
// gather all the decodedEntitiesCacheEntries
126+
// gather all the decodedEntitiesCacheEntries that have a relation to this entity to
127+
// invoke their query listeners below
95128
if (entityRelationParentsMap.has(entityId)) {
96129
const decodedEntitiesCacheEntries = entityRelationParentsMap.get(entityId);
97130
if (!decodedEntitiesCacheEntries) return;
98131

99-
for (const entry of decodedEntitiesCacheEntries) {
132+
for (const [entry] of decodedEntitiesCacheEntries) {
100133
touchedRelationParents.add(entry);
101134
}
102135
}
@@ -121,17 +154,20 @@ const subscribeToDocumentChanges = (handle: DocHandle<DocumentContent>) => {
121154
}
122155
}
123156

124-
// gather all the queries of impacted parent relation queries
157+
// gather all the queries of impacted parent relation queries and then remove the cacheEntry
125158
if (entityRelationParentsMap.has(entityId)) {
126159
const decodedEntitiesCacheEntries = entityRelationParentsMap.get(entityId);
127160
if (!decodedEntitiesCacheEntries) return;
128161

129-
for (const entry of decodedEntitiesCacheEntries) {
162+
for (const [entry] of decodedEntitiesCacheEntries) {
130163
touchedRelationParents.add(entry);
131164
}
165+
166+
entityRelationParentsMap.delete(entityId);
132167
}
133168
}
134169

170+
// update the queries affected queries
135171
for (const [typeName, queryKey] of touchedQueries) {
136172
const cacheEntry = decodedEntitiesCache.get(typeName);
137173
if (!cacheEntry) continue;
@@ -155,7 +191,6 @@ const subscribeToDocumentChanges = (handle: DocHandle<DocumentContent>) => {
155191
}
156192

157193
// trigger all the listeners of the parent relation queries
158-
// TODO: align with the touchedQueries to avoid unnecessary trigger calls
159194
for (const decodedEntitiesCacheEntry of touchedRelationParents) {
160195
decodedEntitiesCacheEntry.isInvalidated = true;
161196
for (const query of decodedEntitiesCacheEntry.queries.values()) {
@@ -278,12 +313,13 @@ export function subscribeToFindMany<const S extends AnyNoContext>(
278313
if (Array.isArray(value)) {
279314
for (const relationEntity of value) {
280315
let relationParentEntry = entityRelationParentsMap.get(relationEntity.id);
281-
if (!relationParentEntry) {
282-
relationParentEntry = [];
316+
if (relationParentEntry) {
317+
relationParentEntry.set(cacheEntry, (relationParentEntry.get(cacheEntry) ?? 0) + 1);
318+
} else {
319+
relationParentEntry = new Map();
283320
entityRelationParentsMap.set(relationEntity.id, relationParentEntry);
321+
relationParentEntry.set(cacheEntry, 1);
284322
}
285-
286-
relationParentEntry.push(cacheEntry);
287323
}
288324
}
289325
}
@@ -317,16 +353,12 @@ export function subscribeToFindMany<const S extends AnyNoContext>(
317353
// if the last query is removed, cleanup the entityRelationParentsMap and remove the decodedEntitiesCacheEntry
318354
if (cacheEntry.queries.size === 0) {
319355
entityRelationParentsMap.forEach((relationCacheEntries, key) => {
320-
for (const relationCacheEntry of relationCacheEntries) {
321-
if (relationCacheEntry === cacheEntry) {
322-
entityRelationParentsMap.set(
323-
key,
324-
relationCacheEntries.filter((entry) => entry !== cacheEntry),
325-
);
356+
for (const [relationCacheEntry, counter] of relationCacheEntries) {
357+
if (relationCacheEntry === cacheEntry && counter === 0) {
358+
relationCacheEntries.delete(cacheEntry);
326359
}
327360
}
328-
const updatedRelationCacheEntries = entityRelationParentsMap.get(key);
329-
if (updatedRelationCacheEntries && updatedRelationCacheEntries.length === 0) {
361+
if (relationCacheEntries.size === 0) {
330362
entityRelationParentsMap.delete(key);
331363
}
332364
});

0 commit comments

Comments
 (0)