Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit df3b20b

Browse files
committed
Add simple profile cache invalidation
1 parent 0254328 commit df3b20b

File tree

4 files changed

+148
-41
lines changed

4 files changed

+148
-41
lines changed

src/stores/UserProfilesStores.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ limitations under the License.
1515
*/
1616

1717
import { logger } from "matrix-js-sdk/src/logger";
18-
import { IMatrixProfile, MatrixClient } from "matrix-js-sdk/src/matrix";
18+
import { IMatrixProfile, MatrixClient, MatrixEvent, RoomMember, RoomMemberEvent } from "matrix-js-sdk/src/matrix";
1919

2020
import { LruCache } from "../utils/LruCache";
2121

@@ -25,12 +25,17 @@ type StoreProfileValue = IMatrixProfile | undefined | null;
2525

2626
/**
2727
* This store provides cached access to user profiles.
28+
* Listens for membership events and invalidates the cache for a profile on update with different profile values.
2829
*/
2930
export class UserProfilesStore {
3031
private profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
3132
private knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
3233

33-
public constructor(private client?: MatrixClient) {}
34+
public constructor(private client?: MatrixClient) {
35+
if (client) {
36+
client.on(RoomMemberEvent.Membership, this.onRoomMembershipEvent);
37+
}
38+
}
3439

3540
/**
3641
* Synchronously get a profile from the store cache.
@@ -84,8 +89,6 @@ export class UserProfilesStore {
8489
* Null if the profile does not exist or there was an error fetching it.
8590
*/
8691
public async fetchOnlyKnownProfile(userId: string): Promise<StoreProfileValue> {
87-
console.log("fetchOnlyKnownProfile");
88-
8992
// Do not look up unknown users. The test for existence in knownProfiles is a performance optimisation.
9093
// If the user Id exists in knownProfiles we know them.
9194
if (!this.knownProfiles.has(userId) && !this.isUserIdKnown(userId)) return undefined;
@@ -124,4 +127,28 @@ export class UserProfilesStore {
124127
return !!room.getMember(userId);
125128
});
126129
}
130+
131+
/**
132+
* Simple cache invalidation if a room membership event is received and
133+
* at least one profile value differs from the cached one.
134+
*/
135+
private onRoomMembershipEvent = (event: MatrixEvent, member: RoomMember): void => {
136+
const profile = this.profiles.get(member.userId);
137+
138+
if (
139+
profile &&
140+
(profile.displayname !== member.rawDisplayName || profile.avatar_url !== member.getMxcAvatarUrl())
141+
) {
142+
this.profiles.delete(member.userId);
143+
}
144+
145+
const knownProfile = this.knownProfiles.get(member.userId);
146+
147+
if (
148+
knownProfile &&
149+
(knownProfile.displayname !== member.rawDisplayName || knownProfile.avatar_url !== member.getMxcAvatarUrl())
150+
) {
151+
this.knownProfiles.delete(member.userId);
152+
}
153+
};
127154
}

src/utils/LruCache.ts

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,21 @@ export class LruCache<K, V> {
124124
}
125125
}
126126

127+
/**
128+
* Deletes an item from the cache.
129+
*
130+
* @param key - Key of the item to be removed
131+
*/
132+
public delete(key: K): void {
133+
const item = this.map.get(key);
134+
135+
// Unknown item.
136+
if (!item) return;
137+
138+
this.removeItemFromList(item);
139+
this.map.delete(key);
140+
}
141+
127142
/**
128143
* Returns an iterator over the cached values.
129144
*/
@@ -143,17 +158,7 @@ export class LruCache<K, V> {
143158
// No update required.
144159
if (item === this.head) return item;
145160

146-
// Remove item from the list…
147-
if (item === this.tail && item.prev) {
148-
this.tail = item.prev;
149-
}
150-
151-
item.prev.next = item.next;
152-
153-
if (item.next) {
154-
item.next.prev = item.prev;
155-
}
156-
161+
this.removeItemFromList(item);
157162
// …and put it to the front.
158163
this.head.prev = item;
159164
item.prev = null;
@@ -162,4 +167,22 @@ export class LruCache<K, V> {
162167

163168
return item;
164169
}
170+
171+
private removeItemFromList(item: CacheItem<K, V>): void {
172+
if (item === this.head) {
173+
this.head = item.next ?? null;
174+
}
175+
176+
if (item === this.tail) {
177+
this.tail = item.prev ?? null;
178+
}
179+
180+
if (item.prev) {
181+
item.prev.next = item.next;
182+
}
183+
184+
if (item.next) {
185+
item.next.prev = item.prev;
186+
}
187+
}
165188
}

