Skip to content

Commit 96d77d3

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 52b0679 commit 96d77d3

File tree

3 files changed

+95
-34
lines changed

3 files changed

+95
-34
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
);
@@ -90,8 +90,20 @@ describe('ThumbnailCache', () => {
9090
expect(cache.hasThumbnail(folder)).toBeFalse();
9191
});
9292

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

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

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

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

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

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,43 +7,79 @@ export interface FolderThumbData {
77
folderContentsType: FolderContentsType;
88
}
99

10+
enum CachedThumbnailType {
11+
Image = 'image',
12+
Icon = 'icon',
13+
}
14+
15+
interface CachedImage {
16+
type: CachedThumbnailType.Image;
17+
url: string;
18+
}
19+
20+
interface CachedIcon {
21+
type: CachedThumbnailType.Icon;
22+
icon: FolderContentsType;
23+
}
24+
25+
type CachedThumbnail = CachedImage | CachedIcon;
26+
27+
function isCachedThumbnail(entry: unknown): entry is CachedThumbnail {
28+
return (
29+
typeof entry === 'object' &&
30+
entry !== null &&
31+
'type' in entry &&
32+
Object.values(CachedThumbnailType).includes(
33+
(entry as CachedImage | CachedIcon).type,
34+
)
35+
);
36+
}
37+
1038
export class ThumbnailCache {
11-
private cache: Map<number, [string, string]>;
39+
// Stores `unknown` because the cache is hydrated from local storage,
40+
// which may contain entries in an old or otherwise unexpected format.
41+
private cache: Map<number, unknown> = new Map();
1242
private readonly STORAGE_KEY: string = 'folderThumbnailCache';
1343

1444
constructor(private storage: StorageService) {
15-
this.fetchCacheMapFromStorage();
45+
this.refreshCacheFromStorage();
1646
}
1747

1848
public saveThumbnail(item: ItemVO, thumbs: FolderThumbData): void {
19-
this.fetchCacheMapFromStorage();
49+
this.refreshCacheFromStorage();
2050
if (thumbs.folderContentsType === FolderContentsType.NORMAL) {
21-
this.cache.set(item.folder_linkId, [thumbs.folderThumb, '']);
51+
this.cache.set(item.folder_linkId, {
52+
type: CachedThumbnailType.Image,
53+
url: thumbs.folderThumb,
54+
});
2255
} else {
23-
this.cache.set(item.folder_linkId, ['icon', thumbs.folderContentsType]);
56+
this.cache.set(item.folder_linkId, {
57+
type: CachedThumbnailType.Icon,
58+
icon: thumbs.folderContentsType,
59+
});
2460
}
25-
this.saveMapToStorage();
61+
this.commitCacheToStorage();
2662
}
2763

2864
public getThumbnail(item: ItemVO): FolderThumbData {
2965
if (this.cache.has(item.folder_linkId)) {
30-
const thumbs = this.cache.get(item.folder_linkId);
31-
if (thumbs && Array.isArray(thumbs) && thumbs.length > 1) {
32-
if (thumbs[0] === 'icon') {
66+
const cacheEntry = this.cache.get(item.folder_linkId);
67+
if (isCachedThumbnail(cacheEntry)) {
68+
if (cacheEntry.type === CachedThumbnailType.Image) {
69+
return {
70+
folderThumb: cacheEntry.url,
71+
folderContentsType: FolderContentsType.NORMAL,
72+
};
73+
}
74+
if (cacheEntry.type === CachedThumbnailType.Icon) {
3375
return {
3476
folderThumb: '',
35-
folderContentsType: thumbs[1] as FolderContentsType,
77+
folderContentsType: cacheEntry.icon,
3678
};
3779
}
38-
// Cast to string just to be sure we actually have strings from our data structure.
39-
return {
40-
folderThumb: `${thumbs[0]}`,
41-
folderContentsType: FolderContentsType.NORMAL,
42-
};
43-
} else {
44-
this.cache.delete(item.folder_linkId);
45-
this.saveMapToStorage();
4680
}
81+
this.cache.delete(item.folder_linkId);
82+
this.commitCacheToStorage();
4783
}
4884
return {
4985
folderThumb: '',
@@ -52,27 +88,34 @@ export class ThumbnailCache {
5288
}
5389

5490
public hasThumbnail(item: ItemVO): boolean {
55-
return this.cache.has(item.folder_linkId);
91+
if (!this.cache.has(item.folder_linkId)) {
92+
return false;
93+
}
94+
if (isCachedThumbnail(this.cache.get(item.folder_linkId))) {
95+
return true;
96+
}
97+
this.cache.delete(item.folder_linkId);
98+
this.commitCacheToStorage();
99+
return false;
56100
}
57101

58102
public invalidateFolder(folderLinkId: number): void {
59103
if (this.cache.has(folderLinkId)) {
60104
this.cache.delete(folderLinkId);
61-
this.saveMapToStorage();
105+
this.commitCacheToStorage();
62106
}
63107
}
64108

65-
private fetchCacheMapFromStorage(): void {
109+
private refreshCacheFromStorage(): void {
66110
const cacheData = this.storage.session.get(this.STORAGE_KEY);
67111
if (cacheData && Array.isArray(cacheData)) {
68-
this.cache = new Map<number, [string, string]>(cacheData);
112+
this.cache = new Map<number, unknown>(cacheData);
69113
} else {
70-
this.cache = new Map<number, [string, string]>();
71-
this.saveMapToStorage();
114+
this.cache = new Map<number, unknown>();
72115
}
73116
}
74117

75-
private saveMapToStorage(): void {
118+
private commitCacheToStorage(): void {
76119
this.storage.session.set(
77120
this.STORAGE_KEY,
78121
Array.from(this.cache.entries()),

src/app/views/components/timeline-view/timeline-templates.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ export const TimelineLineTemplate = `
55
`;
66
export const TimelineRecordTemplate = compile(`
77
<div class="timeline-item timeline-record">
8-
<img src={{item.thumbnailUrl}} style="width:{{imageWidth}}" height="{{imageHeight}}" width="{{imageWidth}}">
8+
<img src={{thumbnailUrl}} style="width:{{imageWidth}}" height="{{imageHeight}}" width="{{imageWidth}}">
99
</div>
1010
`);
1111

1212
export const TimelineFolderTemplate = compile(`
1313
<div class="timeline-item timeline-folder">
14-
<img src={{item.thumbnailUrl}} style="width:{{imageWidth}}">
14+
<img src={{thumbnailUrl}} style="width:{{imageWidth}}">
1515
<div>
1616
<div class="timeline-folder-name">{{item.displayName}}</div>
1717
{{#if item.FolderSizeVO}}

0 commit comments

Comments
 (0)