Skip to content

Commit e5704a2

Browse files
committed
Refactor thumbnail cache
The thumbnail cache was trying to fit two concepts into one structure -- icons and multi-dimensional thumbnail arrays. This was awkward originally, but now that we are moving towards ditching the concept of a list of thumbnail types it really doesn't fit any longer. We can do better by having the cache store two types of thumbnails: icons and images. These are reflected explicitly now via TS interfaces.
1 parent 89d6200 commit e5704a2

File tree

2 files changed

+89
-28
lines changed

2 files changed

+89
-28
lines changed

src/app/shared/utilities/thumbnail-cache/thumbnail-cache.spec.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe('ThumbnailCache', () => {
3030
it('should return an empty FolderThumbData object for uncached thumbnail', () => {
3131
const thumbs = cache.getThumbnail(folder);
3232

33-
expect(thumbs.folderThumb).toBeDefined();
33+
expect(thumbs.folderThumb).toBe('');
3434
expect(thumbs.folderContentsType).toBe(
3535
FolderContentsType.BROKEN_THUMBNAILS,
3636
);
@@ -91,8 +91,20 @@ describe('ThumbnailCache', () => {
9191
expect(cache.hasThumbnail(folder)).toBeFalse();
9292
});
9393

94-
it('handles linking another value instead of a string tuple', () => {
95-
storage.session.set('folderThumbnailCache', [[1234, 'potato']]);
94+
it('reports no cache hit for stale entries', () => {
95+
storage.session.set('folderThumbnailCache', [
96+
[1234, ['https://old200', 'https://old500']],
97+
]);
98+
cache = new ThumbnailCache(storage);
99+
100+
expect(cache.hasThumbnail(folder)).toBeFalse();
101+
expect(storage.session.get('folderThumbnailCache').length).toBe(0);
102+
});
103+
104+
it('wipes stale [thumb200, thumb500] tuples from old format', () => {
105+
storage.session.set('folderThumbnailCache', [
106+
[1234, ['https://old200', 'https://old500']],
107+
]);
96108
cache = new ThumbnailCache(storage);
97109
const thumbz = cache.getThumbnail(folder);
98110

@@ -104,13 +116,19 @@ describe('ThumbnailCache', () => {
104116
expect(storage.session.get('folderThumbnailCache').length).toBe(0);
105117
});
106118

107-
it('handles properly casting other values to string if a tuple is provided', () => {
108-
storage.session.set('folderThumbnailCache', [[1234, [3.141, {}]]]);
119+
it('wipes entries with unrecognized shape', () => {
120+
storage.session.set('folderThumbnailCache', [
121+
[1234, { something: 'unexpected' }],
122+
]);
109123
cache = new ThumbnailCache(storage);
110124
const thumbz = cache.getThumbnail(folder);
111125

112-
expect(thumbz.folderThumb).toBe('3.141');
113-
expect(thumbz.folderContentsType).toBe(FolderContentsType.NORMAL);
126+
expect(thumbz.folderThumb).toBe('');
127+
expect(thumbz.folderContentsType).toBe(
128+
FolderContentsType.BROKEN_THUMBNAILS,
129+
);
130+
131+
expect(storage.session.get('folderThumbnailCache').length).toBe(0);
114132
});
115133
});
116134
});

src/app/shared/utilities/thumbnail-cache/thumbnail-cache.ts

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,40 @@ export interface FolderThumbData {
77
folderContentsType: FolderContentsType;
88
}
99

10+
enum CachedThumbnailType {
11+
Image = 'image',
12+
Icon = 'icon',
13+
}
14+
15+
interface CachedThumbnailBase {
16+
type: CachedThumbnailType;
17+
}
18+
19+
interface CachedImage extends CachedThumbnailBase {
20+
type: CachedThumbnailType.Image;
21+
url: string;
22+
}
23+
24+
interface CachedIcon extends CachedThumbnailBase {
25+
type: CachedThumbnailType.Icon;
26+
icon: FolderContentsType;
27+
}
28+
29+
type CachedThumbnail = CachedImage | CachedIcon;
30+
31+
function isCachedThumbnail(entry: unknown): entry is CachedThumbnail {
32+
return (
33+
typeof entry === 'object' &&
34+
entry !== null &&
35+
'type' in entry &&
36+
Object.values(CachedThumbnailType).includes(
37+
(entry as CachedThumbnailBase).type,
38+
)
39+
);
40+
}
41+
1042
export class ThumbnailCache {
11-
private cache: Map<number, [string, string]>;
43+
private cache: Map<number, CachedThumbnail>;
1244
private readonly STORAGE_KEY: string = 'folderThumbnailCache';
1345

1446
constructor(private storage: StorageService) {
@@ -18,35 +50,38 @@ export class ThumbnailCache {
1850
public saveThumbnail(item: ItemVO, thumbs: FolderThumbData): void {
1951
this.fetchCacheMapFromStorage();
2052
if (thumbs.folderContentsType === FolderContentsType.NORMAL) {
21-
this.cache.set(item.folder_linkId, [
22-
thumbs.folderThumb,
23-
'',
24-
]);
53+
this.cache.set(item.folder_linkId, {
54+
type: CachedThumbnailType.Image,
55+
url: thumbs.folderThumb,
56+
});
2557
} else {
26-
this.cache.set(item.folder_linkId, ['icon', thumbs.folderContentsType]);
58+
this.cache.set(item.folder_linkId, {
59+
type: CachedThumbnailType.Icon,
60+
icon: thumbs.folderContentsType,
61+
});
2762
}
2863
this.saveMapToStorage();
2964
}
3065

3166
public getThumbnail(item: ItemVO): FolderThumbData {
3267
if (this.cache.has(item.folder_linkId)) {
33-
const thumbs = this.cache.get(item.folder_linkId);
34-
if (thumbs && Array.isArray(thumbs) && thumbs.length > 1) {
35-
if (thumbs[0] === 'icon') {
68+
const entry = this.cache.get(item.folder_linkId);
69+
if (isCachedThumbnail(entry)) {
70+
if (entry.type === CachedThumbnailType.Image) {
71+
return {
72+
folderThumb: entry.url,
73+
folderContentsType: FolderContentsType.NORMAL,
74+
};
75+
}
76+
if (entry.type === CachedThumbnailType.Icon) {
3677
return {
3778
folderThumb: '',
38-
folderContentsType: thumbs[1] as FolderContentsType,
79+
folderContentsType: entry.icon,
3980
};
4081
}
41-
// Cast to string just to be sure we actually have strings from our data structure.
42-
return {
43-
folderThumb: `${thumbs[0]}`,
44-
folderContentsType: FolderContentsType.NORMAL,
45-
};
46-
} else {
47-
this.cache.delete(item.folder_linkId);
48-
this.saveMapToStorage();
4982
}
83+
this.cache.delete(item.folder_linkId);
84+
this.saveMapToStorage();
5085
}
5186
return {
5287
folderThumb: '',
@@ -55,7 +90,15 @@ export class ThumbnailCache {
5590
}
5691

5792
public hasThumbnail(item: ItemVO): boolean {
58-
return this.cache.has(item.folder_linkId);
93+
if (!this.cache.has(item.folder_linkId)) {
94+
return false;
95+
}
96+
if (isCachedThumbnail(this.cache.get(item.folder_linkId))) {
97+
return true;
98+
}
99+
this.cache.delete(item.folder_linkId);
100+
this.saveMapToStorage();
101+
return false;
59102
}
60103

61104
public invalidateFolder(folderLinkId: number): void {
@@ -68,9 +111,9 @@ export class ThumbnailCache {
68111
private fetchCacheMapFromStorage(): void {
69112
const cacheData = this.storage.session.get(this.STORAGE_KEY);
70113
if (cacheData && Array.isArray(cacheData)) {
71-
this.cache = new Map<number, [string, string]>(cacheData);
114+
this.cache = new Map<number, CachedThumbnail>(cacheData);
72115
} else {
73-
this.cache = new Map<number, [string, string]>();
116+
this.cache = new Map<number, CachedThumbnail>();
74117
this.saveMapToStorage();
75118
}
76119
}

0 commit comments

Comments
 (0)