test/stores/UserProfilesStore-test.ts

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ limitations under the License.
1515
*/
1616

1717
import { mocked, Mocked } from "jest-mock";
18-
import { IMatrixProfile, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
18+
import { IMatrixProfile, MatrixClient, MatrixEvent, Room, RoomMemberEvent } from "matrix-js-sdk/src/matrix";
1919

2020
import { UserProfilesStore } from "../../src/stores/UserProfilesStores";
21-
import { filterConsole, mkRoomMemberJoinEvent, stubClient } from "../test-utils";
21+
import { filterConsole, mkRoomMember, mkRoomMemberJoinEvent, stubClient } from "../test-utils";
2222

2323
describe("UserProfilesStore", () => {
24-
const userUnknownId = "@unknown:example.com";
24+
const userIdDoesNotExist = "@unknown:example.com";
2525
const user1Id = "@user1:example.com";
2626
const user1Profile: IMatrixProfile = { displayname: "User 1", avatar_url: null };
2727
const user2Id = "@user2:example.com";
@@ -36,7 +36,7 @@ describe("UserProfilesStore", () => {
3636
"Error retrieving profile for userId @user3:example.com",
3737
);
3838

39-
beforeAll(() => {
39+
beforeEach(() => {
4040
mockClient = mocked(stubClient());
4141
room = new Room("!room:example.com", mockClient, mockClient.getSafeUserId());
4242
room.currentState.setStateEvents([
@@ -58,37 +58,67 @@ describe("UserProfilesStore", () => {
5858
expect(userProfilesStore.getProfile(user1Id)).toBeUndefined();
5959
});
6060

61-
it("fetchProfile should return the profile from the API and cache it", async () => {
62-
const profile = await userProfilesStore.fetchProfile(user1Id);
63-
expect(profile).toBe(user1Profile);
64-
expect(userProfilesStore.getProfile(user1Id)).toBe(user1Profile);
65-
});
61+
describe("fetchProfile", () => {
62+
it("should return the profile from the API and cache it", async () => {
63+
const profile = await userProfilesStore.fetchProfile(user1Id);
64+
expect(profile).toBe(user1Profile);
65+
expect(userProfilesStore.getProfile(user1Id)).toBe(user1Profile);
66+
});
6667

67-
it("fetchProfile for an unknown user should return null and cache it", async () => {
68-
const profile = await userProfilesStore.fetchProfile(userUnknownId);
69-
expect(profile).toBeNull();
70-
expect(userProfilesStore.getProfile(userUnknownId)).toBeNull();
68+
it("for an user that does not exist should return null and cache it", async () => {
69+
const profile = await userProfilesStore.fetchProfile(userIdDoesNotExist);
70+
expect(profile).toBeNull();
71+
expect(userProfilesStore.getProfile(userIdDoesNotExist)).toBeNull();
72+
});
7173
});
7274

7375
it("getOnlyKnownProfile should return undefined if the profile was not fetched", () => {
7476
expect(userProfilesStore.getOnlyKnownProfile(user1Id)).toBeUndefined();
7577
});
7678

77-
it("fetchOnlyKnownProfile should return undefined if no room shared with the user", async () => {
78-
const profile = await userProfilesStore.fetchOnlyKnownProfile(user1Id);
79-
expect(profile).toBeUndefined();
80-
expect(userProfilesStore.getOnlyKnownProfile(user1Id)).toBeUndefined();
81-
});
79+
describe("fetchOnlyKnownProfile", () => {
80+
it("should return undefined if no room shared with the user", async () => {
81+
const profile = await userProfilesStore.fetchOnlyKnownProfile(user1Id);
82+
expect(profile).toBeUndefined();
83+
expect(userProfilesStore.getOnlyKnownProfile(user1Id)).toBeUndefined();
84+
});
8285

83-
it("fetchOnlyKnownProfile for a known user should return the profile from the API and cache it", async () => {
84-
const profile = await userProfilesStore.fetchOnlyKnownProfile(user2Id);
85-
expect(profile).toBe(user2Profile);
86-
expect(userProfilesStore.getOnlyKnownProfile(user2Id)).toBe(user2Profile);
86+
it("for a known user should return the profile from the API and cache it", async () => {
87+
const profile = await userProfilesStore.fetchOnlyKnownProfile(user2Id);
88+
expect(profile).toBe(user2Profile);
89+
expect(userProfilesStore.getOnlyKnownProfile(user2Id)).toBe(user2Profile);
90+
});
91+
92+
it("for a known user not found via API should return null and cache it", async () => {
93+
const profile = await userProfilesStore.fetchOnlyKnownProfile(user3Id);
94+
expect(profile).toBeNull();
95+
expect(userProfilesStore.getOnlyKnownProfile(user3Id)).toBeNull();
96+
});
8797
});
8898

89-
it("fetchOnlyKnownProfile for a known user not found via API should return null and cache it", async () => {
90-
const profile = await userProfilesStore.fetchOnlyKnownProfile(user3Id);
91-
expect(profile).toBeNull();
92-
expect(userProfilesStore.getOnlyKnownProfile(user3Id)).toBeNull();
99+
describe("when there are cached values and membership updates", () => {
100+
beforeEach(async () => {
101+
await userProfilesStore.fetchProfile(user1Id);
102+
await userProfilesStore.fetchOnlyKnownProfile(user2Id);
103+
});
104+
105+
describe("and membership events with the same values appear", () => {
106+
beforeEach(() => {
107+
const roomMember1 = mkRoomMember(room.roomId, user1Id);
108+
roomMember1.rawDisplayName = user1Profile.displayname;
109+
roomMember1.getMxcAvatarUrl = () => null;
110+
mockClient.emit(RoomMemberEvent.Membership, {} as MatrixEvent, roomMember1);
111+
112+
const roomMember2 = mkRoomMember(room.roomId, user2Id);
113+
roomMember2.rawDisplayName = user2Profile.displayname;
114+
roomMember2.getMxcAvatarUrl = () => null;
115+
mockClient.emit(RoomMemberEvent.Membership, {} as MatrixEvent, roomMember2);
116+
});
117+
118+
it("should not invalidate the cache", () => {
119+
expect(userProfilesStore.getProfile(user1Id)).toBe(user1Profile);
120+
expect(userProfilesStore.getOnlyKnownProfile(user2Id)).toBe(user2Profile);
121+
});
122+
});
93123
});
94124
});

test/utils/LruCache-test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,33 @@ describe("LruCache", () => {
7474
cache.set("c", "c value");
7575
});
7676

77+
it("deleting an unkonwn item should not raise an error", () => {
78+
cache.delete("unknown");
79+
});
80+
81+
it("deleting the first item should work", () => {
82+
cache.delete("a");
83+
expect(Array.from(cache.values())).toEqual(["b value", "c value"]);
84+
});
85+
86+
it("deleting the item in the middle should work", () => {
87+
cache.delete("b");
88+
expect(Array.from(cache.values())).toEqual(["a value", "c value"]);
89+
});
90+
91+
it("deleting the last item should work", () => {
92+
cache.delete("c");
93+
expect(Array.from(cache.values())).toEqual(["a value", "b value"]);
94+
});
95+
96+
it("deleting and adding some items should work", () => {
97+
cache.set("d", "d value");
98+
cache.get("b");
99+
cache.delete("b");
100+
cache.set("e", "e value");
101+
expect(Array.from(cache.values())).toEqual(["c value", "d value", "e value"]);
102+
});
103+
77104
describe("and accesing the first added item and adding another item", () => {
78105
beforeEach(() => {
79106
cache.get("a");

0 commit comments

Comments
 (0)